系列定位 :jeeflow 系列第 6 篇(第二季「核心设计」第 3 篇) 平台 :掘金(代码密度高,原理讲透) 素材版本 :引擎 v1.8.15,源码取自 jeeflow-java 前置阅读 :第 3 篇 · 状态机与 submitType · 第 5 篇 · DDD 聚合根与充血模型
一、一个约定,三条链路
在 jeeflow 的 10 个共享流程 JSON 里,有一个规律:每个流程的第一个任务节点,assignee 永远是 "applicant"。
json
{
"id": "apply",
"type": "snaker:task",
"properties": {
"assignee": "applicant",
"taskType": 0,
"performType": 0
},
"text": { "value": "发起申请" }
}
applicant 不是一个用户名,而是一个契约标记 ------引擎解析时把它替换成流程发起人(instance.operator)。
这个约定看似简单,但它串起了三条关键链路:
- 发起链路:谁发起流程,申请节点就分配给谁,自动完成
- 退回链路:任何审批人点"退回发起人",流程回到第一个任务节点,发起人收到新待办
- 重提链路:发起人改单后重新提交(submitType=5),流程继续往下走
没有这个约定,"退回发起人"就无从谈起------引擎不知道"发起人"是谁。
本文把这三条链路一次讲透。
二、发起链路:startAndExecute 契约
2.1 引擎不自动执行节点
jeeflow 引擎有一个重要设计原则:引擎只负责"路由",不负责"自动完成"。
startProcessInstanceById 启动流程后,引擎会执行开始节点(snaker:start),开始节点的输出边指向第一个任务节点(apply)。引擎为 apply 节点创建任务 ,但不会自动完成它。
java
// JeeflowEngineImpl.startProcessInstanceById 核心流程
public ProcessInstance startProcessInstanceById(Long defineId, String operator, FlowData args) {
// 1. 查流程定义
ProcessDefine define = repository.findDefineById(defineId);
// 2. 解析流程模型
ProcessModel model = ModelParser.parse(define.getContent());
// 3. 追加用户信息(u_userId / u_realName 等)
FlowUtil.addUserInfoToArgs(operator, args, userProvider);
// 4. 创建聚合根(operator 写入 instance.operator)
ProcessInstance instance = ProcessInstance.create(define, operator, args, null, null);
// 5. 持久化
repository.createInstance(instance);
// 6. 构建 Execution 并执行开始节点
Execution exec = buildExecution(model, instance, args, operator);
model.getStart().execute(exec); // → 触发 apply 节点,创建任务
return instance;
}
启动后,apply 节点的任务状态是 10(进行中) ,参与者是发起人。但它没有被完成 ------需要调用方显式调用 executeProcessTask。
2.2 调用方负责完成申请节点
这就是 startAndExecute 契约:启动 + 自动完成申请节点。
java
// JeeflowFacade.startAndExecute
private Map<String, Object> startAndExecute(Map<String, Object> args) {
Long defineId = toLong(args.get("processDefineId"));
String operator = toStr(args.get("operator"));
FlowData flowArgs = buildFlowArgs(args);
// 1. 启动流程(引擎创建 apply 任务,但不完成)
ProcessInstance inst = engine.startProcessInstanceById(defineId, operator, flowArgs);
// 2. 查找进行中任务(此时只有 apply 节点)
List<ProcessTask> doingTasks = repository.findDoingTasks(inst.getInstanceId(), null);
// 3. 自动完成申请节点
for (ProcessTask task : doingTasks) {
repository.addTaskActor(task.getTaskId(), Collections.singletonList(operator));
flowArgs.put(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode());
// 对齐 boot3:f_nextNodeOperator → tf_nextNodeOperator
Object startNextOp = flowArgs.get("f_nextNodeOperator");
if (startNextOp != null && !startNextOp.toString().isEmpty()) {
flowArgs.put("tf_nextNodeOperator", startNextOp);
}
engine.executeProcessTask(task.getTaskId(), operator, flowArgs);
}
return ok(Collections.singletonMap("processInstanceId", inst.getInstanceId()));
}
为什么引擎不自己做这件事?
因为"申请节点"的语义是业务层的------引擎不知道哪个节点是"申请节点",它只知道"第一个任务节点"。由调用方(Facade / Controller)决定"启动后自动完成第一个任务",引擎保持纯粹的路由职责。
2.3 "applicant" 的解析过程
apply 节点的 assignee: "applicant" 是怎么变成发起人的?
java
// CreateTaskHandler.resolveActors --- 参与者解析核心逻辑
private List<String> resolveActors(TaskModel taskModel, ProcessModel model, Execution execution) {
List<String> actors = new ArrayList<>();
String assignee = taskModel.getAssignee();
for (String raw : assignee.split(",")) {
String token = raw.trim();
// ★★★ 契约核心:applicant → instance.operator ★★★
if (token.contains("applicant")) {
token = token.replace("applicant",
execution.getProcessInstance().getOperator());
}
// 尝试从流程变量中取值
Object v = execution.getArgs().get(token);
if (v != null) {
actors.add(v.toString());
} else {
// 变量未命中 → token 本身作为字面量
actors.add(token);
}
}
return actors;
}
解析过程:
- 读到
assignee: "applicant" "applicant"包含关键字"applicant"→ 替换为instance.getOperator()(即发起人,如"admin")"admin"作为 token 在流程变量中查找 → 未命中"admin"作为字面量加入参与者列表
五语言同构 :Go/Python/Node/PHP 引擎都有完全相同的解析逻辑------p == "applicant" → inst.Operator。
三、退回链路:submitType=6
3.1 完整的退回发起人流程
用户在审批页面点"退回发起人"按钮 → 前端调用 processTask/execute,传 submitType=6:
java
// JeeflowFacade.execute --- submitType=6 分支
if (ProcessSubmitTypeEnum.ROLLBACK_TO_OPERATOR.getCode().equals(submitType)) {
engine.executeAndJumpToFirstTaskNode(taskId, operator, flowArgs);
}
3.2 引擎实现:强制覆盖 assignee
java
// JeeflowEngineImpl.executeAndJumpToFirstTaskNode
@Override
public List<ProcessTask> executeAndJumpToFirstTaskNode(
Long taskId, String operator, FlowData args) {
return runInTx(() -> {
Execution exec = prepareExecution(taskId, operator, args);
if (exec == null) return Collections.emptyList();
ProcessModel model = exec.getProcessModel();
// 关键:从 start 节点沿输出边找到第一个任务节点
for (TransitionModel tm : model.getStart().getOutputs()) {
tm.setEnabled(true);
if (tm.getTarget() instanceof TaskModel) {
// ★★★ 强制 assignee = 发起人 ★★★
((TaskModel) tm.getTarget())
.setAssignee(exec.getProcessInstance().getOperator());
}
tm.execute(exec); // 创建新任务
}
persistTasks(exec);
return exec.getProcessTaskList();
});
}
核心动作只有两步:
- 找到第一个任务节点(apply)
- 强制把 assignee 设为发起人 (
instance.getOperator())
不管 JSON 里原来写的是什么,退回发起人时一律覆盖为发起人。
3.3 状态变化
| 阶段 | 实例状态 | 任务状态 |
|---|---|---|
| 退回前 | 10(进行中) | 当前审批任务 10→99(废弃) |
| 退回后 | 10(进行中,不变) | 新 apply 任务 10(发起人收到待办) |
注意:实例状态不变,还是 10。只有当前审批人的任务被废弃(99),同时为发起人创建一个新任务。
四、重提链路:submitType=5
发起人收到退回的待办后,修改表单数据,点"重新提交":
java
// submitType=5 → 走普通 executeProcessTask 路径
// 但有一个关键区别:f_nextNodeOperator 变量
重提时,前端可以传 f_nextNodeOperator(流程启动时下一节点处理人),引擎会把它转成 tf_nextNodeOperator,跳过正常的 assignee 解析,直接指定下一节点处理人。
java
// startAndExecute 中的变量转换
Object startNextOp = flowArgs.get("f_nextNodeOperator");
if (startNextOp != null && !startNextOp.toString().isEmpty()) {
flowArgs.put("tf_nextNodeOperator", startNextOp);
}
这意味着发起人重新提交时,可以指定审批人(比如换个领导审批),而不必走原来的 assignee 解析链路。
五、系统身份:flow.auto 与 flow.admin
在参与者解析和权限校验中,有两个特殊身份:
| 身份 | 常量 | 含义 |
|---|---|---|
flow.auto |
FlowConst.AUTO_ID |
系统自动执行(如定时任务触发) |
flow.admin |
FlowConst.ADMIN_ID |
超级管理员代执行 |
它们在两个地方生效:
1. 权限放行 (ProcessTask.isAllowed):
java
public boolean isAllowed(String operator) {
if (FlowConst.AUTO_ID.equalsIgnoreCase(operator)
|| FlowConst.ADMIN_ID.equalsIgnoreCase(operator)) {
return true; // 系统身份直接放行
}
return isDoing() && this.actorIds != null
&& this.actorIds.contains(operator);
}
2. 跳过用户信息注入 (FlowUtil.addUserInfoToArgs):
java
public static void addUserInfoToArgs(String operator, FlowData args, IUserProvider userProvider) {
if (FlowConst.AUTO_ID.equalsIgnoreCase(operator)
|| FlowConst.ADMIN_ID.equalsIgnoreCase(operator)) {
return; // 非真实用户,跳过 u_userId 等变量注入
}
// 正常用户:注入 u_userId / u_realName / u_deptId 等
...
}
六、内置参与者处理器
除了 assignee 字段,jeeflow 还提供一套内置参与者处理器 (AssignmentHandler SPI),按优先级排序:
| 处理器 | 说明 | 优先级 |
|---|---|---|
OperatorAssignmentHandler |
流程发起人 | -9999(最高) |
ApplicantDeptLeaderAssignmentHandler |
发起人所属部门经理 | 10 |
ApplicantDeptMainLeaderAssignmentHandler |
发起人所属部门分管领导 | 20 |
DeptLeaderAssignmentHandler |
当前用户所属部门经理 | 30 |
DeptMainLeaderAssignmentHandler |
当前用户所属部门分管领导 | 40 |
FormFieldAssigneeHandler |
根据表单字段值分配 | 50 |
TaskRoleAssigneeHandler |
根据任务节点关联角色分配 | 60 |
这些处理器通过 HandlerRegistry 注册,引擎在 assignee 为空时按优先级依次尝试。
关键设计 :OperatorAssignmentHandler(流程发起人)优先级最高(-9999),确保"发起人"这个身份在任何场景下都能被正确解析。
七、闭环全景
把三条链路串起来,看一个完整的"退回-重提"闭环:
ini
admin 发起请假申请
→ startAndExecute(operator="admin")
→ instance.operator = "admin"
→ apply 节点 assignee="applicant" → 解析为 "admin"
→ 自动完成 apply(submitType=0)
→ 流程推进到 task1(部门经理审批,assignee="deptLeader")
lina(部门经理)审批中...
→ 点"退回发起人"(submitType=6)
→ executeAndJumpToFirstTaskNode
→ apply 节点 assignee 强制 = instance.operator = "admin"
→ lina 的任务废弃(99)
→ admin 收到新待办(apply 节点,状态 10)
admin 修改请假天数,重新提交(submitType=5)
→ executeProcessTask(applyTaskId, "admin", args)
→ 流程重新推进到 task1(部门经理审批)
这就是"applicant 契约"的完整闭环 ------从发起到退回到重提,发起人的身份始终由 instance.operator 承载,不依赖外部配置。
结语
一个
assignee: "applicant",串起了发起、退回、重提三条链路。
这不是语法糖,而是引擎与调用方之间的契约:
- 引擎承诺:
instance.operator全局可访问,applicant标记统一解析 - 调用方承诺:每个流程第一个任务节点是申请节点,启动后自动完成
有了这个契约,"退回发起人"不再是一个特殊的 hack,而是引擎能力的自然延伸。
下一篇预告:[第 7 篇 · 零依赖的秘密:6 大 SPI 设计](#第 7 篇 · 零依赖的秘密:6 大 SPI 设计 "#") ------ 引擎核心 98KB,不碰 JSON 库和 ORM,靠什么扩展?
参考资料
- jeeflow GitHub 仓库
- jeeflow 文档站
- 开源演示站(五语言后端切换)
- 集成演示站(admin/123456)
- ProcessInstance 源码
- JeeflowEngineImpl 源码
- CreateTaskHandler 源码
下一篇预告 :[第 7 篇 · 零依赖的秘密:6 大 SPI 设计](#第 7 篇 · 零依赖的秘密:6 大 SPI 设计 "#") ------ 引擎核心 98KB,不碰 JSON 库和 ORM,靠什么扩展?IProcessRepository / IUserProvider / IJsonProvider / IExpressionEvaluator / IIdGenerator / ITransactionTemplate 逐一拆解。