Skip to main content

approval-module

BiSheng 审批模块(审批中心 F025)的架构与代码参考。 覆盖统一审批网关、多场景引擎、多节点流转、outbox 业务执行、站内信通知、异常处理。 迭代审批功能或修复审批相关 Bug 前先读本 skill,可直接定位架构与代码锚点,无需全仓搜索。 TRIGGER when: 用户要改动/修复"审批""审批中心""approval"相关功能(菜单权限申请、频道订阅审批、 知识空间加入审批、审批流程/节点配置、异常处理、outbox/Celery 执行),或排查审批通过后业务未生效、 审批人看不到任务、站内信未发等问题。

معلومات المصدر

المستودع
dataelement/bisheng
آخر نشاط في المصدر
٢٨ يوليو ٢٠٢٦ في ٠٢:٥٥
لغة SKILL.md المكتشفة
الصينية
النجوم
١٢٬٠٣١
التفرعات
١٬٩٧٧

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
approval-module
description
BiSheng 审批模块(审批中心 F025)的架构与代码参考。 覆盖统一审批网关、多场景引擎、多节点流转、outbox 业务执行、站内信通知、异常处理。 迭代审批功能或修复审批相关 Bug 前先读本 skill,可直接定位架构与代码锚点,无需全仓搜索。 TRIGGER when: 用户要改动/修复"审批""审批中心""approval"相关功能(菜单权限申请、频道订阅审批、 知识空间加入审批、审批流程/节点配置、异常处理、outbox/Celery 执行),或排查审批通过后业务未生效、 审批人看不到任务、站内信未发等问题。
# 审批模块(审批中心 F025) ## ⚠️ 维护契约(修改代码后必读) **本 skill 是审批模块的唯一权威参考,必须与代码永远一致。** 当你改动以下任意一项时,**同一个改动里必须同步更新本文件对应章节**,否则视为改动未完成: - 主流程分支逻辑(`ApprovalGate.request_or_pass` 的 pass/flow/exception 分流、`decide_task` / `_advance_after_node_approved` 的节点流转)→ 更新 [§2 架构与主流程](#2-架构与主流程) - 新增/删除/重命名服务文件或关键方法 → 更新 [§3 代码锚点](#3-代码锚点) - 新增/删除预置场景或改动其触发入口、Handler → 更新 [§4 预置场景](#4-预置场景) - 数据库表/状态枚举变化 → 更新 [§5 数据库表](#5-数据库表) - API 路由增删改 → 更新 [§7 API 列表](#7-api-列表) - 站内信触发时机/接收人变化 → 更新 [§8 站内信通知矩阵](#8-站内信通知矩阵) - Celery 队列/路由变化 → 更新 [§6 outbox 与 Celery](#6-outbox-与-celery) > 自检:改完代码后问自己"本 skill 里有没有哪句话现在变成假的了?"——有就改它。 --- ## 1. 概述 审批中心是一套**通用多场景审批引擎**,所有场景共用同一套网关 / 路由 / 流程 / 节点 / 实例 / 任务 / outbox 机制。 **核心原则:审批"通过"与"执行业务"解耦为两步**——通过后只写 `approval_outbox(PENDING)`,由 Celery 异步执行业务 `on_approved()`,成功后实例才置 `EXECUTED`。 > ⚠️ **已废弃**:另有一套独立的旧系统——部门知识空间文件上传审批(`approval_request` 表),由 `approval_service.py` + `message_handler.py` 承载,路由在 `/approval/requests/*` 与 `/approval/department-knowledge-space/*`。该功能**已废弃**,仅为兼容存量保留,**不要在其上新增功能**;新需求一律走审批中心引擎。改审批中心时也不要误改它。 --- ## 2. 架构与主流程 ``` 申请人触发业务入口 │ ▼ ApprovalGate.request_or_pass() ← 统一网关,所有场景从这里进入 │ 路由匹配 (approval_route_rule 表,按 sort_order 自上而下) │ ┌────┴───────────────────────────┐ │ pass 分支 (route_type=pass) │ → instance(APPROVED) + outbox → Celery → on_approved() → EXECUTED │ flow 分支 (route_type=flow) │ → instance(PENDING) + 首节点 task(PENDING) → 等待审批人 │ 无分支命中 │ → instance(EXCEPTION, route_missing) + 通知管理员 │ 审批人解析为空 │ → instance(EXCEPTION, approver_empty) + 通知管理员 └────────────────────────────────┘ │ (flow 分支被审批人处理) ▼ ApprovalCenterService.decide_task() │ 通过 → _advance_after_node_approved() ├── 有后续节点(node_order 更大) → 解析下一节点审批人 + 建 tasks + 通知审批人;解析为空 → EXCEPTION(approver_empty) └── 无后续节点(最后节点) → instance(APPROVED) + outbox → Celery → EXECUTED + 通知申请人 拒绝 → instance(REJECTED) + 通知申请人 撤回 → instance(WITHDRAWN) + 通知有 task 的审批人 ``` **多节点 / 会签**:`_advance_after_node_approved()` 实现顺序流转。 - OR 节点(`node_mode=or`):任一人通过即把同节点其余 PENDING task 置 SKIPPED 并 advance。 - AND 节点(`node_mode=and`):同节点全部通过才 advance。 - finalize 时若 `handler_key` 未注册,记录 error 后仍照常 APPROVED + 建 outbox(避免卡死)。 **异常实例也留痕**:`_create_exception_result()` 在创建异常后会补写 `action='approval.request.submit'` 审计日志(与正常 PENDING/PASS 分支一致)。 --- ## 3. 代码锚点 > 路径相对 `src/backend/bisheng/`。这些是定位问题的第一入口。 ### 后端服务 | 文件 | 职责 | 关键方法 | |------|------|---------| | `approval/domain/services/approval_gate.py` | 统一入口:路由匹配、实例创建、pass/pending/exception 分流 | `request_or_pass()`、`_create_exception_result()`、`_notify_admins_of_exception()` | | `approval/domain/services/approval_center_service.py` | 用户端:任务列表/详情、同意/拒绝、撤回、菜单申请、多节点流转 | `decide_task()`、`_advance_after_node_approved()`、`_dispatch_outbox()`、`_send_approval_notify()` | | `approval/domain/services/approval_exception_service.py` | 管理端异常处理:重试/指定审批人/跳过节点/取消/标记完成 | `assign_approvers()`、`_resolve_exception_node()` | | `approval/domain/services/approval_outbox_service.py` | outbox 执行与重试;成功后置 instance=EXECUTED | `execute_outbox()`、`retry_outbox()` | | `approval/domain/services/approval_scenario_admin_service.py` | 管理端:场景/分支/流程/节点配置、异常列表 | — | | `approval/domain/services/approver_resolver.py` | 解析审批人来源 `direct_user` / `department_admin` / `tenant_admin` | `resolve_approvers_from_sources()` | | `approval/domain/services/approval_registry.py` | 场景预置目录 + handler 注册表 | `with_default_presets()`、`register_handler()`、`get_handler()` | | `approval/domain/services/approval_runtime_handler_factory.py` | 为 outbox 执行 / 多节点 advance 重新构造运行时 handler | `build_runtime_handler(scenario_code)` | | `approval/domain/services/approval_notification_service.py` | 站内信统一封装 | `notify_user()` / `notify_users()` / `notify_admins()` | | `approval/domain/services/user_menu_access_service.py` | 菜单授权增删查,含父级菜单依赖自动补全 | `grant_menu_access()`、`revoke_menu_access()`、`ensure_application_allowed()` | | `approval/domain/services/approval_service.py` + `message_handler.py` | **旧系统(已废弃)**:部门知识空间文件上传审批(`approval_request` 表),与审批中心独立,仅兼容存量、勿新增功能 | `ApprovalService.decide_request()` | | `worker/approval/tasks.py` | Celery 任务(走默认 `celery` 队列) | `execute_approval_outbox`、`retry_approval_outbox` | | `worker/config.py` | Celery 路由配置(审批任务**不**配路由,fall through 到默认队列) | `task_routes` | | `approval/api/endpoints/approval_user.py` | Client 端 API(`/api/v1/approval/...`) | — | | `approval/api/endpoints/approval_admin.py` | Platform 管理 API(`/api/v1/approval/admin/...`) | — | | `approval/api/endpoints/approval.py` | 旧系统 legacy API(`/api/v1/approval/requests/...`),**已废弃** | — | ### 三个场景 Handler | 文件 | 类 | |------|----| | `approval/domain/services/menu_access_handler.py` | `MenuAccessApprovalHandler` | | `approval/domain/services/channel_subscribe_scenario_handler.py` | `ChannelSubscribeScenarioHandler` | | `approval/domain/services/knowledge_space_subscribe_scenario_handler.py` | `KnowledgeSpaceSubscribeScenarioHandler` | ### 前端 | 文件 | 职责 | |------|------| | `src/frontend/client/src/components/approval/ApprovalCenterDialog.tsx` | 审批中心弹窗(我的审批 + 我的申请 + 时间线) | | `src/frontend/client/src/api/approval.ts` | 审批 API 封装,含 `ApprovalApiError`(非 200 自动抛出) | | `src/frontend/client/src/pages/MenuUnavailablePage.tsx` | 无权限占位页 + 申请入口 | | `src/frontend/client/src/layouts/MenuApprovalPluginGate.tsx` | 菜单审批路由守卫 | | `src/frontend/platform/src/pages/ApprovalPage/index.tsx` | 管理后台审批页(场景/分支/流程/节点/异常) | | `src/frontend/platform/src/controllers/API/approval.ts` | Platform 审批 API 封装 | --- ## 4. 预置场景 三个场景由 `ApprovalRegistry.with_default_presets()` 注册(仅是"目录/下拉来源",**不等于已启用**)。每个场景的业务入口在创建 `ApprovalGateRequest` 时**都需要传 `applicant_department_id`**(供 `department_admin` 审批人来源使用,查 `UserDepartmentDao.aget_user_primary_department()`)。 **首次部署自动落库**:4.2 频道订阅审批、4.3 知识空间加入审批由 `common/init_data.py::_init_default_approval_scenarios()`(在 `init_default_data` 内)为默认租户幂等 seed——各建「默认分支(catch-all, route_type=flow) → 默认流程 → 单节点(node_mode=or 或签)」,审批人来源即资源 owner+manager(频道 `channel_owner`/`channel_manager`,知识空间 `knowledge_space_owner`/`knowledge_space_manager`),场景 `enabled=True`。按 `tenant_id+scenario_code` 判存在即跳过,绝不覆盖人工改动。菜单权限申请(4.1)**不**自动 seed。新租户不自动 seed,需管理后台手工配置。 ### 4.1 菜单权限申请 (`menu_access_request`) - **入口**:Client `/workspace/menu-unavailable?plugin=xxx` → `POST /api/v1/approval/menu-access/apply` - **Handler**:`MenuAccessApprovalHandler` - `on_approved` 调 `UserMenuAccessService.grant_menu_access()`,自动补父级依赖(如 `knowledge_space` → 同时授权 `workstation`);`on_revoke` 调 `revoke_menu_access()` - 申请前校验 `ensure_application_allowed()`(`menu_approval_mode=false` 或已有权限时拒绝) ### 4.2 频道订阅审批 (`channel_subscribe_request`) - **入口**:`channel/domain/services/channel_service.py::subscribe_channel()`(`REVIEW` 可见性频道) - **Handler**:`ChannelSubscribeScenarioHandler` - 通过 / pass 路径调 `ChannelService.sync_direct_channel_user_permissions()` 写 ReBAC(OpenFGA) 关系(否则成员不出现在 ReBAC 成员列表) - `on_approved` 先把申请人的 **PENDING** membership 翻成 ACTIVE 再写 ReBAC(查 membership 注意频道默认只返回 ACTIVE,激活需带非 ACTIVE 状态) - PENDING 时调 `_send_channel_approval_notification()` 通知审批人 ### 4.3 知识空间加入审批 (`knowledge_space_subscribe_request`) - **入口**:`knowledge/domain/services/knowledge_space_service.py::subscribe_space()`(`auth_type=APPROVAL`) - **Handler**:`KnowledgeSpaceSubscribeScenarioHandler` - 通过 / ACTIVE 路径调 `sync_direct_space_user_permissions()` 写 ReBAC 关系 - PENDING 时调 `_send_space_approval_notification()` 通知审批人 - **不变量:先过网关、再落 membership。** `subscribe_space` 对 APPROVAL 空间必须先 `await gate.request_or_pass()`,按 gate 结果(pass→ACTIVE / pending·exception→PENDING)才通过 `_persist_space_member()` 写 `space_channel_member`。**严禁在调网关前预写 PENDING membership**——否则场景未配置/未启用时网关 `raise ApprovalScenarioDisabledError`,但 PENDING 行已落库,下次点"关注"会被 `subscribe_space` 顶部"已 PENDING 直接返回 pending"的早退分支短路,掩盖错误(首次报错、二次假成功)。无场景时每次点击都应一致报错。 --- ## 5. 数据库表 | 表名 | 说明 | 关键状态字段 | |------|------|------------| | `approval_scenario` | 租户下启用的审批场景 | `enabled` | | `approval_route_rule` | 场景下条件分支(按 `sort_order` 匹配) | `route_type: pass/flow`、`enabled` | | `approval_flow_definition` | 审批流程定义头 | — | | `approval_flow_version` | 流程版本快照 | `is_active` | | `approval_node_definition` | 流程版本内顺序节点 | `node_order`、`node_mode: or/and`、`approver_config` | | `approval_instance` | 一次审批申请 | `pending/approved/rejected/withdrawn/executed/execute_failed/exception/cancelled` | | `approval_task` | 分配给审批人的节点待办 | `pending/approved/rejected/skipped/cancelled` | | `approval_exception` | 异常记录 | `open/resolved`,`exception_type: route_missing/approver_empty/execute_failed` | | `approval_outbox` | 业务执行队列 | `pending/success/failed` | | `approval_action_log` | 时间线日志 | — | | `user_menu_access` | 用户级菜单授权(菜单审批专用) | `active/revoked` | | `approval_request` | **旧系统(已废弃)**:部门知识空间文件上传审批,仅兼容存量 | — | > 模型定义见 `approval/domain/models/approval_instance.py`、`approval_scenario.py`、`user_menu_access.py`。 > `approval_instance.latest_approver_user_id` 字段已定义但**当前从未赋值**(已知限制,需要时在 `decide_task` 里补)。 --- ## 6. outbox 与 Celery 业务执行走 outbox:通过后写 `approval_outbox(PENDING)` → Celery `execute_approval_outbox` 执行 `handler.on_approved()` → 成功 outbox=SUCCESS、instance=EXECUTED;失败 outbox=FAILED、instance=EXECUTE_FAILED 并建 `execute_failed` 异常。 > **原则:业务回调(`on_approved` 等)不得静默失败。** 该执行成功/失败由「是否抛异常」判定:抛异常 → outbox=FAILED + `execute_failed` 异常暴露问题;正常返回 → 一律视为成功并置 instance=EXECUTED。因此前置条件缺失(如找不到要激活的 membership/资源)**必须 raise**,绝不能 `return {'status':'xxx'}` 之类把失败伪装成成功——否则会出现 instance=executed 但业务实际没生效的「假成功」,且无任何告警。 **dispatch 入口(两处,功能相同名字不同):** - `approval_center_service.py::_dispatch_outbox(outbox_id)` — `decide_task` 最后节点通过 / skip_node - `approval_gate.py` PASS 分支 — 调 `execute_approval_outbox.delay(outbox_id)` **Celery 队列:走默认 `celery` 队列。** `worker/config.py` **不**为 `bisheng.worker.approval.*` 配路由,任务自然 fall through 到默认队列。`workflow_celery` 专供工作流 DAG 执行,审批任务不占用。 > ⚠️ 部署时必须有 worker 消费默认 `celery` 队列(`run_celery.py` 的 `all` / `file` 模式都含),否则审批通过后业务不执行。站内信发送是同步写库,不依赖 Celery。 启动消费默认队列的 worker: ```bash uv run celery -A bisheng.worker.main worker -l info -c 100 -P threads -n default@%h ``` --- ## 7. API 列表 > 全局前缀 `/api/v1`。以代码为准(`approval_user.py` / `approval_admin.py` / `approval.py`)。 ### 用户端(`/approval`) ``` GET /approval/my-tasks # 我的待办(审批人视角) GET /approval/my-tasks/{task_id} # 任务详情 POST /approval/tasks/{task_id}/decision # 同意/拒绝 GET /approval/my-requests # 我的申请(申请人视角) GET /approval/instances/{instance_id} # 实例详情(tasks + flow_nodes + action_logs) POST /approval/instances/{instance_id}/withdraw # 撤回 GET /approval/menu-access/pending-check # 菜单申请前置校验 POST /approval/menu-access/apply # 菜单权限申请 POST /approval/menu-access/{instance_id}/revoke-grant # 撤销菜单授权(审批人) ``` ### 管理端(`/approval/admin`) ``` GET /approval/admin/scenario-presets # 预置场景目录(下拉来源) GET /approval/admin/scenarios # 场景列表 POST /approval/admin/scenarios # 新增场景 PUT /approval/admin/scenarios/{scenario_id} # 更新场景 DELETE /approval/admin/scenarios/{scenario_id} # 删除场景 GET /approval/admin/scenarios/{scenario_id}/routes # 分支列表 POST /approval/admin/scenarios/{scenario_id}/routes # 新增分支 PUT /approval/admin/routes/{route_rule_id} # 更新分支
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub