系列定位 :jeeflow 系列第 4 篇(第二季「核心设计」第 1 篇) 平台 :掘金(代码密度高,原理讲透) 素材版本 :引擎 v1.8.15,pro 站实时截图 前置阅读 :第 2 篇 · jeeflow:98KB 的工作流引擎长什么样
一、从一张流程图说起
打开 jeeflow-pro 集成演示站(jeeflow-pro.mldong.com),在「流程设计」里点开「报销申请」:

四个节点,一条直线:
scss
开始 → 发起申请 → 部门经理审批 → 财务复核(会签) → 总经理审批 → 结束
其中「财务复核」节点右上角挂着一个蓝色标签------并行会签。
这张图是给人看的。但引擎不吃图,引擎吃的是一份 JSON。
本文要回答的核心问题:这份 JSON 长什么样?它怎么把"画图"变成"跑流程"?
二、JSON 结构解剖
jeeflow 的流程定义文件是一个标准 JSON,位于 jeeflow-java/jeeflow-core/src/test/resources/flows/ 目录,五语言 demo 启动时统一加载。
以最简单的 01-simple.json(简单审批流程)为例:
json
{
"name": "simple",
"displayName": "简单审批流程",
"type": "approval",
"instanceUrl": "/form/apply",
"nodes": [ ... ],
"edges": [ ... ]
}
顶层只有 5 个字段:
| 字段 | 含义 | 示例 |
|---|---|---|
name |
唯一编码(程序用) | "simple" |
displayName |
显示名称(人看的) | "简单审批流程" |
type |
流程类型 | "approval" |
instanceUrl |
发起页路由 | "/form/apply" |
nodes / edges |
节点数组 + 边数组 | 见下文 |
2.1 节点(nodes)
每个节点是一个对象,核心字段:
json
{
"id": "task1",
"type": "snaker:task",
"x": 300,
"y": 200,
"properties": {
"width": 100,
"height": 50,
"form": "leave-form",
"assignee": "leader",
"taskType": 0,
"performType": 0
},
"text": {
"value": "上级审批"
}
}
| 字段 | 作用 |
|---|---|
id |
节点唯一标识,边通过 sourceNodeId / targetNodeId 引用 |
type |
节点类型:snaker:start / snaker:task / snaker:decision / snaker:end |
x / y |
画布坐标(设计器用,引擎不关心) |
properties.assignee |
审批人表达式 ------引擎通过 IUserProvider SPI 解析 |
properties.performType |
执行方式------0=普通,1=并行会签,2=串行会签 |
properties.countersignType |
会签类型 ------PARALLEL / SEQUENTIAL / RATIO |
properties.form |
关联表单路由 |
text.value |
节点显示文本 |
2.2 边(edges)
边定义节点间的连接关系:
json
{
"id": "e3",
"sourceNodeId": "decision1",
"targetNodeId": "task2",
"properties": {
"expr": "amount > 1000"
},
"text": {
"value": "金额>1000"
}
}
普通边的 properties 为空对象 {},条件边 才带 expr 字段------这就是引擎路由决策的依据。
2.3 节点类型体系
jeeflow 只有 4 种节点类型,覆盖所有流程模式:
| 类型 | 标识 | 作用 |
|---|---|---|
| 开始节点 | snaker:start |
流程入口,自动生成 |
| 任务节点 | snaker:task |
人工审批 / 自动任务 |
| 决策节点 | snaker:decision |
条件分支网关 |
| 结束节点 | snaker:end |
流程出口 |
没有"子流程节点"、没有"事件节点"、没有"定时器节点"------jeeflow 的哲学是用少量原语组合出复杂行为,而不是堆砌节点类型。
三、10 个共享流程逐一拆解
jeeflow 的测试资源目录里有 10 个共享流程 JSON (01-simple → 10-mixed-mode),五语言引擎共用同一份文件,启动时自动加载。
3.1 总览表
| # | 文件名 | 场景 | 核心模式 | 关键 JSON 特征 |
|---|---|---|---|---|
| 01 | simple |
简单审批 | 单任务 | 1 个 task,assignee=leader |
| 02 | multi-task |
串行多节点 | 线性串联 | 多个 task 首尾相连 |
| 03 | decision-expr |
条件分支 | 表达式网关 | edge 带 expr 条件 |
| 04 | fork-join |
并行分叉/汇聚 | fork+join | 分叉后多路径并行,join 汇聚 |
| 05 | countersign-parallel |
并行会签 | 多人同时审批 | performType:1 + countersignType:PARALLEL |
| 06 | countersign-sequential |
串行会签 | 逐个审批 | performType:2 + countersignType:SEQUENTIAL |
| 07 | countersign-ratio |
比例会签 | 通过比例阈值 | countersignType:RATIO + 比例值 |
| 08 | sequential-approve |
逐一审批 | 特殊会签模式 | 逐个创建任务,逐个完成 |
| 09 | with-reject |
驳回流程 | 退回上游 | edge 回指前序节点 |
| 10 | mixed-mode |
混合模式 | 组合以上所有 | 条件分支 + 会签 + 驳回 |
3.2 条件分支详解(03-decision-expr)
这是最常用也最容易被误解的模式。完整 JSON 结构:
json
{
"name": "decision-expr",
"displayName": "决策表达式流程",
"type": "approval",
"nodes": [
{
"id": "start",
"type": "snaker:start",
"text": { "value": "开始" }
},
{
"id": "apply",
"type": "snaker:task",
"properties": {
"assignee": "applicant",
"taskType": 0
},
"text": { "value": "发起申请" }
},
{
"id": "decision1",
"type": "snaker:decision",
"properties": {
"expr": "amount > 1000"
},
"text": { "value": "金额>1000?" }
},
{
"id": "task2",
"type": "snaker:task",
"properties": { "assignee": "manager" },
"text": { "value": "经理审批" }
},
{
"id": "task3",
"type": "snaker:task",
"properties": { "assignee": "director" },
"text": { "value": "总监审批" }
},
{
"id": "end",
"type": "snaker:end",
"text": { "value": "结束" }
}
],
"edges": [
{ "sourceNodeId": "start", "targetNodeId": "apply" },
{ "sourceNodeId": "apply", "targetNodeId": "decision1" },
{
"sourceNodeId": "decision1",
"targetNodeId": "task2",
"properties": { "expr": "amount > 1000" },
"text": { "value": "金额>1000" }
},
{
"sourceNodeId": "decision1",
"targetNodeId": "task3",
"properties": { "expr": "amount <= 1000" },
"text": { "value": "金额≤1000" }
},
{ "sourceNodeId": "task2", "targetNodeId": "end" },
{ "sourceNodeId": "task3", "targetNodeId": "end" }
]
}
路由逻辑:
bash
decision1 节点评估 expr → 遍历所有出边 → 找到第一条 expr 为 true 的边 → 走那条路
关键点:
decision1节点自身的properties.expr是默认表达式(兜底用)- 每条出边的
properties.expr是分支条件(优先匹配) - 引擎通过
IExpressionEvaluatorSPI 计算表达式,不绑定任何表达式引擎(SpEL / OGNL / 自研均可)
3.3 并行会签详解(05-countersign-parallel)
json
{
"id": "task1",
"type": "snaker:task",
"properties": {
"assignee": "userA,userB,userC",
"performType": "1",
"countersignType": "PARALLEL",
"field": {
"candidateUsers": "userA,userB,userC"
}
},
"text": { "value": "会签审批" }
}
三个关键字段决定会签行为:
| 字段 | 值 | 含义 |
|---|---|---|
performType |
"1" |
会签模式(0=普通,1=并行,2=串行) |
countersignType |
"PARALLEL" |
并行会签(所有人同时收到任务) |
assignee |
"userA,userB,userC" |
会签人员列表 |
引擎行为:
- 流程到达该节点时,一次性为所有会签人创建任务
- 每个任务独立审批,互不阻塞
- 所有人通过后,流程才流转到下一节点
- 任一节点驳回 → 整体会签驳回(一票否决,
submitType=20)
跨语言差异:Java 引擎一次建全部任务;Python/Node 引擎逐个创建。行为一致,实现策略不同------这是"契约对齐,实现自由"的典型体现。
四、从 JSON 到业务:pro 站的 9 个真实场景
共享 JSON 是"教科书",pro 站(jeeflow-pro.mldong.com)里跑的是"实战"。
4.1 流程定义列表
superAdmin 登录后进入「流程定义」,可以看到 28 条记录(9 个业务场景 × 多版本):

| 流程名称 | 唯一编码 | 流程类型 | 核心模式 |
|---|---|---|---|
| 请假申请 | biz_leave |
假勤管理 | 条件分支(天数网关) |
| 采购申请 | biz_purchase |
业务管理 | 条件分支(金额网关) |
| 报销申请 | biz_reimburse |
人事管理 | 串行 + 并行会签 |
| 用印申请 | biz_seal |
业务管理 | 简单审批 |
| 加班申请 | biz_overtime |
业务管理 | 条件分支(工时网关) |
| 合同审批 | biz_contract |
业务管理 | 条件分支(合同金额) |
| 出差申请 | biz_business_trip |
业务管理 | 条件分支(预算网关) |
| 岗位异动申请 | biz_transfer |
人事管理 | 串行多节点 |
| 资产领用申请 | biz_asset |
业务管理 | 简单审批 |
4.2 条件分支实战:请假申请
请假申请的设计器视图:

条件分支逻辑:
- ≤3 天 → 部门经理审批
- >3 天 → 分管领导审批
用 admin 账号发起一笔 1 天事假,流程轨迹如下:

绿色高亮路径清晰显示:开始 → 申请人(蒙立东) → 条件分支(≤3天路径) → 部门经理审批(李娜) → 结束。
对应的 JSON 核心结构:
json
{
"nodes": [
{ "id": "apply", "type": "snaker:task",
"properties": { "assignee": "applicant" },
"text": { "value": "发起申请" } },
{ "id": "decision_days", "type": "snaker:decision",
"properties": { "expr": "days > 3" } },
{ "id": "task_manager", "type": "snaker:task",
"properties": { "assignee": "deptLeader" },
"text": { "value": "部门经理审批" } },
{ "id": "task_director", "type": "snaker:task",
"properties": { "assignee": "chargeLeader" },
"text": { "value": "分管领导审批" } }
],
"edges": [
{ "sourceNodeId": "decision_days", "targetNodeId": "task_manager",
"properties": { "expr": "days <= 3" },
"text": { "value": "≤3天" } },
{ "sourceNodeId": "decision_days", "targetNodeId": "task_director",
"properties": { "expr": "days > 3" },
"text": { "value": ">3天" } }
]
}
4.3 金额网关实战:采购申请
采购申请走的是同样的条件分支模式,只是判断条件从"天数"变成了"金额":

- ≤2 万 → 部门经理审批
- >2 万 → 总经理审批
admin 发起一笔 10000 元采购,流程轨迹:

路径高亮显示走了"≤2万"分支,部门经理李娜审批通过。
4.4 并行会签实战:报销申请
这是最复杂的场景。设计器视图:

审批链路:申请人 → 部门经理 → 财务复核(并行会签) → 总经理 → 结束
admin 发起一笔 5000 元差旅报销,完整审批轨迹:

四个节点全部通过:
- 申请人 ✓ 蒙立东
- 部门经理审批 ✓ 李娜
- 并行会签 财务复核 ✓ 赵敏、黄红、蒙立东、吴昊、郑芳、李青(6 人全部通过)
- 总经理审批 ✓ 王强
注意:实际会签组有 6 人,不是设计器上看到的"财务复核"两个字。会签人员由
assignee字段 +IUserProviderSPI 动态解析,可以是固定列表、部门角色、甚至表达式计算结果。
4.5 映射关系总结
| 共享流程 | pro 站业务场景 | JSON 模式 |
|---|---|---|
03-decision-expr |
请假申请(天数网关) | 条件分支 |
03-decision-expr |
采购申请(金额网关 ≤2万) | 条件分支 |
03-decision-expr |
合同审批(10万阈值) | 条件分支 |
03-decision-expr |
出差申请(5000预算) | 条件分支 |
03-decision-expr |
加班申请(4h工时) | 条件分支 |
05-countersign-parallel |
报销申请(财务会签 ALL) | 并行会签 |
01-simple |
用印申请(单审批) | 简单审批 |
01-simple |
资产领用(单审批) | 简单审批 |
02-multi-task |
岗位异动(多节点串行) | 串行多节点 |
9 个业务场景 = 4 种 JSON 模式的排列组合。
五、版本管理与设计器
5.1 多版本共存
pro 站的流程定义列表显示 28 条记录------同一个流程(如"采购申请")有 v3、v4 多个版本共存。
版本管理的意义:
- 新版本不影响在途实例:已发起的流程按发起时的版本跑完
- 新发起的走最新版本:自动路由到最高版本号
- 回滚能力:随时切回旧版本
5.2 设计器操作
流程设计器(基于 mldong-flow-designer-plus npm 包)提供以下操作:
| 功能 | 说明 |
|---|---|
| 缩小/放大 | 画布缩放 |
| 适应 | 自动缩放至画布大小 |
| 清空 | 清除画布 |
| 查看数据 | 查看/编辑底层 JSON |
| 导入 | 从 JSON 文件导入流程定义 |
| 保存 | 保存当前设计到后端 |
| 全屏 | 全屏编辑模式 |
设计器是纯前端组件,不绑定任何后端------它产出 JSON,引擎消费 JSON,两者通过 JSON 契约解耦。
六、JSON 即协议
回到开篇的问题:这份 JSON 怎么把"画图"变成"跑流程"?
答案是三层解耦:
javascript
┌─────────────┐ JSON ┌──────────────┐ JSON ┌──────────────┐
│ 流程设计器 │ ───────────→ │ 流程定义文件 │ ───────────→ │ 工作流引擎 │
│ (vben5-wf) │ 产出 JSON │ (.json) │ 解析 JSON │ (jeeflow) │
└─────────────┘ └──────────────┘ └──────────────┘
前端 文件 后端
不绑定引擎 不绑定前端 不绑定框架
- 设计器不绑定引擎:flow-designer / vben5-wf 都能渲染同一份 JSON
- JSON 不绑定前端:它是纯数据,任何系统都能生成
- 引擎不绑定框架:Java/Go/Python/Node/PHP 五语言都能解析同一份 JSON
这就是 jeeflow "一套流程定义,四种语言实现" 的根基------不是口号,是一份 JSON 文件。
参考资料
- jeeflow GitHub 仓库
- jeeflow 文档站
- 开源演示站(五语言后端切换)
- 集成演示站(admin/123456)
- 共享流程 JSON 源码
下一篇预告 :[第 5 篇 · 用 DDD 设计工作流引擎:聚合根与充血模型](#第 5 篇 · 用 DDD 设计工作流引擎:聚合根与充血模型 "#") ------ ProcessInstance 为什么必须是聚合根?ProcessTask 作为子实体的职责边界在哪?为什么贫血模型扛不住工作流场景?