业务系统里最容易被低估的复杂度,不是表单本身,而是表单背后那条"谁来填、填完给谁、什么条件走哪条路"的流程。请假、报销、采购申请、合同用印,表面都是一张表,硬编码写起来很快,可一旦业务方提出"金额超过一万要多加一级审批""A 部门走这个流程、B 部门走另一个",代码就会迅速膨胀成一堆 if-else 嵌套,改一处怕动全身。本文拆解一套可配置的表单与审批工作流引擎的核心设计:表单 Schema 化、流程的条件分支、审批实例的状态机,并给出落地时最容易踩的坑。
很多系统把这三者揉在一张业务表里,结果就是改不动。清晰的引擎会把它们拆开:
拆开之后,"改流程"只是新增一个流程定义版本,历史实例仍然引用它发起时的旧版本,不会因为流程改了而错乱——这是工作流引擎能长期维护的前提。
不要为每种申请表单写一套页面和校验。用一份 JSON 描述字段,前端用渲染器统一生成,后端用同一份 Schema 做服务端校验(前端校验永远不可信)。
{
"formId": "purchase_apply",
"version": 3,
"fields": [
{"key": "title", "label": "事由", "type": "text", "required": true, "maxLength": 50},
{"key": "amount", "label": "金额(元)", "type": "number", "required": true, "min": 0.01},
{"key": "department", "label": "申请部门", "type": "select", "options": ["研发", "市场", "行政"]},
{"key": "supplier", "label": "供应商", "type": "text", "visibleWhen": "amount > 5000"}
]
}几个关键设计点:
visibleWhen,而不是在页面里写监听函数,否则联动关系散落各处、无法回溯;流程的本质是一张有向图:节点(Node)表示一个处理环节,边(Edge)表示流转方向,边上可以挂条件。常见节点类型有发起节点、审批节点、抄送节点、条件网关、结束节点。
{
"nodes": [
{"id": "start", "type": "start"},
{"id": "manager", "type": "approval", "assignee": "applicant.leader"},
{"id": "gateway", "type": "condition"},
{"id": "director", "type": "approval", "assignee": "role:director"},
{"id": "finance", "type": "approval", "assignee": "role:finance"},
{"id": "end", "type": "end"}
],
"edges": [
{"from": "start", "to": "manager"},
{"from": "manager", "to": "gateway"},
{"from": "gateway", "to": "director", "when": "form.amount > 10000"},
{"from": "gateway", "to": "finance", "when": "form.amount <= 10000"},
{"from": "director", "to": "finance"},
{"from": "finance", "to": "end"}
]
}条件网关的求值要在服务端完成,表达式只允许访问白名单上下文(表单数据、申请人属性),不要直接 eval 用户输入,避免注入。审批人不要写死成某个人,而是用"申请人直属上级""某角色"这类解析规则,在节点流转时动态解析,这样人员调岗后流程不用改。
一个审批实例在任意时刻只能处于确定的状态,且状态之间的迁移必须合法。用状态机显式约束,比在每个接口里判断要可靠得多。
const TRANSITIONS = {
draft: { submit: 'pending' },
pending: { approve: 'approving', reject: 'rejected', withdraw: 'withdrawn' },
approving:{ pass: 'approved', reject: 'rejected', returnBack: 'pending' },
approved: {},
rejected: { resubmit: 'pending' },
withdrawn: { resubmit: 'pending' }
};
function move(current, action) {
const next = TRANSITIONS[current]?.[action];
if (!next) throw new Error(`非法流转:${current} 不允许 ${action}`);
return next;
}状态机带来三个好处:一是杜绝并发覆盖,两个人同时点同意,第二个请求会因为状态已经变化而命中"非法流转";二是每个动作幂等,重复提交不会产生两条审批记录;三是全程可追溯,状态迁移和审批意见一起落审计表,任何一单都能还原完整路径。
自研一套工作流引擎的成本主要在边界情况:并签会签、加签转办、超时催办、委托代理,这些往往做到第二三期才暴露。如果团队的核心诉求是"把高频审批和表单快速线上化、流程可配置",可以引入开源工作流引擎做二次开发,也可以在已有内部框架上抽象出流程配置层,把精力留给真正差异化的核心业务,而不是在审批流的边界情况上重复投入。无论哪条路径,上面的"表单/定义/实例三层分离 + 状态机约束 + 全程留痕"都是通用骨架。
表单和审批看起来是业务系统里最"没技术含量"的部分,但真正决定它好不好维护的,是有没有在一开始就把表单、流程定义、流程实例分清楚,并用状态机把流转约束住。把易变的业务规则变成可版本化的配置,把不变的流转约束沉淀成引擎骨架,后续业务方再怎么调整审批路径,系统都能从容应对。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。