引言
在上一篇中,我们从底层架构拆解了工作流引擎的执行机制(Execution流转)与持久化机制(数据库状态机)。在标准的 BPMN 2.0 规范下,流程是高度结构化且单向向前的。
但在真实的业务场景中,流程绝不仅仅是"同意"和"下一步"。业务人员会提出大量打破常规流转规则的需求。本篇将直接抛弃比喻,纯粹从技术实现与底层 API 出发,直面工作流开发中最棘手的四大问题,并给出破局方案。
1 复杂工作流的破局之道:从驳回、加签到会签
1.1 业务痛点
流程定义为 A(发起) -> B(部门主管) -> C(总监) -> D(财务)。
当流程流转到 D 时,财务发现单据有问题,要求直接打回给 B 修改,或者打回给 A 重新发起。
BPMN 2.0 规范中并没有原生的"驳回"连线,如果在画图时将每个节点都连一条线回到起点,会导致流程图极其混乱(面条图),且无法维护。
1.2 技术解析
工作流引擎的核心是维护 Execution(执行实例)在 Activity(节点)之间的位置。向前流转是引擎解析连线(Sequence Flow)自动计算的。要实现向后跳转,我们需要跳过连线解析,直接通过 API 强行修改 Execution 的当前指针位置。
1.3 解决方案(以 Flowable 为例)
现代开源引擎(如 Flowable 6.x)提供了专门的节点跳转 API:ChangeActivityStateBuilder。
- 获取当前节点与目标节点:通过 API 获取当前正在执行的任务节点 ID,以及需要驳回到的目标节点 ID
- 清理当前状态:引擎需要先作废当前节点产生的所有待办任务(Task),并清理当前节点作用域内的局部变量
- 指针转移 :通过 API 直接将
Execution的指针移动到目标节点 - 触发执行:引擎在目标节点重新触发进入逻辑,生成新的待办任务
核心代码逻辑:
java
// 使用 Flowable 原生 API 实现跳转
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveActivityIdTo(currentActivityId, targetActivityId) // 从当前节点跳到目标节点
.changeState();
注意:在执行跳转前,通常需要往历史表中写入一条自定义的批注(Comment),记录"由节点D驳回至节点B,原因:发票不符",以保证审计记录的完整性。
2 问题二:审批人需要临时增加人员,如何实现"动态加签"?
2.1 业务痛点
流程定义中,当前节点是"技术总监审批"。但在实际审批时,技术总监认为这个方案涉及安全问题,需要临时让"安全总监"先审批,或者和"安全总监"一起审批。这个"安全总监"在原流程图中根本不存在。
2.2 技术解析
这涉及两种不同的加签模式,底层处理逻辑完全不同:
- 前加签(顺序执行):安全总监先批,批完再还给技术总监
- 并签(并行执行):安全总监和技术总监同时审批,两人都同意才算过
2.3 解决方案
2.3.1 场景A:前加签(基于任务委派 Delegation)
引擎原生支持任务的委派。技术总监不直接完成任务,而是调用 taskService.delegateTask(taskId, "安全总监")。
此时,底层表 ACT_RU_TASK 中该任务的 ASSIGNEE_(处理人)会变成安全总监,而 OWNER_(所有者)保留为技术总监,任务状态变为 PENDING。
安全总监处理完成后,调用 taskService.resolveTask(taskId),任务处理人会自动切回技术总监,流程并没有往下走,完美实现前加签。
2.3.2 场景B:并签(基于多实例动态扩展)
如果要在运行时增加一个并行任务,直接 new Task() 插入数据库是极其危险的,因为独立生成的任务脱离了 Execution 的生命周期管控,完成后无法触发流程继续。
正确的做法是:将该节点在画图时配置为"多实例(Multi-Instance)"节点 。
当需要动态加签时,通过调用引擎的多实例扩展 API:
java
// 动态向多实例节点中追加一个执行实例和任务
runtimeService.addMultiInstanceExecution(
activityId,
processInstanceId,
Collections.singletonMap("assignee", "安全总监")
);
引擎会基于当前节点的配置,动态分裂出一个新的 Execution 并生成对应的任务,统一纳入多实例的完成条件计算中。
3 问题三:"5人中3人同意即可",如何实现复杂的"会签/或签"逻辑?
3.1 业务痛点
一个评审节点由 5 名专家组成。业务规则不是"全部同意才通过",而是"只要有 3 人同意即通过";或者"只要有 1 人拒绝,直接一票否决,流程结束"。
3.2 技术解析
在标准节点中,一个任务完成,流程就走向下一步。针对多人审批,必须使用 多实例(Multi-Instance) 节点。
引擎在运行多实例时,会在内部维护三个极其重要的内置变量:
nrOfInstances:总实例数(比如 5)nrOfCompletedInstances:已完成的实例数nrOfActiveInstances:当前活动的实例数
3.3 解决方案:基于完成条件(Completion Condition)的表达式注入
在流程定义的 XML 中,为多实例节点配置 <completionCondition> 标签。引擎在每一次单个任务完成时,都会计算这个表达式。如果结果为 true,引擎会立刻销毁剩余未完成的实例,并驱动流程走向下一个节点。
3.3.1 实现"3人同意即通过"
配置表达式:${nrOfCompletedInstances >= 3}。
当第 3 个人点击同意并完成任务时,表达式返回 true,剩下的 2 个待办任务会被引擎物理删除,流程进入下一环节。
3.3.2 实现"一票否决"
- 任何人在点击"拒绝"时,通过 API 向引擎注入一个局部变量:
taskService.setVariableLocal(taskId, "isVeto", true) - 配置完成条件表达式:
${isVeto == true || nrOfCompletedInstances == nrOfInstances} - 只要有人触发了否决变量,表达式立即成立,多实例结束。随后在节点后方连接一个排他网关(Exclusive Gateway),根据
isVeto的值路由到结束节点
4 问题四:发起人点错提交了,如何实现"流程撤回"?
4.1 业务痛点
员工发起请假单,流程已经流转到了部门主管。员工发现天数填错了,想要撤回单据重新修改。
4.2 技术解析
撤回本质上是一种附带条件限制的特定节点跳转 。它与"驳回"的技术实现完全一致(都是通过修改 Execution 指针实现),区别在于业务校验逻辑。
4.3 解决方案:严格的校验前置 + 跳转 API
- 状态校验 :撤回操作必须在后端进行严格的校验。查询当前流程实例的活动任务列表(
taskService.createTaskQuery())。如果目标任务已经被主管签收(Claim)或完成(Complete),则抛出业务异常,拒绝撤回 - 执行跳转 :如果主管还未处理,调用与"驳回"相同的
ChangeActivityStateBuilderAPI,将Execution从主管节点强制拉回到"开始节点"或专门的"草稿节点" - 数据清理 :撤回后,业务系统需将自身业务表(如
leave_request)的状态从"审批中"更新回"草稿",并允许用户修改表单数据
5 总结
通过对以上四个高频复杂问题的拆解,我们可以得出一个关于工作流引擎的核心结论:
工作流引擎不仅是一套严格遵守 BPMN 规范的解析器,更是一个提供底层状态机操作 API 的基础平台。
面对复杂的业务需求,单纯依靠画图(配置)是无法解决所有问题的。架构师和开发者必须跳出"连线流转"的思维局限,深刻理解 Execution(执行实例)、Task(任务)、Variable(变量)之间的关系,通过代码调用引擎提供的干预 API(如跳转、委派、多实例动态干预),结合业务系统自身的逻辑校验,才能构建出既规范又灵活的企业级工作流中台。
掌握了这些,你就不再只是一个"会用框架调 API 的码农",而是真正具备了掌控复杂流转系统底层逻辑的架构能力。