SpringBoot 整合Activiti(三)——动态任务分配与审批历史追踪

张开发
2026/5/10 8:01:22 15 分钟阅读

分享文章

SpringBoot 整合Activiti(三)——动态任务分配与审批历史追踪
1. 动态任务分配的实现原理在流程审批系统中动态任务分配是个非常实用的功能。想象一下你正在处理一个请假审批流程不同部门的员工请假可能需要不同层级的主管来审批。如果每次都要手动修改流程图来调整审批人那简直是个噩梦。Activiti的动态任务分配机制就是为了解决这个问题而生的。Activiti提供了两种主要的动态分配方式通过表达式动态指定办理人可以在流程定义中直接使用${expression}来调用服务方法在运行时动态设置办理人通过TaskService在代码中灵活设置我比较推荐第一种方式因为它把业务规则和流程定义紧密结合维护起来更方便。下面这段代码展示了如何在流程定义文件中使用表达式userTask idapprovalTask name审批 activiti:candidateUsers${approvalService.findApprovers(execution)}/这里的approvalService.findApprovers(execution)会调用Spring容器中的approvalService服务传入当前执行上下文返回一个审批人列表。2. 实现动态分配的服务层代码要实现动态分配我们需要编写一个服务方法来决定任务的办理人。这个方法的返回值可以是单个用户ID也可以是用户ID列表。下面是一个完整的服务类示例Service public class ApprovalService { Autowired private UserRepository userRepository; public ListString findApprovers(DelegateExecution execution) { // 获取业务数据 String businessKey execution.getProcessBusinessKey(); LeaveApplication application leaveApplicationRepository.findById(businessKey); // 根据业务规则确定审批人 if(application.getLeaveDays() 3) { // 3天以内由部门主管审批 return Arrays.asList(application.getDepartment().getManagerId()); } else { // 超过3天需要部门主管和HR审批 return Arrays.asList( application.getDepartment().getManagerId(), hr_admin // HR专员ID ); } } }在实际项目中我遇到过几个常见的坑需要特别注意服务方法命名要一致流程定义中调用的方法名必须和服务类中的方法名完全一致包括大小写返回值类型要正确如果是多候选人的任务返回List单个办理人可以直接返回String注意事务边界服务方法中如果涉及数据库操作要确保事务配置正确3. 审批历史记录的查询实现流程走完后我们通常需要查看审批历史。Activiti提供了HistoryService来查询这些数据。历史数据主要存储在以下几个表中ACT_HI_PROCINST流程实例历史ACT_HI_ACTINST活动实例历史ACT_HI_TASKINST任务实例历史ACT_HI_DETAIL流程明细历史下面这段代码展示了如何查询一个流程实例的完整审批历史public ListApprovalHistory getApprovalHistory(String processInstanceId) { // 查询历史任务实例 ListHistoricTaskInstance tasks historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .orderByTaskCreateTime().asc() .list(); // 查询历史活动实例 ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); // 组合数据 ListApprovalHistory history new ArrayList(); for (HistoricTaskInstance task : tasks) { ApprovalHistory item new ApprovalHistory(); item.setTaskName(task.getName()); item.setAssignee(task.getAssignee()); item.setStartTime(task.getStartTime()); item.setEndTime(task.getEndTime()); item.setDuration(task.getDurationInMillis()); history.add(item); } return history; }在实际项目中我发现历史数据的查询性能是个需要注意的问题。当流程实例很多时直接查询可能会很慢。我通常采取以下优化措施添加适当的索引特别是processInstanceId和start_time字段分页查询对于可能返回大量结果的查询一定要实现分页缓存常用数据对于高频访问的历史数据可以考虑使用缓存4. 动态分配与历史查询的整合应用现在我们把动态分配和历史查询结合起来实现一个完整的审批流程。假设我们有一个请假审批的场景员工提交请假申请启动流程根据请假天数动态分配审批人审批人处理任务查询完整的审批历史首先定义流程process idleaveApproval name请假审批流程 startEvent idstartEvent/ userTask idapplyTask name提交申请 activiti:assignee${initiator}/ sequenceFlow sourceRefstartEvent targetRefapplyTask/ userTask idapprovalTask name审批 activiti:candidateUsers${approvalService.findApprovers(execution)}/ sequenceFlow sourceRefapplyTask targetRefapprovalTask/ exclusiveGateway iddecisionGateway/ sequenceFlow sourceRefapprovalTask targetRefdecisionGateway/ sequenceFlow sourceRefdecisionGateway targetRefendEvent conditionExpression xsi:typetFormalExpression ${approved} /conditionExpression /sequenceFlow sequenceFlow sourceRefdecisionGateway targetRefapplyTask conditionExpression xsi:typetFormalExpression ${!approved} /conditionExpression /sequenceFlow endEvent idendEvent/ /process然后实现流程控制代码RestController RequestMapping(/leave) public class LeaveController { Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; Autowired private HistoryService historyService; PostMapping(/start) public String startProcess(RequestBody LeaveRequest request) { // 设置流程变量 MapString, Object variables new HashMap(); variables.put(initiator, request.getApplicantId()); variables.put(leaveDays, request.getLeaveDays()); // 启动流程 ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveApproval, request.getId(), // 业务ID variables ); return instance.getId(); } GetMapping(/history/{processInstanceId}) public ListApprovalHistory getHistory(PathVariable String processInstanceId) { // 调用前面实现的历史查询方法 return approvalService.getApprovalHistory(processInstanceId); } }在这个实现中我特别加入了几个实用功能业务键关联使用业务ID作为流程实例的业务键方便后续查询流程变量传递在启动时传递必要的业务数据条件路由根据审批结果决定流程走向5. 高级功能与性能优化对于更复杂的场景我们可能需要一些高级功能。比如有时我们需要在运行时动态调整任务办理人或者在查询历史时获取更多详细信息。运行时调整办理人public void reassignTask(String taskId, String newAssignee) { // 先认领任务 taskService.claim(taskId, currentUserId); // 再重新分配 taskService.setAssignee(taskId, newAssignee); // 添加评论记录 taskService.addComment(taskId, null, 任务重新分配给: newAssignee); }带附件的历史查询public ListHistoricDetail getTaskDetails(String taskId) { return historyService.createHistoricDetailQuery() .taskId(taskId) .includeProcessVariables() .includeTaskLocalVariables() .list(); }性能优化方面我有几个实际项目中的经验分享批量操作当需要处理大量历史数据时使用批量查询和分页延迟加载对于不立即需要的数据采用懒加载策略历史级别配置根据实际需要配置适当的历史级别不是所有场景都需要FULL级别// 批量查询示例 public ListHistoricProcessInstance getCompletedProcesses(int page, int size) { return historyService.createHistoricProcessInstanceQuery() .finished() .orderByProcessInstanceEndTime().desc() .listPage(page * size, size); }6. 常见问题排查与调试技巧在实际开发中动态任务分配和历史查询可能会遇到各种问题。下面分享几个我遇到的典型问题及解决方法。问题1动态分配的服务方法没有被调用症状任务创建了但分配人没有按预期设置排查步骤检查流程定义中的表达式是否正确特别是$和#的区别确认服务类是否被Spring管理方法名是否完全匹配检查Activiti日志看是否有异常抛出问题2历史数据缺失症状流程走完了但查询不到历史记录可能原因历史级别设置太低流程实例被意外删除查询条件不正确解决方案是在流程引擎配置中设置合适的历史级别Bean public ProcessEngineConfigurationImpl processEngineConfiguration(DataSource dataSource) { SpringProcessEngineConfiguration config new SpringProcessEngineConfiguration(); config.setDataSource(dataSource); config.setDatabaseSchemaUpdate(true); config.setHistory(full); // 设置历史级别为full return config; }调试技巧启用SQL日志查看实际执行的SQL语句logging.level.org.activiti.engine.impl.persistence.entityDEBUG使用Activiti Explorer这是一个官方提供的管理界面可以直观查看流程状态和历史数据单元测试为关键功能编写单元测试特别是动态分配逻辑Test public void testDynamicAssignment() { // 启动流程 ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveApproval, variables ); // 验证任务分配 Task task taskService.createTaskQuery() .processInstanceId(instance.getId()) .singleResult(); assertNotNull(task); assertEquals(审批, task.getName()); // 验证分配人 ListIdentityLink links taskService.getIdentityLinksForTask(task.getId()); assertTrue(links.stream().anyMatch( link - link.getUserId().equals(expectedApprover) )); }7. 安全注意事项与最佳实践在实现动态分配和历史查询时安全性不容忽视。以下是几个关键的安全考虑权限控制确保用户只能查询自己有权限查看的历史记录对动态分配的服务方法进行权限校验public ListString findApprovers(DelegateExecution execution) { // 验证当前用户是否有权限执行此操作 String currentUser SecurityContextHolder.getContext() .getAuthentication().getName(); if(!permissionService.canAssignApprovers(currentUser)) { throw new AccessDeniedException(无权分配审批人); } // 原有逻辑... }数据脱敏在返回历史数据时对敏感信息进行脱敏处理审计日志记录重要的分配操作和历史查询Aspect Component public class AuditLogAspect { AfterReturning( pointcutexecution(* com..ApprovalService.findApprovers(..)), returningapprovers ) public void logAssignment(JoinPoint jp, ListString approvers) { DelegateExecution execution (DelegateExecution) jp.getArgs()[0]; auditLogService.log( 动态分配审批人: approvers 流程实例: execution.getProcessInstanceId() ); } }最佳实践建议保持业务逻辑与流程分离虽然可以在表达式中写复杂逻辑但最好保持服务方法简洁合理设计历史级别根据业务需求选择适当的历史级别平衡功能需求和性能定期归档历史数据对于完成时间较久的流程实例考虑归档到单独的存储监控关键指标如任务分配耗时、历史查询响应时间等// 监控示例 Around(execution(* com..HistoryService.*(..))) public Object monitorHistoryQuery(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); try { return pjp.proceed(); } finally { long duration System.currentTimeMillis() - start; metricsService.recordHistoryQuery( pjp.getSignature().getName(), duration); } }

更多文章