인터페이스는 현지화되어 있으며, Skill 소개와 설명 문서는 선택한 언어로 AI가 자동 번역합니다. 코드·명령어·경로·전문 용어는 원문을 유지하며 원작자 문서가 우선합니다.
Skills 둘러보기
개발 및 엔지니어링 · SKILL.md

approval-module

approval-module

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

Independent GitHub snapshot · Automated classification · Original documentation governs

Ddataelement / bishengApache-2.0

Skill 문서

GitHub

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

审批模块(审批中心 F025)

⚠️ 维护契约(修改代码后必读)

本 skill 是审批模块的唯一权威参考,必须与代码永远一致。 当你改动以下任意一项时,同一个改动里必须同步更新本文件对应章节,否则视为改动未完成:

自检:改完代码后问自己"本 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.pyoutbox 执行与重试;成功后置 instance=EXECUTEDexecute_outbox()、retry_outbox()
approval/domain/services/approval_scenario_admin_service.py管理端:场景/分支/流程/节点配置、异常列表—
approval/domain/services/approver_resolver.py解析审批人来源 direct_user / department_admin / tenant_adminresolve_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 重新构造运行时 handlerbuild_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.pyCelery 任务(走默认 celery 队列)execute_approval_outbox、retry_approval_outbox
worker/config.pyCelery 路由配置(审批任务不配路由,fall through 到默认队列)task_routes
approval/api/endpoints/approval_user.pyClient 端 API(/api/v1/approval/...)—
approval/api/endpoints/approval_admin.pyPlatform 管理 API(/api/v1/approval/admin/...)—
approval/api/endpoints/approval.py旧系统 legacy API(/api/v1/approval/requests/...),已废弃—

三个场景 Handler

文件类
approval/domain/services/menu_access_handler.pyMenuAccessApprovalHandler
approval/domain/services/channel_subscribe_scenario_handler.pyChannelSubscribeScenarioHandler
approval/domain/services/knowledge_space_subscribe_scenario_handler.pyKnowledgeSpaceSubscribeScenarioHandler

前端

文件职责
src/frontend/client/src/components/messageApproval/MessageApprovalDialog.tsx「消息与审批」统一弹窗外壳(导航栏:我的审批 / 我的申请 / 通知 + 徽标)
src/frontend/client/src/components/approval/ApprovalPane.tsx我的审批 / 我的申请 列表 + 详情 + 同意/拒绝/撤回/撤销授权
src/frontend/client/src/components/approval/ApprovalDetailPanels.tsx任务详情 / 实例详情面板(基础信息、业务内容、进度时间轴)
src/frontend/client/src/components/approval/approvalPresentation.tsx审批公共展示层(状态徽章、时间轴节点、信息网格、格式化)
src/frontend/client/src/components/messageApproval/NotificationPane.tsx通知区:未读/已读页签(服务端过滤)+ 搜索 + 全部已读
src/frontend/client/src/components/messageApproval/NotificationRow.tsx单条通知;仅在用户主动打开时置已读,不做 hover / 曝光自动已读
src/frontend/client/src/components/messageApproval/notificationContent.ts站内信 payload 解析(action_code、业务对象、审批深链)
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.tsPlatform 审批 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
  • 审批人来源从 F048 当前 Catalog 的有效 permission_grant / permission_grant_assignee roster 读取:channel_owner 仅取 owner 模型的直接用户,channel_manager / channel_admin 仅取 manager 模型的直接用户;权限运行时异常时才回退 space_channel_member 的 CREATOR / ADMIN,不能把 owner 与 manager 混成同一集合
  • 通过 / 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
  • 审批人来源从 F048 当前 Catalog 的有效 permission_grant / permission_grant_assignee roster 读取:knowledge_space_owner 仅取 owner 模型的直接用户,knowledge_space_manager / space_admin 仅取 manager 模型的直接用户;不要读取 F048 资源对象上不存在的 legacy owner / manager 直连 tuple。仅权限运行时异常时回退 space_channel_member 的 CREATOR / ADMIN
  • 通过 / 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:

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/pending-count          # 待我处理数量(徽标),返回 {"count": n};必须声明在 {task_id} 之前
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}                 # 更新分支
DELETE /approval/admin/routes/{route_rule_id}                 # 删除分支
PATCH  /approval/admin/scenarios/{scenario_id}/routes/reorder # 分支排序
GET    /approval/admin/scenarios/{scenario_id}/flows          # 流程列表
POST   /approval/admin/scenarios/{scenario_id}/flows          # 新增流程
PUT    /approval/admin/flows/{flow_definition_id}             # 更新流程
DELETE /approval/admin/flows/{flow_definition_id}             # 删除流程
GET    /approval/admin/flows/{flow_definition_id}/nodes       # 节点配置
PUT    /approval/admin/flows/{flow_definition_id}/nodes       # 提交节点(全量提交触发新版本)
GET    /approval/admin/flows/{flow_definition_id}/versions/{flow_version_id} # 版本预览
GET    /approval/admin/exceptions                            # 异常列表
POST   /approval/admin/exceptions/{exception_id}/retry       # 重试/指定审批人/跳过节点/标记完成
POST   /approval/admin/exceptions/{exception_id}/cancel      # 取消审批(必须填原因)

旧系统 legacy(/approval/requests、/approval/department-knowledge-space)— ⚠️ 已废弃

部门知识空间文件上传审批,独立于审批中心,见 approval.py。已废弃,仅兼容存量数据,不要在此新增/扩展接口。


8. 站内信通知矩阵

触发时机接收人实现位置
创建审批任务(菜单申请)审批人ApprovalCenterService._send_menu_access_approval_messages()
频道审批创建(PENDING)审批人ChannelService._send_channel_approval_notification()
知识空间审批创建(PENDING)审批人KnowledgeSpaceService._send_space_approval_notification()
中间节点通过、生成下一节点任务下一节点审批人_advance_after_node_approved() → _send_approval_notify('approval_task_pending')
审批通过(最后节点 finalize)申请人_advance_after_node_approved() → _send_approval_notify('approval_instance_approved')
审批拒绝申请人decide_task() reject 分支
申请撤回有 task 的审批人ApprovalCenterService.withdraw_instance()
异常产生(route_missing/approver_empty)管理员(AdminRole)ApprovalGate._notify_admins_of_exception() / ApprovalNotificationService.notify_admins()
异常取消申请人ApprovalExceptionService.cancel_exception_api()

待办类通知不进通知列表:approval_task_pending / request_menu_access / request_channel / request_knowledge_space(MessageService.APPROVAL_TODO_ACTION_CODES)只属于【我的审批-待我处理】。 /message/list?tab=notify 与未读数 notify 都会排除它们,全部已读 也不会把它们置为已读。

注:申请人侧"通过"通知是在最后节点 finalize 时发的(即审批通过即通知),不等 outbox 业务真正执行完。若要"业务执行成功"的精确通知,需在 execute_outbox 成功回调里补。


9. 审批进度时间轴

get_instance_detail 返回三组数据,前端合并展示:

action_logs[action=submitted]      ← 提交申请
flow_nodes (按 node_order 排序)     ← 完整流程骨架(来自 approval_node_definition,含未到达节点)
  ├── 已有 task → 实际状态
  └── 无 task  → 灰色"未到达"
action_logs[action!=submitted]     ← 撤回/取消等其他日志

flow_nodes 解决了"tasks 只有已创建节点"的问题,能展示完整流程定义。


10. 配置要点

条件分支 match_config 格式:

{}                                                  // 无条件,始终命中(catch-all)
{"field": "applicant_role", "value": "dept_admin"}  // 申请人是部门管理员
{"field": "menu_key", "value": "knowledge_space"}   // 申请特定菜单
{"field": "space_type", "value": "department"}      // 知识空间类型

applicant_role 枚举:admin(系统管理员) / tenant_admin(租户管理员) / dept_admin(部门管理员) / regular_user(普通用户, catch-all) / role_{id}(特定角色)。

节点 approver_config.sources 格式:

[
  {"type": "direct_user", "user_ids": [701], "user_names": ["00017"]},
  {"type": "department_admin"},
  {"type": "tenant_admin"}
]

user_names 由前端保存时写入,用于节点卡片直接显示用户名,避免二次查库。


11. 调试指南

"审批通过但业务没下发"

SELECT id, status, applicant_user_id FROM approval_instance WHERE id=<N>;
SELECT id, status, error_summary FROM approval_outbox WHERE instance_id=<N>;
  • outbox 不存在 → _dispatch_outbox 没调
  • outbox 存在且 pending → 没有 worker 消费默认 celery 队列
  • outbox 存在且 failed → 看 error_summary,并查 approval_exception 的 execute_failed

手动补偿:

# set_current_tenant_id(tenant_id)
# handler = await build_runtime_handler(outbox.handler_key)
# await handler.on_approved(instance_id, outbox.payload_snapshot)

"审批人看不到任务"

SELECT id, approver_user_id, status FROM approval_task WHERE instance_id=<N>;
SELECT id, exception_type, status, detail FROM approval_exception WHERE instance_id=<N>;

若异常类型是 approver_empty:检查 approval_instance.applicant_department_id 是否为 NULL,以及节点 approver_config.sources 里 department_admin 是否依赖部门。

"频道/知识空间审批通过但成员列表看不到"

检查对应 sync_direct_channel_user_permissions / sync_direct_space_user_permissions 是否在该激活路径被调用(写 ReBAC/OpenFGA 关系)。若 instance=executed 但 space_channel_member.status 仍为 PENDING,说明 on_approved 没真正激活成员(见 §6 的"业务回调不得静默失败"原则)。


12. 测试

审批相关测试在 src/backend/test/approval/(asyncio_mode=auto)。新测试放到该目录,不放 test/ 根。

cd src/backend && uv run pytest test/approval/
저장한 Skills