Activiti7会签审批实战:告别低效抄送,用多实例重构企业审批流

当审批单在OA系统中流转时,你是否还在为以下场景头疼?财务报销需要部门全员确认,却要手动勾选十几个抄送人;项目立项必须获得三位总监联签,但系统只能逐个串行审批;紧急请假需要任意一位经理快速审批,却因流程设计被迫等待固定审批人。这些传统审批流程中的"人工接力赛",正在吞噬企业运营效率。

1. 会签与或签:重新定义审批协作模式

会签(Approval Meeting)和或签(Or Approval)是工作流引擎中两种典型的多人协作审批模式。前者要求指定范围内一定数量的审批人达成共识,后者则只需任意一位审批人处理即可推进流程。这两种模式在Activiti7中均通过**多实例活动(Multi-Instance Activity)**实现,但配置策略和适用场景截然不同。

表:会签与或签的核心差异对比

特征 会签模式 或签模式
完成条件 需满足预设人数/比例 任意一人处理即完成
典型应用 部门预算审批、项目立项 紧急请假、故障处理
并行性 可配置并行/串行 通常并行执行
结果处理 支持投票统计 首个响应者决定流向
系统负载 较高(需跟踪多实例状态) 较低(单实例完成即结束)

在技术实现层面,这两种模式共享相同的底层机制——都通过BPMN 2.0规范的 multiInstanceLoopCharacteristics 元素实现。关键区别仅体现在 completionCondition 属性的表达式配置上:

<!-- 会签示例:超过半数同意即通过 -->
<userTask id="groupApproval" name="部门会签">
  <multiInstanceLoopCharacteristics 
    isSequential="false" 
    collection="approvers" 
    elementVariable="approver">
    <completionCondition>${nrOfCompletedInstances/nrOfInstances > 0.5}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

<!-- 或签示例:任意一人处理即完成 -->
<userTask id="urgentApproval" name="紧急审批">
  <multiInstanceLoopCharacteristics
    isSequential="false"
    collection="managers"
    elementVariable="manager">
    <completionCondition>${nrOfCompletedInstances >= 1}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

2. 会签实现四步法:从设计到部署

2.1 BPMN可视化配置

在Activiti Modeler中创建会签节点时,需要关注以下核心属性配置:

  1. 多实例类型 :选择"Parallel"实现并行会签(审批人同时收到任务),选择"Sequential"实现串行会签(按列表顺序逐个审批)
  2. 参与者集合 :通过 collection 属性指定审批人列表变量名(如 ${approvers}
  3. 元素变量 :设置临时变量名存储当前审批人(通常与任务Assignee占位符一致)
  4. 完成条件 :使用预定义变量编写UEL表达式:
    • ${nrOfCompletedInstances == nrOfInstances} (全员通过)
    • ${nrOfCompletedInstances >= 2} (至少两人同意)
    • ${nrOfCompletedInstances/nrOfInstances > 0.6} (超过60%同意)

图:会签节点在流程设计器中的典型配置界面 (注:此处应插入配置截图,展示Loop cardinality、Completion condition等字段的填写示例)

2.2 动态参与者配置

审批人列表的注入时机和方式直接影响会签的灵活性。推荐三种实践方案:

方案一:启动时静态注入

// 流程启动时传入审批人列表
Map<String, Object> variables = new HashMap<>();
variables.put("approvers", Arrays.asList("user1", "user2", "user3"));
runtimeService.startProcessInstanceByKey("meetingApproval", variables);

方案二:运行时动态查询

// 通过监听器动态设置参与者
public class DynamicApproverListener implements TaskListener {
  @Override
  public void notify(DelegateTask task) {
    String deptId = (String) task.getVariable("applyDept");
    List<String> approvers = departmentService.findApprovers(deptId);
    task.setVariable("approvers", approvers);
  }
}

方案三:混合模式(固定+动态)

// 合并固定审批人和动态查询结果
List<String> fixedApprovers = Arrays.asList("CFO", "CTO");
List<String> dynamicApprovers = projectService.getProjectStakeholders(projectId);
List<String> allApprovers = Stream.concat(fixedApprovers.stream(), 
                             dynamicApprovers.stream()).collect(Collectors.toList());
variables.put("approvers", allApprovers);

2.3 完成条件进阶策略

除基础的数量条件外,实际业务往往需要更复杂的完成逻辑:

权重投票场景

<!-- 不同审批人拥有不同票权 -->
<completionCondition>
  ${(voteResults['director'] ? 3 : 0) + 
    (voteResults['manager'] ? 2 : 0) + 
    (voteResults['staff'] ? 1 : 0) >= 5}
</completionCondition>

自定义条件类

public class CustomCompletionCondition implements JavaDelegate {
  public void execute(DelegateExecution execution) {
    boolean isRejected = (boolean) execution.getVariable("globalReject");
    int agreeCount = ((List)execution.getVariable("voteResults"))
                   .stream().filter(r -> "agree".equals(r)).count();
    execution.setVariable("meetingPassed", !isRejected && agreeCount > 1);
  }
}

2.4 会签结果聚合

多实例任务完成后,通常需要汇总各审批意见。通过 execution.getVariableLocal() 可获取实例级变量:

// 在流程后续节点中收集会签结果
List<Map<String, Object>> allComments = new ArrayList<>();
for (String approver : approvers) {
  Map<String, Object> comment = taskService.getVariableLocal(
    taskService.createTaskQuery()
      .processInstanceId(processInstanceId)
      .taskAssignee(approver)
      .singleResult().getId(), "approvalComment");
  allComments.add(comment);
}

3. SpringBoot集成实战与避坑指南

3.1 自动配置陷阱

当SpringBoot遇到Activiti7时,这些配置项最容易出问题:

# 必须关闭自动部署校验(否则修改BPMN后需重启)
spring.activiti.check-process-definitions=false

# 建议使用新事务管理器(避免会签任务卡死)
spring.activiti.async-executor-activate=true
spring.activiti.async-executor-thread-pool-size=10

3.2 变量传递黑洞

会签任务中变量作用域的特殊性常导致意外:

// 错误示范:直接设置全局变量(其他审批人看不到)
taskService.setVariable(taskId, "comment", "同意");

// 正确做法:使用setVariableLocal设置实例级变量
taskService.setVariableLocal(taskId, "privateComment", "建议补充材料");

表:Activiti变量作用域对照

方法 作用域 多实例可见性
runtimeService.setVariable 流程实例 所有节点可见
taskService.setVariable 任务 仅当前任务可见
taskService.setVariableLocal 任务实例 多实例中各任务独立

3.3 事务一致性方案

会签任务并行执行时,需要考虑事务隔离问题。推荐采用Saga模式:

@Transactional
public void completeApproval(String taskId, ApprovalVO vo) {
  // 1. 记录审批操作(本地事务保证)
  approvalRecordRepository.save(convertToEntity(vo));
  
  // 2. 发送领域事件(异步补偿)
  eventPublisher.publishEvent(new ApprovalEvent(
    taskId, vo.getComment(), vo.isAgree()));
  
  // 3. 完成任务(最后执行)
  taskService.complete(taskId, 
    Collections.singletonMap("approvalResult", vo.isAgree()));
}

3.4 性能优化策略

大规模会签场景下,这些优化手段能显著提升性能:

  1. 批量任务查询 :避免N+1查询问题
// 低效做法
for (String approver : approvers) {
  Task task = taskService.createTaskQuery()
    .taskAssignee(approver).singleResult();
}

// 高效做法
List<Task> tasks = taskService.createTaskQuery()
  .processInstanceId(processInstanceId)
  .taskAssigneeIds(approvers)
  .list();
  1. 启用历史级别优化
# 只记录必要的历史数据
spring.activiti.history-level=audit
  1. 异步日志处理
@Bean
public AsyncLogListener asyncLogListener() {
  return new AsyncLogListener(executor);
}

4. 企业级会签场景深度解析

4.1 层级审批:矩阵式会签

对于需要跨部门联动的审批场景,可采用分层会签设计:

<process id="matrixApproval">
  <startEvent id="start"/>
  <!-- 第一层:部门内会签 -->
  <userTask id="deptApproval" name="部门审批">
    <multiInstanceLoopCharacteristics 
      collection="${deptApprovers}" 
      elementVariable="approver">
      <completionCondition>${nrOfCompletedInstances >= 2}</completionCondition>
    </multiInstanceLoopCharacteristics>
  </userTask>
  <!-- 第二层:跨部门会签 -->
  <userTask id="crossDeptApproval" name="跨部门会签">
    <multiInstanceLoopCharacteristics
      collection="${crossDeptApprovers}"
      elementVariable="approver">
      <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
    </multiInstanceLoopCharacteristics>
  </userTask>
  <endEvent id="end"/>
</process>

4.2 动态路由:条件化或签

结合网关实现智能路由的或签流程:

<sequenceFlow id="toHR" sourceRef="approvalGateway" targetRef="hrApproval">
  <conditionExpression xsi:type="tFormalExpression">
    ${firstApprover.department == 'HR'}
  </conditionExpression>
</sequenceFlow>
<sequenceFlow id="toFinance" sourceRef="approvalGateway" targetRef="financeApproval">
  <conditionExpression xsi:type="tFormalExpression">
    ${firstApprover.department == 'Finance'}
  </conditionExpression>
</sequenceFlow>

4.3 混合模式:会签+或签组合

复杂审批链的典型结构示例:

开始 → [预算会签](3人至少2人同意) → [技术或签](任意架构师) → 
[CFO审批](单人) → [终审会签](全部董事) → 结束

对应的BPMN片段:

<userTask id="finalApproval" name="董事会终审">
  <multiInstanceLoopCharacteristics
    isSequential="true"
    collection="${boardMembers}"
    elementVariable="member">
    <completionCondition>
      <![CDATA[
      ${nrOfCompletedInstances == nrOfInstances || 
       nrOfRejectedInstances >= 1}
      ]]>
    </completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

4.4 异常处理:会签超时与干预

对于可能陷入僵局的会签任务,需要设计熔断机制:

@Scheduled(fixedDelay = 3600000)
public void monitorStuckApprovals() {
  List<ProcessInstance> instances = runtimeService.createProcessInstanceQuery()
    .activityId("groupApproval")
    .startedBefore(new Date(System.currentTimeMillis() - 48 * 3600000))
    .list();
  
  instances.forEach(instance -> {
    runtimeService.setVariable(instance.getId(), "forceComplete", true);
    runtimeService.trigger(instance.getId());
  });
}

在BPMN中配置相应的边界事件:

<boundaryEvent id="timeoutEvent" attachedToRef="groupApproval">
  <timerEventDefinition>
    <timeDuration>PT48H</timeDuration>
  </timerEventDefinition>
</boundaryEvent>

更多推荐