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和6,7版本最大的改进是彻底拥抱了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> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency> <!-- 如果需要使用Activiti自带的REST API(通常我们不用,用自己写的),可添加 --> <!-- <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter-rest-api</artifactId> <version>7.1.0.M6</version> </dependency> -->接下来是核心的application.yml配置。Activiti的配置项不少,但必须关注的几个如下:
# application.yml spring: datasource: # Ruoyi业务数据源,Activiti也会用这个数据源创建自己的表 url: jdbc:mysql://localhost:3306/ry_vue?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%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 List<org.activiti.engine.identity.User> findUsersByGroupId(String groupId) { // 根据角色ID(groupId)查找属于该角色的所有用户 List<SysUser> sysUsers = userService.selectUserListByRoleId(Long.valueOf(groupId)); List<org.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 List<org.activiti.engine.identity.Group> findGroupsByUser(String userId) { // 根据用户ID查询其拥有的所有角色 List<SysRole> sysRoles = roleService.selectRolesByUserId(Long.valueOf(userId)); List<org.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. 流程发起Service
@Service 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) { // 设置流程变量 Map<String, 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) { // 完成任务,可以附带一些表单数据 Map<String, 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查询 List<Task> 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(); List<Map<String, Object>> result = new ArrayList<>(); for (Task task : taskList) { Map<String, 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. 设置流程变量,驱动网关判断 Map<String, 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)。对于复杂的业务对象,只存储其ID(businessKey),在需要时通过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负责把“水电管线”(业务流程)布置得灵活又清晰,两者各司其职,才能真正打造出健壮、易维护的企业级应用。