资讯动态

GoWind OA 拆解:用 3000 行 Go 写一个够用的审批工作流引擎

发布时间:2026/10/25 1:56:45 来源:尧图企业网站定制
GoWind OA 拆解用 3000 行 Go 写一个够用的审批工作流引擎本文回答一个问题不引入 Camunda/Flowable自己写一个支持条件分支、并行会签、子流程、回退环的审批引擎核心复杂度到底在哪几处GoWind OA协同办公审批流转、请假报销、考勤内置了一个自研的轻量级工作流引擎——约 3000 行核心 Go 代码不含生成物7 张表28 个图模型单测 service 层集成测试配一篇 358 行的设计文档。它不追求 BPMN 兼容只追求把审批这件事的边界情况全部想清楚。这篇按引擎的六个机制拆图契约、编译期校验、运行期推进、会签或签、寻人与委托、业务挂钩——每一段都有真实的取舍可讲。一、图契约JSON 存图轻量级的第一层含义流程定义不打节点表整图以一个 JSON 字段落盘workflow_definition表只有 code/version/node_config/form_schema/status 四个业务字段(tenant_id, code, version)唯一索引{version:2,nodes:[{id:start,type:START},{id:task_1,type:TASK,strategy:ALL,approvers:[{type:USER,id:123},{type:LEADER},{type:POSITION,id:456}]},{id:gw,type:EXCLUSIVE_GATEWAY},{id:sub,type:SUBPROCESS,subprocessDefinition:LEAVE:1},{id:end,type:END}],edges:[{from:start,to:task_1},{from:gw,to:task_1,condition:days 3},{from:gw,to:end,condition:default}]}7 种节点类型START / END / TASK / 排他网关 / 并行分叉 / 并行汇聚 / 子流程3 种审批人规格指定用户 / 直属主管 LEADER / 职位 POSITION。一个诚实的演进注脚README 早期自述线性状态机实际代码已是有向图模型支持条件分支、并行分叉汇聚、子流程、回退边。parseWorkflowGraph保留了旧节点数组格式的兼容层——旧格式自动转等价线性图。格式升级时给旧数据留自动翻译比写迁移脚本便宜。二、编译期校验双向可达性环是合法的validateGraph的结构约束是常规的恰一个 START/END、TASK 一入一出、排他网关出边全带 condition 且恰一条 default、FORK 禁 condition、JOIN 入度 ≥2有含量的是双向可达性检查从 START 正向 BFS所有节点可达防孤岛从 END 逆向 BFS所有节点可达 END活性保证。第二条是审批引擎特有的流程必须保证每个节点都有出口通向终态。而环是合法的——回退边驳回到上一节点重审是审批流的真实需求单测里专门有TestValidateGraph_CycleAllowed。环带来的死循环风险不在编译期拒绝而在运行期用步数上限兜底下一节。编译期管结构运行期管活性——把约束放在成本最低的那一层是这类引擎设计的基本功。三、运行期推进递归 walker、token 语义与 fail-closed引擎核心是一个递归 walkeradvanceFromNode按节点类型分派START / 并行分叉沿出边递归FORK 对每条出边各递归一次——这就是 token 分裂TASK解析审批人 → 剔除申请人 → 若剩余为空写审批人为申请人本人自动通过日志继续否则事务内清残留任务 实例置 PENDING 每审批人一条任务后停住等审批排他网关对出边条件用 expr-lang 求值求值环境是实例的 form_data命中即走、全不中走 default、无 default 则 fail-closed 驳回并行汇聚arrived_count计数查不到行建行 1否则 1满入度删计数行继续未满 token 停此ENDcheckTermination——还有 PENDING 任务 / 未满 join / 挂起子流程任一存在就不终态子流程建子实例 写父子链接 父实例 SUSPENDED子流程终态后恢复父流程从 SUBPROCESS 出边继续。两个点睛设计步数守卫。回退环合法意味着 walker 理论上可以无限转解法不是 visited 集合拒环而是朴素计数constwalkerStepLimit1000ws.stepsifws.stepswalkerStepLimit{returns.failClosedReject(ctx,tid,uid,instanceID,walker step limit exceeded)}fail-closed 驳回。任何路由失败——条件全不中且无 default、定义被改坏、步数超限——统一走同一条收敛路径取消全部待办、删 join 行、删父子链接、实例置 REJECTED、通知申请人、触发业务回调。宁可错杀流程失败不让流程挂起。审批系统里卡住的流程比失败的流程代价高一个数量级——失败有人看见卡住没人知道。这个原则贯穿了寻人层主管找不到直接报错避免流程挂起和路由层。每次迈步都会重读定义重新解析图——流程定义是审批时解析而非实例内快照定义被改会影响在途实例。这是一个开放的权衡快照式更稳但复杂度高设计文档如实记录未下结论ERP 侧的审批轨选了快照式见后文对照两个项目的不同选择本身就是素材。四、会签与或签不是计数器是任务表状态推导TASK 节点的strategy字段ALL会签缺省、ANY或签未知值按会签处理fail-safe。实现上没有显式计数器列通过与否由查询任务状态推导ifnode.Task.isAnyStrategy(){// 或签一人通过即推进,取消其余待办任务s.taskRepo.CancelPendingByInstanceNode(ctx,tid,instanceID,nodeID,taskID)returns.advanceFromNode(...)}// 会签:全部任务通过才推进;仍有待办则等待其他审批人statuses,_:s.taskRepo.ListNodeTaskStatuses(ctx,tid,instanceID,nodeID)for_,st:rangestatuses{ifstworkflowtask.TaskStatusPending{returnemptypb.Empty{},nil}}returns.advanceFromNode(...)这个设计的隐性收益是部分同意的轨迹天然可查每人一条任务行 每人一条带 comment 的审批日志行3 人会签、2 人已同意、等待张三直接是两个 SELECT。计数器方案要额外维护计数与明细的一致性状态推导方案里明细就是唯一真相。任一驳回则不做节点级判断统一升级为全实例终结——取消全部待办注释原话“否则残留半死分支”并行分支一并杀、实例 REJECTED、通知、业务回调。真正需要计数器的只有并行汇聚JOIN 的arrived_count而它的实现是读-改-写而非原子 UPDATE——多审批人并发到达同一 JOIN 有竞态窗口。这是轻量级换正确性风险的自觉取舍单实例内审批天然低频写进了注释。对照 ERP 侧全部用 SQL 条件更新做并发见姊妹篇能清楚看到两个项目对并发窗口值不值得付复杂度的不同回答。五、寻人与委托审批时解析激活后快照寻人三种规格的解析发生在walker 走到 TASK 节点的那一刻resolveApprovers解析结果落assignee_user_id——此后该节点等审批期间的人员变动不影响在途任务。节点被重新激活回退边时清旧任务重新解析。主管寻人只找直属部门负责人不沿部门树向上递归——找不到直接报错。注释点明这是 fail-closed“在提交/推进时即失败避免流程挂起”。想支持多级上报加一层循环即可但默认行为选择宁可失败职位寻人展开为多用户 ID天然服务多人会签节点委托解析时点套用——命中委托规则直接把任务指给代理人而不是先指给原人再转发。委托表约束极简一租户内一委托人唯一、只能给自己设委托、被委托人须在职。撤回、转办、超时升级三个周边动作的语义都收敛得干净撤回是终态creator 本人 PENDING 前提单事务清理全部待办/join/父子链接转办只改任务的 assignee、日志记转办人原审批人在日志里留痕超时升级是 asynq 周期任务——24 小时催办MarkReminded幂等戳防每小时重发、72 小时自动转给审批人的组织负责人MarkEscalated戳避免沿组织树连环上转无上级可升降级为仅催办。六、业务挂钩终态一次性副作用引擎与业务的解耦是进程内回调注册表51 行的workflow_hook.go六个业务 service请假/报销/出差/加班/用印/外出在构造函数里eventRegistry.Register(businessType, svc.onInstanceTerminal)引擎只在三个终态APPROVED/REJECTED/WITHDRAWN同步调Fire。引擎不 import 任何业务包——依赖方向是业务依赖引擎。调用侧防护fireBusinessEvent带defer recover回调失败只记日志不影响状态机最终一致取向。回调侧契约才是精髓以请假为例onInstanceTerminal校验单据的instance_id与回调的 instanceID 一致——防客户端伪造 business 字段触发他人单据的副作用仅处理仍 PENDING 的单据——幂等防重复回调覆盖终态。请假额度的语义是这个契约最好的注脚扣减只发生在 APPROVED 终态AddUsedDaysent upsertREJECTED/WITHDRAWN 只同步状态——所以审批撤回要还额度吗的答案是不用还额度从未被扣过。提交时只做余额检查不做冻结。“终态一次性副作用优于提交即冻结”不用处理冻结/解冻的对称性撤回/驳回天然无需补偿。代价是提交到批准之间的并发窗口注释明说容忍并发窗口——又一次轻量级取舍。配套的还有流程定义自动引导租户首提请假时若无 LEAVE 定义按默认模板主管会签自动创建并启用并发首提撞唯一索引靠回读兜底——默认模板开箱可跑 vs 强制显式建模之间选了前者模板本身用的是旧线性格式正好被兼容层消化。七、结语这个引擎的全部复杂度预算花在了四个地方图的编译期校验双向可达性、异常路径的收敛failClosedReject宁可失败不挂起、并发语义的自觉降级状态推导代替计数器、容忍 JOIN 竞态窗口、副作用时机只在终态发生一次。它不支持的持久化 token 表、消息队列驱动、流程版本快照与它支持的同样重要——轻量级的本质是清楚地知道自己不做什么。如果只带走三条审批引擎最重要的不是路由能力是异常路径的收敛——所有走不下去都落到同一个可见的终态会签/或签用任务表状态推导——明细即真相部分同意的轨迹免费获得副作用只在终态发生一次——提交不冻结撤回不补偿幂等契约只需要两个校验。仓库引擎在backend/app/core/service/internal/service/workflow_service.go与workflow_graph.go设计文档docs/oa-workflow-design.mdGitHubgithub.com/tx7do/go-wind-oaGiteegitee.com/tx7do/go-wind-oa

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑