- 表名统一 dda_ 前缀(dda_approvals/dda_approval_configs),跨应用复用防冲突 - 解耦关键改动:原回调硬编码 biz_type=='content_publish' + 写 cms_content 表, 改为 register_biz_handler(biz_type, handler) 钩子分派,模块内零业务表引用 - 含 models(四段式)/json(CRUD)/9个dspy端点/load_path(any回调+9 logined)/i18n四语言 - README 覆盖凭据配置、审批模板配置、回调地址、审批节点接法(产线步骤)
199 lines
7.1 KiB
Markdown
199 lines
7.1 KiB
Markdown
# dingdingflow — 钉钉审批工作流模块
|
||
|
||
从 cms 模块独立而来。**本模块只负责钉钉审批本身**(发起 / 查询 / 回调 / 模板配置),
|
||
不认识任何业务表。审批通过或驳回后要做什么,由宿主按 `biz_type` 注册钩子决定。
|
||
|
||
## 数据表(统一 `dda_` 前缀,避免跨应用复用时与业务表冲突)
|
||
|
||
| 表名 | 用途 |
|
||
|------|------|
|
||
| `dda_approvals` | 审批记录(每次发起一条,含钉钉实例 ID、状态、意见) |
|
||
| `dda_approval_configs` | 审批流程配置(biz_type → 钉钉审批模板 process_code) |
|
||
|
||
## 安装与集成
|
||
|
||
### 1. 安装
|
||
|
||
```bash
|
||
pip install -e /path/to/dingdingflow # 开发
|
||
# 或宿主 build.sh 里:pip install pkgs/dingdingflow/
|
||
```
|
||
|
||
### 2. 宿主入口注册(例:pipeline-app 的 `app/pipeline_app.py`)
|
||
|
||
```python
|
||
from dingdingflow.init import load_dingdingflow, register_biz_handler
|
||
|
||
load_dingdingflow() # 注册所有函数到 ServerEnv
|
||
```
|
||
|
||
### 3. 建表
|
||
|
||
`models/*.json` 是标准四段式表定义,用 `json2ddl` 生成 DDL:
|
||
|
||
```bash
|
||
cd pkgs/dingdingflow/models && json2ddl mysql . > /tmp/dda_ddl.sql
|
||
mysql -h <host> -u<user> -p<pwd> <db> < /tmp/dda_ddl.sql
|
||
```
|
||
|
||
### 4. RBAC 权限
|
||
|
||
```bash
|
||
py3/bin/python pkgs/dingdingflow/scripts/load_path.py
|
||
```
|
||
|
||
注册 9 个 logined 端点 + 1 个 any 端点(`dingtalk_callback.dspy` 必须 any,
|
||
因为钉钉服务器回调没有登录态;安全性靠回调内部用 `processInstanceId`
|
||
匹配本地记录,匹配不到直接拒绝)。
|
||
|
||
### 5. wwwroot 软链 + 菜单
|
||
|
||
```bash
|
||
ln -sf ../pkgs/dingdingflow/wwwroot wwwroot/dingdingflow
|
||
```
|
||
|
||
---
|
||
|
||
## 配置在哪里设
|
||
|
||
### A. 钉钉应用凭据 → 环境变量
|
||
|
||
放宿主的 `init/.dingdingflow` 文件(不入库,`start.sh` 里 source):
|
||
|
||
```bash
|
||
export DINGTALK_APP_KEY=your_app_key
|
||
export DINGTALK_APP_SECRET=your_app_secret
|
||
export DINGTALK_AGENT_ID=your_agent_id
|
||
```
|
||
|
||
**缺失这三个变量时 `dingtalk_client` 自动走 mock 模式**(返回假的
|
||
access_token 和实例 ID),可在没有钉钉环境时先把流程跑通。
|
||
|
||
去哪拿:钉钉开放平台 → 应用开发 → 企业内部应用 → 凭据与基础信息。
|
||
需要的权限:审批实例创建、审批实例读取、审批回调订阅。
|
||
|
||
### B. 审批模板映射 → `dda_approval_configs` 表
|
||
|
||
每种业务类型配一条,把 `biz_type` 映射到钉钉审批模板:
|
||
|
||
| 字段 | 说明 | 示例 |
|
||
|------|------|------|
|
||
| `biz_type` | 业务类型标识(代码里 `submit_approval` 的第一个参数) | `pipeline_deploy` |
|
||
| `biz_type_title` | 业务类型显示名 | `产线部署审批` |
|
||
| `process_code` | **钉钉审批模板编码** | `PROC-XXXX-XXXX` |
|
||
| `agent_id` | 钉钉应用 AgentId | `1234567` |
|
||
| `form_config` | 表单字段映射 JSON(本地字段 → 钉钉表单控件) | 见下 |
|
||
| `is_active` | 是否启用 `1`/`0` | `1` |
|
||
|
||
`process_code` 去哪拿:钉钉管理后台 → 工作台 → 审批 → 选中模板 →
|
||
URL 里的 `processCode`,或用钉钉 API `/topapi/process/get_by_name` 查。
|
||
|
||
配置入口(两种都行):
|
||
- 页面:`/dingdingflow/api/dd_approval_configs_list.dspy`(CRUD 界面)
|
||
- 直接 SQL / 模块 `init/data.json` 种子
|
||
|
||
### C. 钉钉回调地址
|
||
|
||
钉钉开放平台 → 事件订阅 → 填:
|
||
|
||
```
|
||
https://<你的域名>/dingdingflow/api/dingtalk_callback.dspy
|
||
```
|
||
|
||
订阅事件类型:`bpms_instance_change`(审批实例状态变化)。
|
||
|
||
---
|
||
|
||
## 审批节点在哪里设
|
||
|
||
本模块提供「审批能力」,**审批节点挂在哪由宿主决定**。两种接法:
|
||
|
||
### 接法一:产线步骤即审批节点(推荐给 pipeline-app)
|
||
|
||
产线的步骤定义在 `pipeline_steps` 表,`step_type` 决定用哪个 handler。
|
||
所以审批节点 = 一个 `step_type='dingtalk_approval'` 的步骤:
|
||
|
||
```python
|
||
# pipeline_service/handlers_approval.py
|
||
from dingdingflow.init import submit_approval, register_biz_handler
|
||
|
||
async def handle_dingtalk_approval(tenant_id, task_id, step_name, input_data, config):
|
||
"""审批步骤 handler:发起钉钉审批后挂起,等回调推进。"""
|
||
r = await submit_approval(
|
||
biz_type=config.get('biz_type', 'pipeline_step'),
|
||
biz_id=f"{task_id}:{step_name}", # 回调靠这个定位任务和步骤
|
||
title=config.get('title', f'{step_name} 审批'),
|
||
applicant_id=input_data.get('applicant_id', ''),
|
||
)
|
||
# 返回后由引擎把步骤置为等待人工状态,不阻塞 worker
|
||
return {'approval_id': r.get('approval_id'), 'status': 'waiting_approval'}
|
||
|
||
register_handler('dingtalk_approval', handle_dingtalk_approval)
|
||
```
|
||
|
||
再注册回调钩子,把审批结果写回产线:
|
||
|
||
```python
|
||
async def on_pipeline_approval(biz_id, status, approval_id, comment):
|
||
task_id, step_name = biz_id.split(':', 1)
|
||
env = ServerEnv()
|
||
if status == 'approved':
|
||
await env.approval_approve(tenant_id, task_id, step_name, 'dingtalk', comment)
|
||
else:
|
||
await env.approval_reject(tenant_id, task_id, step_name, 'dingtalk', comment)
|
||
|
||
register_biz_handler('pipeline_step', on_pipeline_approval)
|
||
```
|
||
|
||
**节点具体位置**:在 `pipeline_steps` 表插一行(或产线定义页面加一步):
|
||
|
||
```sql
|
||
INSERT INTO pipeline_steps (id, pipeline_id, step_name, step_type, step_order, step_config)
|
||
VALUES (<id>, <产线id>, 'deploy_approval', 'dingtalk_approval', 30,
|
||
'{"deps": ["build"], "biz_type": "pipeline_step", "title": "上线审批"}');
|
||
```
|
||
|
||
- `step_type` 必须是 `dingtalk_approval`(匹配上面注册的 handler 名)
|
||
- `deps` 控制它卡在哪个步骤之后(DAG 依赖)
|
||
- 后续步骤把它写进自己的 `deps`,就实现「审批通过才继续」
|
||
|
||
### 接法二:任意业务动作前置审批
|
||
|
||
不走产线引擎,直接在业务代码里调:
|
||
|
||
```python
|
||
r = await submit_approval('content_publish', content_id, '发布审批', user_id)
|
||
# ...钉钉审批中...
|
||
# 回调时你注册的 handler 被调用,在里面做真正的业务动作
|
||
register_biz_handler('content_publish', my_publish_handler)
|
||
```
|
||
|
||
---
|
||
|
||
## 对外函数(`load_dingdingflow()` 注册到 ServerEnv)
|
||
|
||
| 函数 | 用途 |
|
||
|------|------|
|
||
| `submit_approval(biz_type, biz_id, title, applicant_id, org_id)` | 发起审批 |
|
||
| `get_approval_status(approval_id)` | 查审批状态(会同步钉钉最新状态) |
|
||
| `handle_dingtalk_callback(data)` | 处理钉钉回调(回调 dspy 调它) |
|
||
| `register_biz_handler(biz_type, handler)` | **注册审批结果处理器** |
|
||
| `list_biz_handlers()` | 查已注册钩子(排障) |
|
||
| `dd_approvals_*` / `dd_approval_configs_*` | 两张表的 CRUD |
|
||
| `get_approval_config_by_type(org_id, biz_type)` | 查审批模板配置 |
|
||
|
||
`register_biz_handler` 的 handler 签名:
|
||
|
||
```python
|
||
async def handler(biz_id, status, approval_id, comment): ...
|
||
# status ∈ ('approved', 'rejected', 'cancelled')
|
||
```
|
||
|
||
## 设计约束
|
||
|
||
- **不硬编码库名**:走宿主的 `get_module_dbname('dingdingflow')` 映射
|
||
- **不认识业务表**:审批结果只通过 `_BIZ_HANDLERS` 分派,模块内零业务表引用
|
||
(这是从 cms 独立时改掉的:原代码把 `biz_type=='content_publish'` 和
|
||
写 `cms_content` 表硬编码在回调里,导致无法复用)
|
||
- **表名统一 `dda_` 前缀**:模块跨应用复用时不与业务表冲突
|