会签开始后,又要增加一个审批人,这是比“启动前确定会签名单”更复杂的需求。最直觉的做法是读取人员列表、追加一人,再写回流程变量。但变量写入成功,并不代表引擎已经生成新任务。

人员列表和运行中的执行不是同一个东西

例如,会签节点进入时的人员集合是张三、李四,引擎据此创建相应子执行和用户任务。随后把王五追加到集合,只改变了业务数据;王五的任务不会因为列表变化就自然出现。

在 Activiti 7.1.0.M6 的并行多实例实现中,可以看到进入活动时解析实例数量、初始化计数并创建子执行的过程。理解这个时机,就能解释为什么只更新集合不够。

内部 Command 方案实际要做什么

直接扩展引擎时,处理的已经不是“给列表加一个值”,而是修改执行树。核心流程可以表示为下面的伪代码:

根据 taskId 找到仍在运行的任务和执行
定位本轮多实例的根执行
检查节点是并行多实例,且本轮尚未结束
校验新审批人及请求幂等性
为该审批人创建新的子执行和局部变量
更新本轮总实例数和活跃实例数
执行用户任务行为,生成任务和相应历史
在同一事务中提交

历史项目采用 ManagementService.executeCommand(...) 执行自定义 Command,通过内部实体管理器创建子执行,并调用多实例内部的用户任务行为。这些是内部扩展点,不是一个稳定、跨版本的业务 API。

其中涉及的计数关系是:

// 仅说明新增一个活跃实例的计数变化,不是一套完整的加签实现。
parentExecution.setVariableLocal("nrOfInstances", oldTotal + 1);
parentExecution.setVariableLocal("nrOfActiveInstances", oldActive + 1);

这两行无法单独保证正确:总数、任务、历史和执行树必须一致,已有完成条件也要重新评估。只修改计数器,会得到“显示有三人、实际只有两条任务”之类的不一致。

共享流程模型不能拿来保存某个人的状态

一种危险写法是从当前执行取得 UserTask,直接调用 setAssignee(newUserId),再把这个模型对象交给新执行。问题在于,这修改的是流程定义模型,而不是仅给当前实例保存一个变量。模型可能被缓存并由多个实例共用。

人员身份更适合通过多实例的 elementVariable 和任务分配表达式表达。例如下面是启动一轮并行会签的 BPMN 片段:

<userTask id="review" name="会签" activiti:assignee="${reviewer}">
  <multiInstanceLoopCharacteristics isSequential="false"
      activiti:collection="reviewers"
      activiti:elementVariable="reviewer"/>
</userTask>

reviewers 是本轮人员集合,reviewer 是每个子实例使用的人员变量。这个配置解决的是进入节点时的人员分配,不是“修改集合后自动增加运行中任务”的开关。多实例集合和完成条件的概念见官方用户指南。

更容易维护的分轮加签

如果业务允许先完成当前一轮,再让新增人员审批,可以在 BPMN 中显式建模:

  1. 当前会签轮次完成后,进入“是否需要补充审批”判断。
  2. 有新增名单时,设置下一轮 reviewers,进入同一个会签节点或专门的补充审批节点。
  3. 将批次编号、人员和决定记录到业务审计中。
  4. 没有新增名单时,继续后续流程。

这种方案与“当前轮次立即增加并行任务”语义不同,但避免了在活动运行中手工改执行树。应由业务明确能否接受分轮处理,不能把两者混称为同一种加签。

必须同时考虑的几种情况

两个人同时请求加签、同一请求重试、加签与最后一个审批同时发生,以及事务回滚,都会考验一致性。请求需要业务幂等键,重复人员如何处理也要明确。

若本轮已经满足完成条件或正在结束,不能再凭一个过期 taskId 强行加任务。顺序多实例与并行多实例的执行方式不同,也不能共用这套假设。

动态加签的关键不是找到一个能调用的内部方法,而是确保业务名单、执行树、任务、计数和历史属于同一轮且一致变化。只有先确定这些约束,才能决定使用所选版本支持的能力、显式流程建模,还是维护专门的内部扩展。