dingdingflow/README.md
ymq a3f2ea8616 feat: 钉钉审批工作流模块从 cms 独立
- 表名统一 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 覆盖凭据配置、审批模板配置、回调地址、审批节点接法(产线步骤)
2026-08-25 18:15:52 +08:00

199 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_` 前缀**:模块跨应用复用时不与业务表冲突