驳回不只是让流程回到一个节点,还要决定回到这个节点后由谁处理。

假设节点 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。

流程模型可能被多个实例共享。修改内存模型时,其他线程可能读到临时连线;即使最终恢复了,也不代表并发期间没有受影响。并行分支、循环、会签还会让“上一条历史任务”的假设失效。

因此,简单流程优先把回退路径和人员规则画进模型。确实需要动态状态迁移时,应依据所选引擎版本支持的能力设计独立方案,明确作用域、事务与审计,不能把临时改模型包装成通用驳回接口。