工作流引擎的灵魂:状态机与 submitType

0 阅读1分钟

jeeflow 系列 · 第 3 篇(认识季)


画流程图的时候,一切都很美好:发起 → 部门审批 → 财务审批 → 结束,几条箭头,干净利落。

但引擎真正要回答的是另一组问题:流程发起后,现在走到哪了?审批中被人退回一步,这算什么状态?发起人撤回了,其他人在等什么?管理员要强行终止,任务们怎么办?

这些问题,流程图答不了——状态机才答得了。这是工作流引擎的"灵魂"。这篇把 jeeflow 的双层状态机和 8 种提交类型一次讲透。

本文内容对齐 jeeflow v1.8.11。本系列由 mldong 快速开发框架生态延续而来,建议按顺序阅读:第 2 篇讲了 jeeflow 的整体设计(98KB / SPI 体系 / DDD 聚合根),本篇深入状态层。


一、为什么是"双层"状态机?

一个流程实例,可能同时存在多个任务:并行分支里,task2 和 task3 同时在跑。所以状态必须分两层:

  • 实例状态:整个流程走到哪了
  • 任务状态:某一个具体审批项走到哪了

jeeflow 的状态机是跨语言契约(四语言枚举值完全一致):

状态码含义
实例10进行中
实例20已完成(正常走到 end)
实例30已撤回(发起人撤回)
实例40强行终止(管理员终止)
实例45已拒绝(驳回/拒绝)
实例50挂起(暂停)
实例99已废弃
任务10待办
任务20已完成
任务30 / 40 / 50撤回 / 终止 / 挂起(随实例级联)
任务99已废弃(驳回/跳转时清理其他进行中任务)

jeeflow 双层状态机(v1.8.4)

三个容易忽略的设计点:

  1. 实例只有"完成"没有"同意"——实例 20 是终态,由最后一个任务完成后触发;
  2. 任务有"废弃"(99)——退回、跳转时,被跳过的待办任务要作废,不然"待办列表"会残留幽灵任务。这个 99 态是很多自研引擎容易漏掉的;
  3. 支线状态靠"级联"——撤回/终止/挂起是实例级命令,任务状态随实例级联落库updateInstance 同连接持久化,v1.0.1 契约),引擎不会留下"实例停了任务还在跑"的半吊子状态。

二、submitType:业务动作怎么驱动状态

状态机是"静态地图",submitType(提交类型)就是"驾驶指令"。这是 mldong 框架的 ProcessSubmitTypeEnum 与 jeeflow 完全对齐的枚举,8 个取值:

code枚举调用方行为效果
0APPLYexecuteProcessTask发起 / 重新提交
1AGREEexecuteProcessTask同意
2REJECTexecuteAndJumpToEnd实例 → 45 已拒绝,无新待办
3ROLLBACK沿边回溯上一任务节点 → executeAndJumpTask退回上一步审批人,实例保持 10
4JUMPexecuteAndJumpTask(..., taskName)跳到指定已办节点(taskName 取自可跳转列表)
5RE_APPLYexecuteProcessTask重新提交
6ROLLBACK_TO_OPERATORexecuteAndJumpToFirstTaskNode第一个任务节点重执行、参与者强制为发起人 → 发起人收新待办,实例保持 10
20COUNTERSIGN_DISAGREEexecuteProcessTask + countersignDisagreeFlag=1会签一票否决

行为细节以 jeeflow-doc 规范(SPEC)为准;各枚举的引擎方法四语言一一对应(Java / Go / Python / Node 同签名)。

几个容易搞混的点,展开说:

拒绝(2)≠ 退回(3)。 拒绝是终止:executeAndJumpToEnd,实例直接变 45,不会再产生任何待办。退回是回溯:沿入边向上找到最近的任务节点(跳过 decision/fork/join),重新生成任务给上一步审批人,实例保持进行中。一个判死刑,一个打回重审——语义完全不同。

退回发起人(6)是个闭环。 它依赖一个约定:每个流程的第一个任务节点必须是"发起申请"节点(assignee 为 applicant,解析为流程发起人)。退回发起人 = 让第一个任务节点重新执行、参与者强制改为发起人。发起人收到新待办后,改单重新提交(5 RE_APPLY),流程继续。没有这个约定,退回发起人就无从谈起——这也是 jeeflow 所有共享流程都遵守"第一节点是申请节点"的原因。

会签否决(20)是"一票否决"。 并行/串行会签中,任一参与者传 20 + countersignDisagreeFlag=1,整个会签节点按否决处理,其他还在等的任务随之废弃。

三、引擎之上的分发:门面 execute

在 jeeflow 的统一门面 JeeflowFacade.flow(action, map) 里,processTask/execute 按 submitType 分发到对应引擎方法(对齐 boot3 的 ProcessTaskController.execute):

submitType分发目标
0 APPLY / 1 AGREE / 20 会签否决executeProcessTask
2 REJECTexecuteAndJumpToEnd
3 ROLLBACKexecuteAndJumpTask(target=null)(沿边回溯)
4 JUMPexecuteAndJumpTask(target=taskName)
5 重新提交业务方语义(发起人重新发起,门面透传)
6 退回发起人executeAndJumpToFirstTaskNode

四、设计洞察:为什么"提交类型"暴露给调用方?

有朋友问过:提交类型不是引擎内部的事吗?为什么要把 submitType 作为接口的一等公民?

因为业务方才是流程动作的发起者:页面上"同意/驳回/退回/跳转"按钮,对应到接口就是 submitType=1/2/3/4。mldong 生态的前端(vben5 + 流程设计器)按这套枚举渲染按钮和提交请求,引擎、接口、前端三方共用同一份语义——这就是"契约对齐"的威力:换引擎(比如 jeeflow 的四个语言实现)不换契约,前端零改动。

所以你在 jeeflow 的四个 demo 里会看到完全一致的接口行为:code=0 成功、msg 字段、submitType 全枚举、highLight/approvalRecord 独立端点——不是巧合,是契约。

结语

流程图是皮,状态机是骨,submitType 是筋。理解了"双层状态 + 8 种提交类型",你就理解了工作流引擎最核心的执行语义——后面讲流程定义、会签、退回机制、持久化演进时,都是在这张骨架上盖房子。

下一篇预告:《流程定义设计:一份 LogicFlow JSON 全解》——节点、边、表达式怎么描述一个流程?10 个共享流程 JSON 逐个拆解。

想亲手点点?四语言演示站已上线,在线体验:jeeflow-demo.mldong.com(Java :8080 / Go :8081 / Python :8100 / Node :8082 + 统一前端),发起一个流程、同意、退回、撤回,状态机怎么转一目了然。

相关链接