为什么审批流适合用状态图来梳理
审批流是典型的"状态驱动"业务:一份申请从提交到最终归档,中间要经历多个环节,每个环节允许的操作不同,且存在驳回、撤回、加签等分支。如果只用 if/else 堆逻辑,代码很快会变成难以维护的面条。
状态图(State Diagram)能把"有哪些状态、什么事件触发迁移、迁移后做什么"一次说清,是审批流建模最自然的工具。
第一步:先定义状态,而不是先写代码
以一个常见的报销审批为例,先罗列状态:
DRAFT:草稿,仅创建人可见SUBMITTED:已提交,等待直属主管审批MANAGER_APPROVED:主管通过,等待财务审批FINANCE_APPROVED:财务通过,等待打款PAID:已打款(终态)REJECTED:被驳回(终态)CANCELLED:申请人撤回(终态)
要点:终态一旦到达就不再迁出,这能在代码层面直接杜绝"已打款的单子又被驳回"这类脏数据。
第二步:定义事件与迁移规则
状态图的核心是迁移表,用"当前状态 + 事件 → 目标状态"表示:
| 当前状态 | 事件 | 目标状态 |
|---|---|---|
| DRAFT | submit | SUBMITTED |
| SUBMITTED | managerApprove | MANAGER_APPROVED |
| SUBMITTED | managerReject | REJECTED |
| SUBMITTED | cancel | CANCELLED |
| MANAGER_APPROVED | financeApprove | FINANCE_APPROVED |
| MANAGER_APPROVED | financeReject | REJECTED |
| FINANCE_APPROVED | pay | PAID |
把这张表画成状态图,你会立刻发现几个问题:谁有权触发 managerApprove?如果主管和财务是同一个人怎么办?驳回后能不能重新提交?这些在写代码前就暴露出来,比上线后返工便宜得多。
第三步:为迁移补充守卫与动作
状态图的迁移边上可以挂两类东西:
- 守卫(Guard):迁移的前置条件,如"只有当前审批人有权限"、"金额超过 5000 需财务总监二次确认"。
- 动作(Action):迁移成功后执行,如发通知、写审计日志、调用打款接口。
建议把守卫和动作都写成独立函数,与状态机本身解耦,方便单测。
第四步:落地到代码
主流语言都有成熟的状态机库,不必手写。以 Java 的 Spring Statemachine、Python 的 transitions、JavaScript 的 xstate 为例,思路一致:
- 声明状态集合与初始状态
- 声明迁移(事件、源状态、目标状态、守卫、动作)
- 由状态机实例承载单条业务记录
- 每次操作前先
can(event)判断,再send(event)执行
伪代码示意:
machine = StateMachine(
states = [DRAFT, SUBMITTED, MANAGER_APPROVED, ...],
initial = DRAFT,
transitions = [
(DRAFT, 'submit', SUBMITTED, guard=isOwner),
(SUBMITTED, 'managerApprove', MANAGER_APPROVED, guard=isManager),
...
]
)
关键工程实践:把状态机的当前状态持久化到数据库字段(如 status 列),每次操作时先加载状态、再触发事件、最后在同一事务里写回状态并记录流转日志。这样既保证了数据一致性,也留下了可追溯的审计轨迹。
第五步:常见坑与应对
- 状态爆炸:不要为每个审批人建一个状态,用"审批节点"字段区分,状态只表达阶段。
- 并发操作:两人同时审批同一条记录,用乐观锁(版本号)或数据库行锁解决。
- 历史回溯:单独建一张
approval_log表,记录每次迁移的事件、操作人、时间、备注。 - 规则变更:审批层级经常变,尽量把"谁审批"配置化,状态机只负责"能不能迁"。
小结
用状态图梳理审批流的价值在于:先把"有哪些状态、谁能触发什么、触发后发生什么"想清楚,再写代码。状态图是沟通工具,也是设计文档,最终还能直接映射到状态机库的配置上。下次遇到多环节、多分支的流程,不妨先画一张状态图,往往能省下大量调试时间。