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 | 已废弃(驳回/跳转时清理其他进行中任务) |

三个容易忽略的设计点:
- 实例只有"完成"没有"同意"——实例 20 是终态,由最后一个任务完成后触发;
- 任务有"废弃"(99)——退回、跳转时,被跳过的待办任务要作废,不然"待办列表"会残留幽灵任务。这个 99 态是很多自研引擎容易漏掉的;
- 支线状态靠"级联"——撤回/终止/挂起是实例级命令,任务状态随实例级联落库(
updateInstance同连接持久化,v1.0.1 契约),引擎不会留下"实例停了任务还在跑"的半吊子状态。
二、submitType:业务动作怎么驱动状态
状态机是"静态地图",submitType(提交类型)就是"驾驶指令"。这是 mldong 框架的 ProcessSubmitTypeEnum 与 jeeflow 完全对齐的枚举,8 个取值:
| code | 枚举 | 调用方行为 | 效果 |
|---|---|---|---|
| 0 | APPLY | executeProcessTask | 发起 / 重新提交 |
| 1 | AGREE | executeProcessTask | 同意 |
| 2 | REJECT | executeAndJumpToEnd | 实例 → 45 已拒绝,无新待办 |
| 3 | ROLLBACK | 沿边回溯上一任务节点 → executeAndJumpTask | 退回上一步审批人,实例保持 10 |
| 4 | JUMP | executeAndJumpTask(..., taskName) | 跳到指定已办节点(taskName 取自可跳转列表) |
| 5 | RE_APPLY | executeProcessTask | 重新提交 |
| 6 | ROLLBACK_TO_OPERATOR | executeAndJumpToFirstTaskNode | 第一个任务节点重执行、参与者强制为发起人 → 发起人收新待办,实例保持 10 |
| 20 | COUNTERSIGN_DISAGREE | executeProcessTask + 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 REJECT | executeAndJumpToEnd |
| 3 ROLLBACK | executeAndJumpTask(target=null)(沿边回溯) |
| 4 JUMP | executeAndJumpTask(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 + 统一前端),发起一个流程、同意、退回、撤回,状态机怎么转一目了然。
相关链接
- jeeflow 文档站:jeeflow-doc.mldong.com
- 开源演示站(在线体验):jeeflow-demo.mldong.com
- 集成演示站:jeeflow-pro.mldong.com
- 引擎仓库(GitHub · mldong 组织):
jeeflow-java/jeeflow-go/jeeflow-python/jeeflow-node/jeeflow-ui