第 13 章:流程审批:制度化的人机协同
13.1 工程问题的本质:把决策权制度化
许多业务操作不是一次调用,而是一条审批流程:发起 → 字段收集 → 逐级审批 → 业务生效。审批的工程本质是把"该由谁、在什么条件下、以什么顺序决策"制度化——不是临时找一个领导签字,而是把决策责任、决策顺序、决策记录固化为可执行的流程。
设计原理: 流程制度化的三个要件:状态可追踪(流程进行到哪一步)、迁移有守卫(每一步由谁批准、前提是否满足)、全程可追溯(谁在何时做了什么决定)。三者分别对应状态机、链式 Gate、签字留痕。
13.2 定义与实例分离:改流程与跑流程互不干扰
流程定义(审批链、必填字段、时限、守卫条件)存储在配置表,流程实例(运行态)存储实例表。定义可训练、实例不可回改。
原理: 定义与实例分离解决"改流程"与"跑流程"的冲突:线上进行中的流程不能被中途改变规则(实例不可回改),新流程按新定义执行(定义更新影响后续实例)。这保证了审批的确定性——用户在发起流程时看到的规则,就是审批时执行的规则。若定义与实例耦合,审批结果将无法预期。
13.3 三道发起校验:谁可以发起
流程发起前执行发起者三道校验:角色(starter_roles)、部门(starter_depts)、启动方式(initiation,manual/event/auto)。
原理: 审批流程的资源是"决策注意力",发起门槛决定决策注意力的分配效率。三道校验是准入控制:不是所有人都有权发起所有流程(角色/部门),也不是所有发起方式都被允许(自动化触发的流程与人工发起受不同约束)。发起即决策链的起点,起点不可控则整链不可信。
13.4 链式 Gate:迁移守卫
每一步推进前校验 5 维前序完整性:前序审批签字、必填字段、关联实体状态、时间窗口、SOD 隔离(审批者与发起者非同一人)。
原理: 链式 Gate 回答"这一审批动作是否具备合法性前提"。它模拟人工审批的实践——接手者检查前序完整性:上一步真的批了吗?该带的数据齐了吗?被审的实体状态对吗?超时了吗?自己和发起者是不是同一个人?守卫缺失时,审批就会在错误前提上做决定——例如"图纸还没生效就审批通过",或"审批人自己发起又自己审批"。
条件表达式无法解析时视为必需(保守策略):守卫不确定时,宁可信其有。
13.5 签字留痕:决策的不可抵赖
每次推进记录审批人、角色、时间与动作,形成审批签字链。对话中的流程:字段收集阶段跨轮收集必填字段,待审批阶段"同意"推进审批、留言补充上下文。
原理: 签字留痕使审批从"一次操作"变为"一份记录"。每一签都是一次责任声明——谁、在何时、基于什么数据、批准了什么。流程单据的展示(申请字段 + 审批签字链)使"谁在何时审批"随时可查,这是审批结果可被业务系统采信的前提。留言机制则支持多人协作中的信息补充,且明确留言为过程性数据、不进入知识库。
13.6 治理之治理:审批链本身可训练
审批链本身可训练:流程定义的训练(新建/修改流程)同样走审批链,训练审批链来自该流程类型的定义行(可训练),无定义兜底 manager 单级。
原理: 若治理规则本身不可治理,治理就会被绕过——业务方可以直接改审批链绕过审批。治理之治理保证:修改审批规则的权力本身也受审批约束。这一递归在何处停止?在硬规则处停止——SOD 与硬规则不可训练(第 12 章),构成递归治理的地基。
13.7 工作流模式与审批完整生命周期
13.7.1 工作流模式:流程不止顺序
简单的审批链是线性顺序,但真实业务需要更丰富的流程形态。工作流领域沉淀了一批通用模式(process patterns)[20][31],其思想可指导流程定义的设计,见表 13-1:
表 13-1 工作流模式与框架对应
| 模式 | 语义 | 在框架中的对应 |
|---|---|---|
| 顺序(Sequence) | 步骤逐一执行 | 审批链逐级推进(默认形态) |
| 并行分叉(Parallel Split) | 一步拆为多路并行 | 多审批人同步骤审批 |
| 同步汇合(Synchronization) | 多路完成后才继续 | 链式 Gate 前序签字全齐才推进 |
| 多选(Multi-choice) | 条件满足才执行某支 | 条件式守卫(amount>500000 才需第二级) |
| 回退(Redo / Loop) | 流程回退到前序步骤 | 审批驳回后流程重回字段/上一级 |
原理: 工作流模式的工程价值是"用已沉淀的形态表达流程,而非每次发明"。模式提供了命名与语义——团队沟通"这一步是并行分叉"比"这一步要让两个人同时看"更精确;模式也提供了实现检验——"同步汇合"必须有汇合条件(前序全批),否则就是错误实现。框架的链式 Gate 守卫(前序签字全齐)正是同步汇合模式的正确定义[30]。
13.7.2 审批完整生命周期:推进之外的动作
审批不止"同意推进",完整生命周期还包括,见表 13-2:
表 13-2 审批动作的语义与设计要求
| 动作 | 语义 | 设计要求 |
|---|---|---|
| 同意(approve) | 当前步骤通过 | 角色校验 + 前序守卫 + 签字留痕 |
| 驳回(reject) | 否决当前步骤 | 须明确拒绝理由,流程回到前序或终止 |
| 留言(comment) | 补充上下文,不改变状态 | 过程性数据,不进入知识库 |
| 撤回(recall) | 发起人撤销未完成流程 | 仅在未推进到终态前允许,须留痕 |
| 超时处理(timeout) | 超过时限未审批 | 链式 Gate 时间窗口守卫,超时即阻断/升级 |
原理: 审批生命周期不完整,会导致"只能前进不能后退"的僵化流程——业务上一旦出现错误审批或信息遗漏,没有出口。驳回、撤回、留言、超时四类动作补全了流程的可逆性与可解释性:可逆(错误可纠正)与可解释(每个动作留痕)。设计检验:流程定义应显式声明每个动作的权限与结果状态——哪些角色可驳回、驳回后回退到哪一步、超时如何处理,都应可配置而非隐式。
把 13.7.1 的"链式推进"与 13.7.2 的"推进之外动作"合并成一张状态机,可看清每个动作把实例导向哪个状态、哪些状态是终态,如图 13-1 所示:
图 13-1 审批实例状态机:推进、驳回、撤回与超时
stateDiagram-v2
direction LR
state "待发起" as Draft
state "审批中" as InReview
state "已通过(终态)" as Approved
state "已驳回" as Rejected
state "已撤回(终态)" as Recalled
state "超时阻断" as Timeout
[*] --> Draft
Draft --> InReview: 发起·三道校验通过
Draft --> Recalled: 发起人撤回
InReview --> InReview: 同意当前级(未到终态)
InReview --> Approved: 末级同意·签字链完整
InReview --> Rejected: 驳回(须附理由)
InReview --> Timeout: 超时·时间窗口守卫
Rejected --> InReview: 回退后重新提交
Rejected --> [*]
Timeout --> InReview: 升级后继续
Timeout --> [*]
Approved --> [*]
Recalled --> [*]
读图要点: 只有"审批中"是活动状态,其余均为进入或离开的边界;已通过与已撤回是不可逆终态(一旦到达即封存),已驳回与超时阻断则保留回到审批中的出口——这正是表 13-2 中"可逆性"的图形表达。留言(comment)不改变状态,故不在此图中出现:它只写入过程性数据,不影响实例迁移。
13.8 失败模式与权衡
表 13-3 流程审批的失败模式与权衡
| 失败形态 | 根因 | 设计应对 |
|---|---|---|
| 审批在错误前提上决定 | 前序未批 / 数据缺失 | 链式 Gate 5 维守卫 |
| 单人闭环舞弊 | 审批者即发起者 | SOD 隔离校验 |
| 流程定义中途变化 | 定义与实例耦合 | 定义/实例分离,实例不可回改 |
| 审批推进无通知 | 通知链路断裂 | 事件总线解耦(见 16.4 节) |
权衡: 审批的确定性以"流程更慢"为代价(逐级、逐守卫)。框架的调节手段:字段收集的引导语、审批链步数与角色均可训练——业务方在"效率"与"制衡"之间拥有调节权,但调节本身受治理约束。
13.9 并行会签与白名单闸门
13.7 节的链式 Gate 是串行迁移(一棒接一棒);业务里还有一个常见形态是并行会签——同一审批实例需要多个角色(销售经理、财务经理…)各自独立表态,全部通过才放行。
设计原理: 并行会签的确定性来自两点约束。其一,白名单闸门:能推动某一节点的角色白名单在流程定义时固化,只有白名单内的角色被允许表态,防越权推进;其二,签名链并行校验(validate_sig_chain_parallel):即使节点并行执行,仍需校验签名链的完整与并行合法——并行不等于无序,必须在"谁先到、谁表态、都齐了才放行"的既有边界内运行。
失败防范: 并行≠放权。若白名单闸门缺失,任何角色都能在未授权节点表态,等同于串行链中跳过 Gate(13.8 节"单人闭环舞弊"的并行变体);校验必须覆盖"并行分支未满即放行"的漏口,保证放行条件里"全部表态"与"逐个合法"二者齐备。
13.10 实现剖析:链式 Gate、发起校验与签字链完整性
本节从 runtime/chain_gate.py(496 行)与 runtime/workflow_enforcer.py(2,359 行)摘取三段,印证 13.4 节的迁移守卫、13.3 节的三道发起校验与 13.5 节的签字留痕。
13.10.1 链式 Gate:五维校验产出"缺失项清单"
def check_predecessor_completeness(self, gate_config, instance):
"""校验前序步骤完整性(5 维)"""
missing_items = []
# 1. 前序审批签字完整性
self._check_approvals(gate_config, instance, steps_done,
current_step, missing_items)
# 2. 必填数据完整性
self._check_required_fields(gate_config, instance, current_step, missing_items)
# 数据库不可用时降级:仅校验 steps_done 与 required_fields
if self._db is not None:
self._check_required_statuses(gate_config, instance, missing_items) # 3. 版本状态
self._check_timeout(gate_config, instance, missing_items) # 4. 时间窗口
self._check_sod(instance, current_step, missing_items) # 5. SOD 隔离
if missing_items:
return GateResult(passed=False, missing_items=missing_items,
message=f"步骤{current_step}前置校验未通过:"
f"{len(missing_items)}项缺失")
return GateResult(passed=True, missing_items=[],
message=f"步骤{current_step}前置校验通过")
def _check_sod(self, instance, current_step, missing_items):
"""SOD 隔离校验:发起者不可审批自己发起的流程"""
created_by = instance.get("created_by")
approver = instance.get("approver")
if isinstance(approver, dict) and approver.get("role") == "admin":
return # v6.56:admin 代批豁免 SOD 校验
# ... 提取审批者 id 集合 approver_ids
if created_by and approver_ids and created_by in approver_ids:
missing_items.append({
"step": current_step - 1,
"type": "sod_violation",
"detail": "当前审批者与发起者为同一人,违反SOD职责分离",
"message": "SOD隔离校验失败:当前审批者与发起者为同一人",
})
这段代码印证 13.4 节:链式 Gate 的产出不是布尔值,而是"缺失项清单"。五维校验各自把失败原因追加进 missing_items(含 type 与 detail),返回给调用方后即可"退回缺失环节 + 通知发起者和审批者"——这正是 13.4 节"阻断当前步骤 + 退回缺失环节"的落点。
同时注意降级设计:数据库不可用时只保留前两维校验,保证最小可信实现仍然可用(第 16 章)。SOD 隔离放在最后一维,与 12.3 节的"标记"形成分工:那里标记违规角色,这里阻断违规推进。
13.10.2 三道发起校验:谁可以发起
def _validate_starter(self, config, user, initiation):
"""发起者三道校验:角色 / 部门 / 启动方式"""
user = user or {}
# 1. 发起者角色校验(允许列表含 'all' 表示任意角色可发起)
starter_roles = self._parse_jsonb_list(config.get("starter_roles"))
if starter_roles and "all" not in starter_roles:
user_role = user.get("role")
if user_role not in starter_roles:
user_base = _role_base(user_role) # 短名/职级名互认
if not (user_base and any(_role_base(s) == user_base
for s in starter_roles)):
return (False, f"发起者角色 {user_role} 不在允许列表 "
f"{starter_roles} 中")
# 2. 发起者部门校验(admin 等角色跳过,避免部门配置二次拦截)
starter_depts = self._parse_jsonb_list(config.get("starter_depts"))
if starter_depts and "all" not in starter_depts:
_bypass_roles = ("admin", "system_manager", "general_manager")
if (user.get("role") not in _bypass_roles
and user.get("department") not in starter_depts):
return (False, f"发起者部门 {user.get('department')} 不在允许列表")
# 3. 启动方式校验(如 manual / auto / trigger)
config_mode = self._extract_initiation_mode(config.get("initiation"))
if config_mode and config_mode != initiation:
return (False, f"启动方式不匹配:期望 {config_mode},实际 {initiation}")
return (True, "")
这段代码印证 13.3 节:发起校验的三个维度都来自流程定义(配置),而非代码。starter_roles / starter_depts / initiation 三项均可由流程训练修改(13.6 节"治理之治理"),代码只负责执行判定。
_role_base 的短名互认说明工程实现要容忍配置的书写差异——权限判定不能因"finance"与"finance_manager"的写法不同而误拒。
13.10.3 签字链完整性:串行不变式与并行不变式
def validate_sig_chain(steps_done, expected):
"""串行审批链签字完整性校验"""
if not isinstance(steps_done, list):
steps_done = []
if len(steps_done) != expected: # ★ 前序签字数必须精确匹配
return (f"审批链不完整:前序已审批 {len(steps_done)} 步,"
f"当前应为 {expected} 步,请修正审批记录后重试")
return None
def validate_sig_chain_parallel(steps_done, chain, barriers, current_step):
"""并行会签步签字链完整性校验(不变式不同于串行)"""
expected = 0
for idx in range(current_step): # 已完成步
step_cfg = chain[idx]
is_par = step_cfg.get("mode") == "parallel" or "approvers" in step_cfg
if is_par: # 并行步按冻结的 required 数计
req = barriers.get(str(idx + 1), {})
req_roles = req.get("required") if isinstance(req, dict) else None
expected += len(req_roles) if isinstance(req_roles, list) else 0
else:
expected += 1 # 串行步计 1
expected += sum(1 for s in steps_done if s.get("step") == current_step + 1)
# ★ 纵深防御:不得存在"未来步"签字,防越级携带伪签字
ahead = [s.get("step") for s in steps_done
if isinstance(s.get("step"), int) and s.get("step") > current_step + 1]
if ahead:
return f"签字链异常:存在未处理步之后的签字(step={ahead}),拒绝推进"
if len(steps_done) != expected:
return (f"并行会签签字不完整:已签字 {len(steps_done)} 条,"
f"当前应为 {expected} 条,请修正审批记录后重试")
return None
这段代码印证 13.5 节:签字留痕的可证明性靠"计数不变式"保证。串行链要求前序签字数精确等于步数;并行会签因多人共记同一步号,须改用"串行步×1 + 并行步×冻结人数 + 当前步已签槽数"的不变式,否则第二位会签者永远签不上(流程卡死)。
"未来步签字"检查是纵深防御——它堵住"直接篡改 current_step 跳级、携带伪签字越过中间审批"的路径,把 13.5 节的"不可抵赖"落成可校验的事实。
13.11 本章小结
- 审批的工程本质是把决策权制度化:状态可追踪、迁移有守卫、全程可追溯;
- 定义与实例分离保证审批规则的确定性;
- 三道发起校验是决策链起点的准入控制;
- 链式 Gate 的 5 维守卫防止在错误前提下决策;
- 治理之治理使审批规则本身受审批约束,递归止于硬规则。