Ruoyi与Activiti深度集成:企业级审批流程解耦实战

发布时间:2026/7/31 14:32:46
Ruoyi与Activiti深度集成:企业级审批流程解耦实战 1. 项目概述与核心价值最近在后台和社群里看到不少朋友在讨论企业级应用开发时如何优雅地处理那些复杂的审批流程。比如一个请假申请从员工提交到组长审批再到部门经理、HR甚至财务一套流程走下来如果全靠硬编码去写状态流转和权限判断代码很快就会变成一团乱麻后期维护和流程变更更是噩梦。我自己在主导一个内部OA系统重构时就遇到了这个痛点最终选择了将Ruoyi框架与Activiti工作流引擎进行深度集成。这不仅仅是两个技术的简单叠加而是一套让业务逻辑与流程控制彻底解耦的工程实践。Ruoyi作为一个基于Spring Boot的快速开发平台提供了用户、角色、菜单、权限等后台管理的基础脚手架开箱即用极大地提升了CRUD业务的开发效率。而Activiti则是业界公认的轻量级、嵌入式的BPMN 2.0工作流引擎它用标准化的流程图来定义流程引擎负责驱动流程实例的流转、任务分配和状态管理。把它们俩结合起来目标很明确利用Ruoyi快速搭建应用骨架和界面同时借助Activiti的强大流程驱动能力来处理所有需要多步骤、多角色协同的业务场景如公文流转、采购审批、项目立项等。这么做的价值远不止于“能用”。首先它实现了关注点分离。业务开发人员只需关心每个审批节点要处理什么业务数据比如审核请假天数是否合规而流程的跳转、任务的创建与完成、候选人的指派全部交给Activiti引擎自动处理。其次流程可视化与可维护性极大提升。所有的流程逻辑都变成了直观的BPMN流程图业务人员也能看懂。当审批流程需要调整时比如增加一个会签环节往往只需要修改流程图并重新部署无需改动或仅需微调后端Java代码真正做到了敏捷响应业务变化。最后历史数据追溯变得异常清晰。Activiti会完整记录每个流程实例的每一步操作、审批人、审批意见和时间为审计和复盘提供了完整的数据链。2. 集成方案设计与核心思路拆解2.1 技术选型与版本考量在动手之前明确技术栈的版本是避免后续兼容性坑的第一步。我选择的组合是Ruoyi 4.7.6 (Spring Boot 2.5.x) Activiti 7.1.0.M6。这里有几个关键考量Spring Boot版本对齐Ruoyi 4.7.6内置的是Spring Boot 2.5.x。Activiti 7.x系列正是为Spring Boot 2.x量身定制的其自动配置和starter依赖与这个版本完美契合。如果强行使用Activiti 6.x与Spring Boot 1.x更配或最新的Activiti 8.x实验性较强会遇到大量的依赖冲突和配置问题。Activiti 7的优势相比Activiti 5和67版本最大的改进是彻底拥抱了Spring Boot提供了activiti-spring-boot-starter简化了集成配置。同时它默认使用Spring Security进行身份集成这与Ruoyi自带的安全框架可以结合并且其REST API设计也更现代化。选择M6里程碑版本是因为它在功能和稳定性上经过了较多社区测试且与Spring Boot 2.5.x兼容性最好。数据库支持Activiti需要独立的数据库表来存储流程定义、实例、任务等数据。它支持多种数据库考虑到Ruoyi常用MySQL我们自然选择MySQL。需要注意的是Activiti对MySQL的版本有一定要求建议使用5.7及以上版本以避免某些DDL语句的兼容性问题。2.2 整体架构与数据流设计集成后的核心架构可以理解为“三层驱动”模型流程定义层设计时使用Activiti Modeler一个Web流程设计器或本地安装的BPMN 2.0设计工具如Eclipse插件、Camunda Modeler绘制流程图.bpmn20.xml文件。这一层定义了业务的“游戏规则”。流程引擎层运行时集成在Spring Boot应用中的Activiti引擎。它负责解析部署流程定义、创建和管理流程实例、分配用户任务、执行网关判断等。这一层是系统的“心脏”。业务应用层交互时即我们的Ruoyi应用。它提供用户界面展示待办任务、发起流程、处理任务填写审批意见、查看流程图进度等。这一层通过调用Activiti的Java API与服务层与引擎交互是用户直接操作的“控制面板”。数据流上最关键的是身份关联。Ruoyi有自己的sys_user用户表和sys_role角色表而Activiti有自己的ACT_ID_USER和ACT_ID_GROUP。我们绝不能维护两套用户体系。标准做法是屏蔽Activiti自带的身份表将Ruoyi的用户体系适配到Activiti的IdentityService接口上。也就是说当引擎需要查询某个任务的候选人时我们告诉它去查Ruoyi的sys_user表当需要判断用户所属组角色时去查Ruoyi的sys_role表。这个适配过程通过实现Activiti的UserEntityManager和GroupEntityManager接口来完成。注意很多初学者会直接向Activiti的身份表里同步数据这是不推荐的做法。它增加了数据冗余和一致性维护的复杂度。适配器模式才是更优雅的解决方案。3. 核心细节解析与实操要点3.1 依赖引入与关键配置首先在Ruoyi项目的父pom.xml中需要统一管理Spring Boot版本。然后在ruoyi-admin模块的pom.xml中添加Activiti starter依赖。!-- 在 ruoyi-admin/pom.xml 中添加 -- dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependency !-- 如果需要使用Activiti自带的REST API通常我们不用用自己写的可添加 -- !-- dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter-rest-api/artifactId version7.1.0.M6/version /dependency --接下来是核心的application.yml配置。Activiti的配置项不少但必须关注的几个如下# application.yml spring: datasource: # Ruoyi业务数据源Activiti也会用这个数据源创建自己的表 url: jdbc:mysql://localhost:3306/ry_vue?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneGMT%2B8 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver activiti: # 1. 数据库策略 false-不自动创建和更新表 true-项目启动时检查表不存在则创建存在则更新生产环境慎用 drop-create-启动时删除旧表再创建 database-schema-update: true # 开发环境设为true生产环境务必改为false或validate # 2. 是否检查流程定义文件BPMN XML的格式开发时可关闭以加快启动 check-process-definitions: false # 3. 历史数据记录级别 none-不保存 activity-保存流程实例和活动实例 audit默认-保存所有细节包括变量 full-最高级别保存所有包括临时变量。审计需求高选audit。 history-level: audit # 4. 关闭Activiti自带的用户/组表初始化因为我们用Ruoyi的体系 db-history-used: true # 自定义身份管理配置指向我们自己的实现类 custom-mybatis-mappers: - com.ruoyi.activiti.adapter.CustomUserEntityManager - com.ruoyi.activiti.adapter.CustomGroupEntityManager # 5. 关闭Spring Security集成因为Ruoyi有自己的Shiro/Spring Security配置避免冲突 # 如果Ruoyi用的是Spring Security可以尝试集成但Shiro环境下必须关闭 use-spring-security-for-identity: false3.2 身份体系适配实现这是集成中最关键、也最容易出错的一环。我们需要创建两个类实现Activiti的接口并注入Spring容器。1. 自定义用户管理器 (CustomUserEntityManager)package com.ruoyi.activiti.adapter; import org.activiti.engine.impl.persistence.entity.UserEntity; import org.activiti.engine.impl.persistence.entity.UserEntityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.ruoyi.system.domain.SysUser; import com.ruoyi.system.service.ISysUserService; import java.util.ArrayList; import java.util.List; /** * 将Activiti的用户查询映射到Ruoyi的SysUser */ Component public class CustomUserEntityManager extends UserEntityManager { Autowired private ISysUserService userService; Override public UserEntity findUserById(String userId) { SysUser sysUser userService.selectUserById(Long.valueOf(userId)); if (sysUser ! null) { UserEntity userEntity new UserEntity(); userEntity.setId(sysUser.getUserId().toString()); userEntity.setFirstName(sysUser.getUserName()); // activiti的firstName存用户名 // lastName, email等可按需映射 return userEntity; } return null; } Override public Listorg.activiti.engine.identity.User findUsersByGroupId(String groupId) { // 根据角色IDgroupId查找属于该角色的所有用户 ListSysUser sysUsers userService.selectUserListByRoleId(Long.valueOf(groupId)); Listorg.activiti.engine.identity.User userList new ArrayList(); for (SysUser sysUser : sysUsers) { UserEntity userEntity new UserEntity(); userEntity.setId(sysUser.getUserId().toString()); userEntity.setFirstName(sysUser.getUserName()); userList.add(userEntity); } return userList; } // 还可以根据需要重写其他方法如根据用户名查询等 }2. 自定义组角色管理器 (CustomGroupEntityManager)package com.ruoyi.activiti.adapter; import org.activiti.engine.impl.persistence.entity.GroupEntity; import org.activiti.engine.impl.persistence.entity.GroupEntityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.ruoyi.system.domain.SysRole; import com.ruoyi.system.service.ISysRoleService; import java.util.ArrayList; import java.util.List; /** * 将Activiti的组查询映射到Ruoyi的SysRole */ Component public class CustomGroupEntityManager extends GroupEntityManager { Autowired private ISysRoleService roleService; Override public GroupEntity findGroupById(String groupId) { SysRole sysRole roleService.selectRoleById(Long.valueOf(groupId)); if (sysRole ! null) { GroupEntity groupEntity new GroupEntity(); groupEntity.setId(sysRole.getRoleId().toString()); groupEntity.setName(sysRole.getRoleName()); groupEntity.setType(assignment); // Activiti组类型通常用assignment return groupEntity; } return null; } Override public Listorg.activiti.engine.identity.Group findGroupsByUser(String userId) { // 根据用户ID查询其拥有的所有角色 ListSysRole sysRoles roleService.selectRolesByUserId(Long.valueOf(userId)); Listorg.activiti.engine.identity.Group groupList new ArrayList(); for (SysRole sysRole : sysRoles) { GroupEntity groupEntity new GroupEntity(); groupEntity.setId(sysRole.getRoleId().toString()); groupEntity.setName(sysRole.getRoleName()); groupEntity.setType(assignment); groupList.add(groupEntity); } return groupList; } }实操心得这里有一个大坑。Activiti默认会使用其自带的DbIdentityServiceImpl进行身份管理它会尝试向ACT_ID_*表里插入数据。即使我们写了适配器如果配置不当引擎在创建任务时可能还是会调用默认实现。确保我们的适配器生效必须在配置类中显式地禁用默认的身份服务并设置我们自己的SessionFactory。这需要创建一个Configuration类来覆盖Activiti的默认配置。3.3 流程定义部署策略流程定义文件.bpmn20.xml如何部署到引擎中常见有三种方式类路径部署将bpmn文件放在src/main/resources/processes/目录下。Activiti starter默认会扫描该目录并自动部署。这种方式最简单适合流程稳定、随应用发布上线的场景。API动态部署通过RepositoryService的createDeployment()API在程序运行时上传bpmn文件或输入流进行部署。这给了我们很大的灵活性可以实现流程版本管理、热部署等功能。数据库存储部署将bpmn文件的XML内容存储在业务数据库的某个表里部署时读取内容并通过API部署。对于大多数Ruoyi项目我推荐方式1和方式2结合。基础流程随应用启动部署方式1。同时在Ruoyi后台开发一个“流程管理”模块提供上传bpmn文件、部署新版本、激活/挂起流程定义的功能方式2。这既保证了基础能力又为运维留下了操作空间。部署时的一个关键点是流程定义KEY和版本。每个流程定义有一个唯一的key在bpmn文件的process标签的id属性。每次部署相同key的流程Activiti会自动为其生成一个递增的版本号。启动流程实例时默认启动最新版本。这个机制天然支持了流程的版本化管理。4. 实操过程与核心环节实现4.1 设计第一个请假流程BPMN图我们以一个最简单的“员工请假审批”流程为例使用Camunda Modeler它与Activiti 7兼容性好来设计。流程节点包括Start Event开始事件流程起点。User Task用户任务- “提交申请”申请人填写请假单。User Task - “部门经理审批”经理审核。Exclusive Gateway排他网关判断审批结果。Sequence Flow顺序流连接各个节点条件流上可设置表达式如${approval yes}。End Event结束事件流程结束。关键设置在于用户任务的“Assignee”或“Candidate Groups”。我们不建议写死具体用户ID如zhangsan而应该使用表达式或监听器动态指定。Assignee受理人可以直接设置为${applyUserId}流程变量表示谁发起流程谁处理第一个任务。但对于审批人更常用监听器来设置。Candidate Groups候选组这里可以填入角色ID如deptManager需要与我们CustomGroupEntityManager中返回的角色ID对应。这样所有具有deptManager角色的用户都能看到这个待办任务。将设计好的图保存为leave_process.bpmn20.xml并放入resources/processes/目录。4.2 在Ruoyi中实现流程发起与审批接口现在我们在Ruoyi中创建对应的Controller和Service。1. 流程发起ServiceService public class ProcessService { Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; Autowired private RepositoryService repositoryService; /** * 发起一个请假流程 * param leaveDto 请假单DTO包含请假类型、天数、原因等 * param userId 发起人ID * return 流程实例ID */ public String startLeaveProcess(LeaveDto leaveDto, Long userId) { // 设置流程变量 MapString, Object variables new HashMap(); variables.put(applyUserId, userId.toString()); variables.put(leaveType, leaveDto.getLeaveType()); variables.put(days, leaveDto.getDays()); variables.put(reason, leaveDto.getReason()); variables.put(startDate, leaveDto.getStartDate()); variables.put(endDate, leaveDto.getEndDate()); // 可以设置下一步审批人的角色这里假设部门经理的角色ID是100 variables.put(deptManagerRoleId, 100); // 使用流程定义的KEY来启动最新版本的流程 ProcessInstance instance runtimeService.startProcessInstanceByKey(leave_process, variables); // 流程启动后第一个任务提交申请通常会自动完成或者由发起人完成。 // 这里我们假设需要发起人提交一下找到他的任务并完成 Task task taskService.createTaskQuery() .processInstanceId(instance.getId()) .taskAssignee(userId.toString()) // 第一个任务的受理人是发起人自己 .singleResult(); if (task ! null) { // 完成任务可以附带一些表单数据 MapString, Object taskVariables new HashMap(); taskVariables.put(formData, leaveDto.toString()); // 存储表单快照 taskService.complete(task.getId(), taskVariables); } return instance.getId(); } }2. 查询待办任务列表这是OA系统的核心功能需要展示给当前用户所有待他审批的任务。public PageInfo queryTodoTasks(Long userId, int pageNum, int pageSize) { // 使用TaskService查询 ListTask taskList taskService.createTaskQuery() // 方式1查询受理人是自己的任务 .taskAssignee(userId.toString()) // 方式2查询候选组包含自己所属角色的任务更常用 // .taskCandidateGroupIn(getUserRoleIds(userId)) // 需要先获取用户角色ID列表 // 方式3或者查询候选人是自己的任务 // .taskCandidateUser(userId.toString()) .orderByTaskCreateTime().desc() .listPage((pageNum - 1) * pageSize, pageSize); long total taskService.createTaskQuery() .taskAssignee(userId.toString()) .count(); ListMapString, Object result new ArrayList(); for (Task task : taskList) { MapString, Object map new HashMap(); map.put(taskId, task.getId()); map.put(taskName, task.getName()); map.put(createTime, task.getCreateTime()); // 通过流程实例ID可以查询业务数据。这里需要根据instanceId去关联你的业务表。 String businessKey runtimeService.createProcessInstanceQuery() .processInstanceId(task.getProcessInstanceId()) .singleResult() .getBusinessKey(); // BusinessKey通常存储业务数据ID需要在启动流程时设置 // ... 根据businessKey查询具体的请假单信息填充到map中 ... result.add(map); } // 使用Ruoyi的分页工具返回 return new PageInfo(result, total); }3. 审批任务接口Transactional(rollbackFor Exception.class) public void completeTask(String taskId, String approvalResult, String comment, Long userId) { // 1. 添加审批意见 taskService.addComment(taskId, task.getProcessInstanceId(), comment); // 2. 设置流程变量驱动网关判断 MapString, Object variables new HashMap(); variables.put(approval, approvalResult); // yes or no variables.put(approver, userId.toString()); variables.put(approveTime, new Date()); // 3. 完成任务流程引擎会自动推动到下一个节点 taskService.complete(taskId, variables); // 4. 可选根据审批结果更新业务数据状态 String businessKey runtimeService.createProcessInstanceQuery() .processInstanceId(task.getProcessInstanceId()) .singleResult() .getBusinessKey(); if (yes.equals(approvalResult)) { // 更新请假单状态为“部门经理通过” // leaveService.updateStatus(businessKey, APPROVED); } else { // 更新为“驳回” // leaveService.updateStatus(businessKey, REJECTED); } }4.3 前端页面集成与流程跟踪Ruoyi-Vue前端需要新增几个页面发起流程页一个表单填写请假信息提交后调用startLeaveProcess接口。我的待办页以表格形式展示queryTodoTasks返回的数据每条数据后有“办理”按钮。任务办理页点击“办理”进入展示请假单详情并有“同意”、“驳回”按钮和意见输入框调用completeTask接口。流程跟踪页这是提升用户体验的关键。通过ProcessInstanceId调用HistoryService和RuntimeService可以获取流程当前到达的节点以及已经流经的历史节点。结合RepositoryService获取的BPMN XML可以使用前端的BPMN渲染库如bpmn-js高亮显示当前流程进度图一目了然。// 前端调用审批接口示例 import { completeTask } from /api/activiti/task; export default { methods: { handleApprove(taskId, result) { const data { taskId: taskId, approvalResult: result, // yes or no comment: this.form.comment }; completeTask(data).then(response { this.$modal.msgSuccess(审批完成); this.close(); // 关闭弹窗 this.$tab.refreshPage(); // 刷新待办列表页 }); } } }5. 常见问题与排查技巧实录在实际集成和开发过程中我踩过不少坑。这里把典型问题和解决方案整理出来希望能帮你节省大量排查时间。5.1 身份认证与候选人查询失败问题现象流程启动了任务也创建了但在“我的待办”里查不到任务。或者在任务查询时使用taskCandidateGroupIn方法无效。排查思路检查适配器是否生效首先确认CustomUserEntityManager和CustomGroupEntityManager是否被Spring容器成功加载。可以在类中加个日志看findGroupsByUser等方法是否被调用。检查角色ID映射Activiti查询候选组时传入的groupId是什么它来自于你BPMN图中设置的Candidate Groups。确保这个值如deptManager与你CustomGroupEntityManager中findGroupById方法能处理的ID格式如100匹配。这里常常是字符串与数字的转换问题。一个稳妥的做法是在流程变量中传递数字型的角色ID在BPMN表达式中使用${deptManagerRoleId}。禁用默认身份服务这是最容易被忽略的一点。必须在配置类中注入自定义的SessionFactory。Configuration public class ActivitiConfig { Autowired private CustomUserEntityManager customUserEntityManager; Autowired private CustomGroupEntityManager customGroupEntityManager; Bean Primary public ProcessEngineConfiguration processEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { // 设置自定义的身份管理器 processEngineConfiguration.setUserEntityManager(customUserEntityManager); processEngineConfiguration.setGroupEntityManager(customGroupEntityManager); // 关键禁用默认的DbIdentityServiceImpl防止它干扰 processEngineConfiguration.setDbIdentityUsed(false); return processEngineConfiguration; } }5.2 流程变量读取为null或类型转换异常问题现象在ServiceTask的JavaDelegate实现类中或者在后置任务监听器中通过execution.getVariable(“variableName”)取到的值是null或者明明存的是Integer却报ClassCastException。解决方案作用域问题流程变量有作用域分为流程实例级别全局和任务级别局部。通过runtimeService.setVariable()设置的是全局变量。通过taskService.setVariableLocal()设置的是局部变量只在该任务生命周期内有效。获取时要注意匹配。通常建议使用全局变量。序列化问题Activiti默认使用JVM序列化来存储复杂的对象变量。如果你的自定义对象没有实现Serializable接口或者类结构发生变化如增删字段在读取时就会出错。最佳实践是流程变量只存储基本类型String, Integer, Long, Date和可序列化的简单对象如Map, List。对于复杂的业务对象只存储其IDbusinessKey在需要时通过ID从业务数据库查询。类型明确获取使用带类型的方法避免隐式转换。例如execution.getVariable(“days”, Integer.class)或(Integer) execution.getVariable(“days”)。5.3 事务管理与数据一致性问题场景在审批任务的方法completeTask中我们既调用了Activiti的taskService.complete()来推动流程又需要更新业务表的状态。如果更新业务表失败流程却已经推进了就会导致数据不一致。解决方案利用Spring全局事务确保你的Service方法上加了Transactional注解并且Activiti的DataSource与Ruoyi业务用的是同一个。这样Activiti的数据库操作和你的业务更新会在同一个数据库事务中成功一起成功失败一起回滚。检查数据源配置这是前提。必须确保spring.datasource只有一个主数据源配置Activiti和Ruoyi都使用它。如果为Activiti配置了独立数据源事务管理会变得复杂。业务键BusinessKey的妙用在启动流程时强烈建议使用runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey, variables)方法。这个businessKey通常就是你业务表的主键ID如请假单ID。之后在任何需要关联业务数据的地方都可以通过ProcessInstance的getBusinessKey()轻松获取保证了流程与业务数据的强关联。5.4 高并发下的任务抢占与锁问题现象在会签场景多个人并行审批同一任务下可能出现两个人同时点击“办理”系统报错或产生重复处理。底层原理Activiti的用户任务本身不是锁。当任务候选人是多个用户时任何一个用户都可以“认领”claim这个任务认领后任务Assignee变为该用户其他人就看不到了。但“认领”和“完成”不是原子操作。规避方案前端防重复提交按钮点击后立即禁用直到接口返回。后端乐观锁在业务层面为任务处理逻辑加锁。例如在办理前先查询任务当前状态是否已被他人认领或者使用Redis分布式锁以taskId为key确保同一时间只有一个请求能进入办理逻辑。使用Activiti的“认领”机制设计前端交互为两步先“签收”调用taskService.claim(taskId, userId)签收成功后再进入办理页面。这样就从逻辑上避免了抢占。5.5 历史数据膨胀与性能优化问题现象系统运行一段时间后ACT_HI_*历史表数据量巨大导致查询流程历史、生成报表变慢。优化策略调整历史级别生产环境可以根据审计要求将activiti.history-level从audit降为activity甚至none如果不需要历史记录。但注意降低级别会影响一些功能如查看流程图历史节点高亮。定时归档与清理Activiti提供了HistoryService.createHistoricProcessInstanceQuery().finished()等API来查询已结束的流程。可以编写一个定时任务定期将超过一定时间如6个月的已完成流程实例的历史数据归档到其他历史报表库或冷存储中然后从Activiti表中删除。删除时务必谨慎先备份再操作。建立数据库索引检查慢查询日志在经常用于查询的字段上建立索引如ACT_HI_PROCINST表的BUSINESS_KEY_,START_TIME_ACT_HI_TASKINST表的ASSIGNEE_,PROC_INST_ID_等。集成初期建议把日志级别调到DEBUG仔细观察Activiti执行SQL的过程能帮你快速定位大部分问题。这套组合拳打下来Ruoyi负责把“房子”系统盖得又快又漂亮Activiti负责把“水电管线”业务流程布置得灵活又清晰两者各司其职才能真正打造出健壮、易维护的企业级应用。