工作流并行网关中的一个分支驳回,其他分支怎么办?

并行网关中的一个分支驳回后,其他分支没有唯一、自动且普遍正确的处理方式。系统必须先确定这次驳回属于哪一种业务语义:如果只是当前分支补正,其他分支可以继续办理或保留已完成结果;如果任何一个分支驳回都代表本轮审批整体失败,就要取消或收拢同一并行作用域内的全部执行令牌,再整体退回到 Fork 之前;如果其他分支结果仍有效但不允许继续变化,则应冻结未完成任务,让已完成分支停在 Join 等待重审。

最危险的做法,是只把驳回分支移动到 Fork 之前,却保留其他分支原有令牌。流程再次经过 Fork 后会重新生成全部分支,旧令牌与新令牌并存,可能造成重复待办、Join 提前通过、流程卡死或重复执行业务动作。

一句话结论:先定义“驳回当前分支”还是“驳回整个并行审批轮次”,再决定保留、冻结还是收拢其他分支;不要让并行网关替业务规则做决定。

一、核心结论与问题边界

先给答案:其他分支只有三种合理去向

同一个流程图,可能对应三种完全不同的产品规则。

业务语义 当前驳回分支 其他未完成分支 已完成并等待 Join 的分支 推荐目标
局部补正 回到本分支前序节点 继续办理 保留结果 仍在同一 Fork—Join 区间内
保留结果、暂停本轮 回到本分支前序节点 冻结或限制提交 保留结果 修正后重新汇合
本轮整体失败 取消当前任务并回退 一并取消 收拢等待令牌 Fork 之前的单一节点

这三种策略没有谁天然更高级。财务、法务分别审查同一份合同,法务要求补材料而财务结论仍有效,适合局部补正;“任意部门不同意即退回申请人”的行政审批,通常适合整体驳回;多个分支共同修改同一组业务字段时,则往往需要先冻结,再由产品规则决定是否保留结果。

在这里插入图片描述

图 1:保留、冻结和整体收拢对应三种业务承诺,不能只靠一个“驳回”按钮隐式决定。

并行网关为什么不会替你处理其他分支

BPMN 2.0 的并行网关解决的是令牌分裂与同步问题,不是审批意见裁决问题。Fork 为每条流出顺序流创建并行执行路径;Join 等待所需的流入路径到齐后再继续。它不知道“不同意”是什么意思,也不知道财务分支的结论能否在法务补正后继续有效。

因此,“A 分支驳回”至少包含两个层面的决策:

  • 流程层:哪些执行令牌要移动、取消或保留;
  • 业务层:哪些审批意见、业务字段、附件和外部副作用仍然有效。

把变量 approved=false 写入流程实例,并不会自动撤销 B、C 分支;把 A 的 Execution 移到某个活动,也不会自动推导企业想保留哪些审批结果。网关只执行模型语义,审批平台必须补上产品语义。

二、关键概念与能力差异

驳回发生时,其他分支可能处于三种状态

设计处理逻辑前,不能只查询“还有几个待办”。同一并行作用域中的其他分支,至少可能处于以下状态:

1. 仍有活动任务

例如法务分支驳回时,财务任务仍在某位审批人待办箱中。系统要明确它是否还能提交。若允许继续,后续结论可能基于旧版本材料;若不允许继续,应用层需要给任务加业务锁或统一取消本轮任务。

2. 已完成并等待 Join

财务任务可能已经完成,其执行令牌正在并行 Join 处等待法务分支。此时数据库里未必还有财务待办,但并不代表该分支“消失了”。局部驳回时应保留这个等待令牌;整体驳回时则必须把它纳入迁移集合。

3. 已产生不可逆副作用

有的分支完成后已经调用付款、发消息、写外部台账或盖章。流程引擎移动令牌不会自动补偿这些动作。若分支可能驳回,副作用应尽量放到 Join 之后,或使用可补偿、可幂等的服务任务。

这三种状态决定了其他分支应“继续、等待、取消还是补偿”。只看 ACT_RU_TASK 会漏掉已经到达 Join 的令牌,也会漏掉外部系统状态。

策略一:只驳回当前分支,其他分支继续或等待

局部驳回适用于各分支相对独立、已经完成的意见允许复用,并且驳回目标仍处于同一 Fork—Join 区间内的场景。它的运行时含义是:只移动当前分支对应的执行实例,不触碰兄弟分支。

以 Flowable 8.0.0 为例,公开的变更状态入口是 RuntimeService.createChangeActivityStateBuilder()。如果当前任务已经解析出唯一 executionId,可以只移动该 Execution:

runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(processInstanceId)
    .moveExecutionToActivityId(currentExecutionId, targetActivityId)
    .changeState();

这里的关键参数是 executionId,不是只有业务含义的 activityId。并行分支、循环重入和多实例都可能让同一个活动 ID 同时存在多个运行实例。只传活动 ID,容易误伤同名的其他实例。

局部驳回还要满足四个约束:

  1. 目标节点与当前节点处在同一个并行作用域内;
  2. 不能跨回分裂网关 Fork 之前;
  3. 其他分支已经形成的结果允许复用;
  4. 当前分支重新完成后,仍能以一个正确令牌到达对应 Join。

若 B、C 分支已经完成并停在 Join,A 分支退回 A1 后重新审批,再到达 Join,三个令牌仍可正常同步。这是一种“重跑 A、复用 B/C”的审批语义,不是重新发起整轮并行审批。

三、模型架构与运行机制

策略二:保留已完成结果,冻结未完成分支

有些企业既不想撤销已经完成的审批意见,也不希望其他审批人在材料变化期间继续提交。这时可以采用“保留 + 冻结”策略:

  • 已完成分支的历史意见和 Join 等待令牌保留;
  • 当前驳回分支回到本分支补正节点;
  • 其他仍活动的待办在业务层标记为 WAITING_FOR_CORRECTION;
  • 补正完成后,重新校验材料版本,再解除冻结。

这里的“冻结”通常不是引擎提供的单分支 Suspend。多数引擎的挂起 API 面向整个流程实例或流程定义;对一个用户任务做业务暂停,更稳妥的方式是由任务服务在查询和完成接口上校验业务锁,不允许被冻结任务提交。

推荐给每一轮并行审批保存以下字段:

字段 作用
roundNo 标识第几轮并行审批
branchKey 标识法务、财务等业务分支
materialVersion 审批依据的材料版本
decisionStatus 同意、驳回、待重审、失效
reusable 材料变化后该意见是否仍可复用
engineTaskId / executionId 关联引擎运行实例

补正导致关键字段变化时,即使执行令牌仍可保留,原审批意见也可能不再有效。系统应按字段敏感性或规则表把相关分支标记为“待重审”,而不是把技术上的可保留误当成业务上的可复用。

策略三:整体驳回到 Fork 之前,必须先收拢所有令牌

如果业务规则是“任何一个分支驳回,本轮审批全部作废”,就不能只移动当前分支。正确模型是先定位同一并行作用域内的全部相关执行实例,包括活动任务、已经在 Join 等待的执行以及必要的子作用域,再把它们合并到 Fork 之前的一个目标节点。

Flowable 8.0.0 的 ChangeActivityStateBuilder 明确提供了 moveExecutionsToSingleActivityId(List<String>, String)。官方 Javadoc 说明它用于并行/包容网关、多实例和事件子流程等并行执行场景:

List<String> scopeExecutionIds = parallelScopeResolver
    .resolveExecutions(processInstanceId, forkId, joinId);

runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(processInstanceId)
    .moveExecutionsToSingleActivityId(scopeExecutionIds, targetBeforeFork)
    .changeState();

真正困难的是 resolveExecutions。它不能简单理解为“查询该流程实例的所有 Execution”,否则可能把事件子流程、嵌套子流程或不相关并行域一起收掉。生产实现至少要校验:

  • Execution 是否属于目标 Fork—Join 的同一流程作用域;
  • 是否存在嵌套并行网关或多实例根 Execution;
  • 当前活动是否已经越过 Join;
  • 是否还有边界事件、事件订阅、作业和子流程实例;
  • 目标节点是否确实位于 Fork 之前并能重新生成一轮干净的分支。

整体收拢会结束其他分支的当前运行实例。系统还需要给历史任务写清楚删除原因,例如“并行轮次整体驳回”,并将对应业务审批意见标记为失效,而不是物理删除审计记录。

在这里插入图片描述

图 2:整体驳回不是循环调用多个单节点跳转,而是先计算完整执行集合,再由一次状态变更完成收拢。

四、核心场景与处理策略

最危险的错误:只把当前分支退到 Fork 之前

假设 Fork 后产生 A、B、C 三条分支。B、C 已经完成并在 Join 等待,A 驳回。若系统只把 A 的 Execution 移到 Fork 之前,流程重新经过 Fork 时会再次创建 A2、B2、C2;与此同时,旧的 B1、C1 仍存在。

最终可能出现:

  • B、C 各出现两套执行令牌和重复待办;
  • Join 收到超过模型预期的令牌;
  • 旧令牌与新令牌混合,提前触发或永远无法满足同步条件;
  • 下游服务任务被重复执行;
  • 历史轨迹看起来像同一审批人无故审批两次。

在这里插入图片描述

图 3:跨过 Fork 的回退必须处理整个并行域,否则旧 B/C 与新 B/C 会同时存在。

因此可以把安全边界写成一条硬规则:

目标仍在当前分支内,可以移动单个 Execution;目标跨到 Fork 之前,必须先收拢当前并行作用域的全部令牌。

Flowable 8.0.0 的实现边界

截至 2026 年 8 月 2 日,Flowable 官方 GitHub 最新正式版为 8.0.0。其 ChangeActivityStateBuilder 为不同令牌数量提供了不同方法,选错 API 本质上就是选错运行时拓扑。

迁移拓扑 推荐 API 并行驳回中的典型用途
一个 Execution 到一个节点 moveExecutionToActivityId 只退当前分支
多个 Execution 合并到一个节点 moveExecutionsToSingleActivityId 整体退到 Fork 前
一个 Execution 拆到多个节点 moveSingleExecutionToActivityIds 受控重建并行路径
一个活动 ID 到另一个活动 ID moveActivityIdTo 仅在活动实例唯一且已严格校验时使用
多个活动 ID 合并到一个节点 moveActivityIdsToSingleActivityId 活动级批量收拢,仍需防同名多实例

changeState() 最终交给动态状态变更逻辑处理 Execution 的销毁、合并与目标活动启动。它解决的是引擎运行时一致性,不会自动处理业务表、外部 API 补偿、审批意见有效性和消息幂等。

Flowable 8.0.0 发布说明中还包含“修复把并行多实例活动移动到单一活动的问题”。这提醒我们:并行网关与多实例叠加时,不能把 API 存在等同于所有拓扑都天然安全。升级引擎后仍应回归本企业的嵌套网关、会签、事件子流程和连续驳回用例。

五、数据、规则与状态设计

Camunda 7 与 Camunda 8 如何对应理解

Camunda 7 可以通过 Process Instance Modification 组合“启动目标活动”和“取消源活动实例”。单分支补正应取消精确的 activityInstanceId;整体驳回则要遍历 Activity Instance Tree,覆盖同一并行作用域中的活动实例和必要的 Transition Instance。仅使用 cancelAllForActivity(activityId) 可能误伤循环或多实例中的同名活动。

Camunda 8 的 Process Instance Modification 使用元素实例键精确定位,并在当前版本提供 Move Instruction。官方文档特别提醒,修改可以制造模型正常执行无法到达的状态;错误地在并行 Join 之前再次激活活动,可能让实例卡在网关。因此,Flowable 的“多 Execution 合一”方案不能机械翻译成 Camunda 7/8 的一组 API 调用,必须按各自的实例树、作用域和修改语义重新实现。

引擎 精确标识 单分支处理 整体处理重点
Flowable 8 executionId 移动单个 Execution 收集并合并并行域 Execution
Camunda 7 activityInstanceId 取消源实例并启动目标 遍历 Activity Instance Tree,成组取消
Camunda 8 elementInstanceKey Move 精确元素实例 计算元素实例与祖先作用域,防止非法状态

六、工程实现与系统集成

历史轨迹和业务状态应该怎样记录

一次并行驳回至少要形成三类可审计记录。

第一类是操作日志:谁在什么时间,对哪个分支执行了何种驳回策略,目标节点是什么,理由是什么。第二类是引擎历史:原任务正常结束、被取消还是被状态变更删除,新的任务属于哪个审批轮次。第三类是业务审批意见:原意见继续有效、暂时冻结还是因整轮驳回而失效。

不要“恢复”旧的已完成 Task。重审应创建新的任务实例,并通过 roundNo、previousOpinionId 或 supersededBy 关联旧记录。这样才能回答:第二次审批依据的是哪一版材料,第一次意见为什么失效,谁触发了整轮回退。

并行分支写共享流程变量也要谨慎。A、B 同时写 approved 或 comment,最后提交者会覆盖前者。更稳妥的方式是使用分支局部变量或带分支键的结构,例如 decision.legal、decision.finance,在 Join 后由聚合服务统一生成总状态。

七、安全、性能与治理要求

低代码平台怎样把它做成可配置能力

企业级低代码平台不应只提供一个“驳回”开关,而应在流程设计阶段显式配置并行驳回策略:驳回范围、其他活动任务如何处理、已完成意见能否复用、材料变化触发哪些分支重审、跨 Fork 时如何收拢、外部动作怎样补偿。
在这里插入图片描述

云程低代码开发平台可以把这些规则沉淀为节点和网关级元数据,并在发布流程前做静态校验:如果目标跨越 Fork,却选择“仅移动当前执行”,设计器直接阻止发布;如果选择保留其他分支,却存在共享可写变量或不可逆服务任务,则给出风险提示。这样能把一次性的项目代码变成可测试、可审计的产品能力。

在这里插入图片描述

图 4:先判断目标范围、结果复用和副作用,再决定其他分支去向。

八、案例实践与迁移路线

上线前至少测试这些场景

  1. A 驳回时 B、C 都未完成,验证它们是继续、冻结还是取消;
  2. B 已完成并停在 Join,A 局部驳回后重审,验证 Join 最终只通过一次;
  3. A 连续驳回两次,验证每轮只创建一个新任务,历史轮次清晰;
  4. A 整体驳回到 Fork 前,验证旧 B/C 待办与等待令牌全部消失;
  5. 并行网关内嵌多实例,验证 Execution 集合没有遗漏或扩大;
  6. 存在边界定时器、消息订阅和异步作业,验证状态变更后无孤儿资源;
  7. 两个管理员并发执行驳回,验证乐观锁、幂等键和重复提交保护;
  8. 驳回事务成功而业务补偿失败,验证 Outbox、重试与人工修复通道;
  9. 流程引擎升级后复跑上述用例,重点关注并行多实例合并;
  10. 检查待办、已办、流程轨迹和业务单据对同一轮次的展示一致性。

测试断言不能只有“接口返回成功”。还要断言活动任务数、Execution 树、Join 前令牌数、历史删除原因、审批意见有效性、外部副作用次数和最终业务状态。

九、平台落地、测试与选型

标准答案

并行网关中的一个分支驳回,其他分支怎么办?

标准答案是:先由业务定义驳回范围,再按运行时令牌拓扑执行。若只是当前分支补正,移动当前分支的精确 Execution,其他分支继续或在 Join 等待;若已完成结果可以复用但活动任务不能继续,则保留已完成分支并在业务层冻结未完成任务;若任何分支驳回代表本轮整体失败,就必须收集同一 Fork—Join 作用域内的全部执行实例,取消活动任务、收拢等待令牌,再合并到 Fork 之前的单一目标节点。

绝不能只把一个分支退回 Fork 之前而保留其他旧令牌。这样会在重新经过 Fork 时生成重复分支,导致重复待办、错误汇合或流程卡死。并行网关负责同步令牌,不负责判断审批意见是否有效;审批平台还必须同步处理轮次、历史意见、共享变量、业务状态和外部副作用。

posted @ 2026-09-16 15:07  大龄码农有梦想  阅读(24)  评论(0)    收藏  举报