驳回不只是让流程回到一个节点,还要决定回到这个节点后由谁处理。
假设节点 A 的候选人是张三和李四,张三领取并完成后进入节点 B。如果 B 驳回到 A,新任务可能重新处于未领取状态,李四也能领取。这是否正确取决于业务:有的流程允许重新分工,有的流程要求必须回到张三。
所以应把“退回到哪里”和“退回给谁”作为两个明确规则。
固定处理人与候选人的差别
固定处理人的节点,可以通过流程变量和条件连线表达回退。候选人节点则还要保存上一轮的实际处理人。候选人列表说明谁有资格领取,assignee 才表示这次任务具体分配给谁。
如果业务要求返回原处理人,就不能仅恢复候选人列表;同时还应确认该人是否在职、是否仍有权限,以及不可用时由谁接管。
用显式分支表达驳回
对于结构明确的串行流程,可以在 BPMN 中把“通过”和“驳回”两条路径画出来。以下是连接“审核 B”和“审核 A”的局部片段,完整模型需要声明命名空间并包含其余节点:
<userTask id="reviewA" name="审核 A"
activiti:assignee="${returnAssignee}"
activiti:candidateUsers="zhangsan,lisi"/>
<userTask id="reviewB" name="审核 B"/>
<exclusiveGateway id="decision"/>
<sequenceFlow id="toDecision" sourceRef="reviewB" targetRef="decision"/>
<sequenceFlow id="rejectToA" sourceRef="decision" targetRef="reviewA">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[
${approved == false}
]]></conditionExpression>
</sequenceFlow>
进入 A 之前要初始化 returnAssignee,首次可以为 null,使任务按候选规则处理;驳回时赋为上一处理人。完整模型还要给 decision 配置“通过”路径或默认流,不能让网关只有一个不一定成立的条件。
怎样取得上一处理人
串行流程也不宜简单取历史列表倒序第二条。业务应记录目标审批轮次的任务 ID,或者通过明确的流程关系选定要返回的那次已完成任务。
以下是 Activiti 7.1.0.M6 引擎层的局部示例;假设已经验证当前用户可以驳回,且 previousTaskId 指向业务选定的上一轮任务:
org.activiti.engine.history.HistoricTaskInstance previous =
historyService.createHistoricTaskInstanceQuery()
.taskId(previousTaskId)
.singleResult();
if (previous == null || previous.getEndTime() == null
|| previous.getAssignee() == null
|| !processInstanceId.equals(previous.getProcessInstanceId())) {
throw new IllegalArgumentException("无法确定同一流程中有效的上一处理人");
}
Map<String, Object> variables = new HashMap<>();
variables.put("returnAssignee", previous.getAssignee());
variables.put("approved", false);
taskService.complete(currentTaskId, variables);
调用前还应核对当前任务属于该实例,并验证目标节点、处理人资格和数据权限。历史查询与任务完成接口见 HistoricTaskInstanceQuery 和 TaskService。
这个方法依赖模型已有回退路径,不是任意节点跳转工具。下一轮正常流转时,也要按规则重置或更新分配变量,避免沿用旧人员。
为什么不建议临时修改 outgoingFlows
一种内部实现会保存当前节点出线、替换为指向上一节点的临时连线,完成任务后再恢复原出线,并设置新任务的处理人。它试图同时解决路径回退和人员恢复,但风险不止是少一个 finally。
流程模型可能被多个实例共享。修改内存模型时,其他线程可能读到临时连线;即使最终恢复了,也不代表并发期间没有受影响。并行分支、循环、会签还会让“上一条历史任务”的假设失效。
因此,简单流程优先把回退路径和人员规则画进模型。确实需要动态状态迁移时,应依据所选引擎版本支持的能力设计独立方案,明确作用域、事务与审计,不能把临时改模型包装成通用驳回接口。