企业 OA 的审批流程表面上看只是“下一步”和“退回”,但真正落到系统里,还需要把流程定义、实例、任务和历史记录拆开建模。本文记录审批流引擎里几个容易返工的设计点。
流程建模
流程不能只存一张“当前走到哪”的表。实际开发中建议至少拆分流程定义、流程实例、节点实例、任务和审批记录,分别保存“模板”和“运行状态”。
节点类型需要覆盖开始、审批、抄送、条件、结束。节点之间用边连接,边负责描述下一步是谁。
{
"definition": {
"nodes": [
{"id": "start", "type": "start"},
{"id": "manager_approve", "type": "approve"},
{"id": "finance_approve", "type": "approve"},
{"id": "end", "type": "end"}
],
"edges": [
{"from": "start", "to": "manager_approve"},
{"from": "manager_approve", "to": "finance_approve"},
{"from": "finance_approve", "to": "end"}
]
}
}
审批任务
审批人的来源不只有“指定人”。实际项目里常见的是指定人、角色、部门负责人、表单字段指定的用户,甚至需要根据提交人自动推导。最好把审批人来源做成一个可解析的表达式,而不是在代码里写死。
任务本身需要区分会签、或签和依次审批:
- 会签:所有审批人都同意,节点才通过。
- 或签:任意一个审批人同意,节点即通过。
- 依次审批:按顺序处理,前一个完成才生成后一个任务。
条件分支
条件分支的难点不在画图,而在条件表达式的校验。给用户配置条件时,不要直接执行拼接出来的代码,应该使用字段白名单和固定操作符,避免配置错误影响整个流程实例。
{
"condition": {
"field": "amount",
"operator": ">",
"value": 10000
}
}
回退机制
回退是审批流里最容易出问题的地方。退回不是简单地把当前节点改回“待审批”,而是要生成新的任务,同时保留上一轮任务的处理记录。
需要注意三个边界:
- 退回到指定节点时,目标节点必须位于当前实例已走过的路径内,不能退回一个不存在的未来节点。
- 重新提交后,历史记录不能被覆盖,新的提交要生成新版本任务。
- 连续退回需要限制范围,避免流程实例在几个节点之间反复循环。
幂等与留痕
审批动作通常由前端按钮触发,网络重试可能导致同一操作执行多次。每个审批动作都应该有唯一的业务幂等键,重复请求只生效一次。
每次操作都要写入审批记录,包含操作人、操作时间、处理结果和意见。这些记录既用于审计,也用于回退时还原上下文。
审批流真正复杂的地方不是拖拽画布,而是状态迁移、回退边界和异常恢复。先把这些边界定清楚,后面的功能才不会越加越乱。