Skip to content

设计原理 03 · 执行引擎——一次审批的完整旅程

系列:jeeflow 工作流引擎设计原理

1. 核心抽象

执行引擎的本质是一个图遍历器:从 start 节点出发,沿着边推进,在任务节点停下生成待办,在决策节点选路,在 fork/join 处分合。

FlowModel(流程定义)
 ├── nodes[]:start / task / decision / fork / join / end / custom
 └── edges[]:sourceNodeId → targetNodeId(决策边带 expr + text.value)

执行状态 = ProcessInstance + 当前节点指针(通过 task.taskName 反查节点)

关键设计:任务完成后,引擎通过 task.taskName 在 FlowModel 里反查节点,再 followEdges 找到下一个节点——不需要在实例里维护"当前节点"指针,任务本身就是指针。这简化了持久化模型(5 张表,无游标表)。

2. 启动流程

2.1 startProcessInstanceById(引擎职责)

1. findDefineById → JSON → FlowModel
2. ProcessInstance.create(工厂:state=10,注入用户变量)
3. saveInstance
4. 发布 PROCESS_START 事件
5. 找到 start 节点 → followEdges → 逐个 executeNode

启动后生成的第一个任务:发起申请节点(assignee="applicant" → 解析为发起人)。

2.2 startAndExecute(调用方契约,引擎不内置)

1. 调 startProcessInstanceById
2. 取所有进行中任务
3. 逐个 executeProcessTask(submitType=0 APPLY)

为什么引擎不自动做第 2、3 步?因为"第一个任务是否自动完成"是业务决策——有些流程第一个节点就是申请人填单(需要手动),有些是纯审批流(自动跳过)。引擎保持中立,由调用方选择。详见 06-契约约定

3. 完成任务(核心路径)

executeProcessTask(taskId, operator, args):
  ┌─ loadAndCheck
  │    task = findTaskById
  │    校验:task.taskState == DOING
  │    校验:task.isAllowed(operator)      # actorIds 包含 operator
  │    inst = findInstanceById

  ├─ 聚合根:inst.completeTask(task, operator, vars)
  │    task.finish(...)                     # 10→20
  │    inst.variables 合并

  ├─ 持久化 + 发布 TASK_COMPLETE 事件

  └─ 推进(关键路径):
       curNode = findNode(flow, task.taskName)
       for next in followEdges(curNode):
           case next.type:
             END      → inst.finish() + 发布 PROCESS_FINISH
             TASK     → executeNode → createTask(解析参与者)
             DECISION → evaluateDecision(选一条出边)
             FORK     → 递归 executeNode 每条出边
             JOIN     → 若无进行中任务才放行

4. 决策节点:三种求值优先级

evaluateDecision(node):
  1. Registry 按名解析(decisionHandler/assignmentHandler)
       → 返回目标边 ID → 直接跳转
  2. 扩展注入的 DecisionHandler
  3. 出边 expr 表达式求值(ExpressionEvaluator SPI)
       → 遍历出边,第一个 expr 为真的边获胜
       → 若出边无 expr,作为默认分支

优先级设计理由:Registry(注册的处理器)是编译期可确定的强约定;表达式是运行期自由求值。有处理器走处理器,没有才回退表达式——业务方按需选择。

5. Fork / Join 语义

FORK:无条件并行——递归执行每条出边(生成多个任务)
JOIN:等待所有分支完成——只有 findDoingTasks 为空时才放行

Join 的实现不统计分支数,而是问聚合根"还有没有进行中任务":

JOIN 放行条件 = inst 无任何 DOING 任务

为什么可以这么简化:因为 fork 之后只可能产生任务节点(决策/嵌套 fork 最终都落到任务),任务完成顺序不定,但只要还有任务在做,join 就等着;全部完成则放行。这是 snaker 风格引擎的经典做法,牺牲了"分支数精确计数",换来了模型简单(不需要在实例上记录分支计数)。

6. 会签:三种模式

会签节点的判定:performType=1 且有 countersignType

6.1 并行会签(PARALLEL)

createTask: 为每个 actor 创建一个独立任务(同 taskName)
完成一个:检查是否还有同节点 DOING 任务 → 有则等待
全部完成:走下一节点

6.2 串行会签(SEQUENTIAL)

createTask: 只创建第一个 actor 的任务
            任务变量写入 operatorList / loopCounter / nrOfInstances
完成任务:loopCounter+1 < nrOfInstances → 创建下一个 actor 的任务
          否则 → 走下一节点

串行会签的关键:进度存在任务变量里operatorList_${nodeId} 等),不落表——因为串行是"同一节点的多次实例化",复用同一 taskName。

6.3 按比例会签(RATIO)

按并行方式创建任务,放行条件由 countersignCompletionCondition 表达式决定
(如 #nrOfCompletedInstances>=2,已支持,见 07-countersign-ratio.json)

按比例的阈值判断(如"2/4 同意即通过")通过会签节点 countersignCompletionCondition 表达式实现(如 #nrOfCompletedInstances>=2),已支持。

7. 驳回与跳转

executeAndJumpToEnd(taskId, operator, args):     # 拒绝(REJECT=2)→ 跳结束
  聚合根 abandonAllDoing → 完成任务 → inst.reject()(10→45)

executeAndJumpTask(taskId, operator, args, target):  # 跳转(JUMP=4)/ 退回上一步(ROLLBACK=3)
  聚合根 abandonAllDoing → 完成任务
  target 节点 → executeNode(重新生成目标任务)

executeAndJumpToFirstTaskNode(taskId, operator, args):  # 退回发起人(ROLLBACK_TO_OPERATOR=6)
  聚合根 abandonAllDoing → 完成任务
  第一个任务节点重执行,参与者强制为发起人 → 发起人收到新待办(实例保持 10)

submitType 行为(与 mldong 框架一致)submitType=2(REJECT)executeAndJumpToEnd(实例→45,无新待办);退回发起人用 submitType=6executeAndJumpToFirstTaskNode。详见 06-契约约定

8. 拦截器与事件挂载点

executeNode(node):
  preHandle(node, instance)          # 按 order 升序;返回 false 中断该节点执行
      ├── 节点分派(task/decision/fork/join/end)
  postHandle(node, instance)         # 无论成功失败都执行

事件挂载点:
  PROCESS_START    启动流程
  PROCESS_FINISH   到达 end
  PROCESS_REJECT   拒绝(跳结束)
  TASK_CREATE      创建任务(扩展预留)
  TASK_COMPLETE    完成任务

9. 一次完整旅程(时序)

发起人: startAndExecute
  start → apply(自动完成) → 组长审批
组长:   executeProcessTask(同意, submitType=1)
  → 经理审批
经理:   executeAndJumpToFirstTaskNode(退回发起人, submitType=6)
  → apply 重执行(参与者=发起人)→ 发起人收到新待办
发起人: executeProcessTask(重新提交, submitType=0)
  → 组长审批 → 经理审批 → 组长审批... 直到 end
  → inst.finish() → PROCESS_FINISH

10. 关键源码位置

主题JavaGoNode.jsPython
启动/执行/跳转core/JeeflowEngineImpl.javaengine/engine_impl.gosrc/engine.tsjeeflow/engine.py
节点遍历 executeNodecore/JeeflowEngineImpl.javaengine/engine_impl.gosrc/engine.tsjeeflow/engine.py
决策求值core/...engine/engine_impl.gosrc/engine.tsjeeflow/engine.py
会签创建handler/impl/CreateTaskHandler.javaengine/engine_impl.gosrc/engine.tsjeeflow/engine.py
聚合根状态转换domain/ProcessInstance.javamodel/instance.gosrc/model.tsjeeflow/model.py

下一篇04 · 扩展机制——拦截器、事件与 HandlerRegistry

jeeflow · 轻量级多语言工作流引擎