保姆级教程:手把手教你为Flowable工作流添加回退与加签(基于RuoYi-Vue-Pro源码)
深度解析:基于RuoYi-Vue-Pro的Flowable工作流回退与加签实战
1. 环境准备与项目初始化
在开始实现Flowable工作流的回退与加签功能前,确保开发环境配置正确至关重要。RuoYi-Vue-Pro作为一个成熟的前后端分离框架,已经内置了Flowable的基础集成,这为我们节省了大量配置时间。
基础环境要求 :
- JDK 1.8+
- Maven 3.6+
- MySQL 5.7+
- Redis 5.0+
- Node.js 12+(前端开发需要)
首先从GitHub克隆最新版RuoYi-Vue-Pro项目:
git clone https://github.com/YunaiV/ruoyi-vue-pro.git
cd ruoyi-vue-pro
后端项目采用标准的Maven多模块结构,工作流相关代码主要集中在:
ruoyi-module-bpm
├── src/main/java
│ ├── cn.iocoder.yudao.module.bpm.controller.admin
│ ├── cn.iocoder.yudao.module.bpm.dal
│ └── cn.iocoder.yudao.module.bpm.service
前端项目结构对应工作流模块位于:
src/views/bpm
├── process
└── task
启动项目前需要初始化数据库,执行项目中的 sql/ruoyi-vue-pro.sql 脚本。特别注意Flowable的23张核心表已经包含在初始化脚本中,无需额外创建。
2. 回退功能的核心设计与实现
2.1 回退功能的业务场景分析
工作流回退功能主要解决以下典型场景:
- 审批过程中发现提交材料不全或有误
- 流程节点审批人选择错误需要重新指派
- 业务流程规则变更需要重新审批
与简单的拒绝操作不同,回退功能需要保持流程实例的连续性,避免以下问题:
- 重新发起流程导致审批历史丢失
- 重复审批造成的效率低下
- 流程数据一致性难以维护
2.2 数据库表结构扩展
RuoYi-Vue-Pro在原生Flowable表基础上扩展了几个关键表:
CREATE TABLE `bpm_task_ext` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`task_id` varchar(64) NOT NULL COMMENT '任务ID',
`result` tinyint NOT NULL COMMENT '审批结果',
`reason` varchar(255) NOT NULL DEFAULT '' COMMENT '审批意见',
`end_time` datetime NOT NULL COMMENT '结束时间',
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`)
) ENGINE=InnoDB COMMENT='任务扩展表';
这个扩展表主要用于记录任务的处理结果和回退原因,与原生 ACT_RU_TASK 表形成互补。
2.3 回退节点查询算法
获取可回退节点是回退功能的核心逻辑,主要涉及以下技术要点:
- BPMN模型解析 :通过Flowable的
BpmnModel对象获取流程定义的结构信息 - 图遍历算法 :采用深度优先搜索(DFS)遍历流程节点
- 串行性校验 :排除并行网关等可能导致流程混乱的节点
关键代码实现:
public List<UserTask> getPreviousUserTaskList(FlowElement source,
Set<String> visitedFlows, List<UserTask> result) {
// 初始化参数
result = result == null ? new ArrayList<>() : result;
visitedFlows = visitedFlows == null ? new HashSet<>() : visitedFlows;
// 处理子流程场景
if (source instanceof StartEvent && source.getSubProcess() != null) {
return getPreviousUserTaskList(source.getSubProcess(),
visitedFlows, result);
}
// 获取当前节点的所有入口连线
List<SequenceFlow> incomingFlows = getElementIncomingFlows(source);
if (CollectionUtils.isEmpty(incomingFlows)) {
return result;
}
// 遍历所有前置节点
for (SequenceFlow flow : incomingFlows) {
if (visitedFlows.contains(flow.getId())) continue;
visitedFlows.add(flow.getId());
FlowElement sourceElement = flow.getSourceFlowElement();
if (sourceElement instanceof UserTask) {
result.add((UserTask) sourceElement);
} else if (sourceElement instanceof SubProcess) {
// 处理子流程内部节点
processSubFlow((SubProcess) sourceElement, visitedFlows, result);
}
// 递归处理
getPreviousUserTaskList(sourceElement, visitedFlows, result);
}
return result;
}
2.4 回退操作的事务处理
回退操作涉及多个数据库更新,必须保证事务一致性:
@Override
@Transactional(rollbackFor = Exception.class)
public void returnTask(BpmTaskReturnReqVO reqVO) {
// 1. 校验任务状态
Task task = validateTaskExist(reqVO.getTaskId());
// 2. 执行回退逻辑
returnTask0(task, reqVO);
// 3. 更新扩展表记录
updateTaskExt(task, reqVO);
}
关键注意事项 :
- 使用Spring的
@Transactional确保原子性 - 避免在事务中执行耗时操作
- 合理设置事务隔离级别(默认READ_COMMITTED)
3. 加签功能的设计与实现
3.1 加签的业务场景
加签功能主要适用于以下情况:
- 当前审批人需要征求其他人员意见
- 业务需要临时增加审批环节
- 特定情况下需要多人会签
RuoYi-Vue-Pro实现了两种加签模式:
- 前加签 :在当前任务前插入新任务
- 后加签 :在当前任务后追加新任务
3.2 加签的数据库设计
加签操作主要涉及以下表:
ACT_RU_TASK:新增加签任务记录ACT_RU_IDENTITYLINK:存储任务与处理人的关系bpm_task_ext:记录加签相关元数据
3.3 加签的核心实现
加签功能的关键在于正确处理任务顺序和流程变量:
public void addSignTask(String taskId, AddSignType type,
List<Long> userIds, String reason) {
// 1. 校验并获取基础数据
Task currentTask = validateTask(taskId);
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceId(currentTask.getProcessInstanceId())
.singleResult();
// 2. 创建加签任务
List<Task> newTasks = new ArrayList<>();
for (Long userId : userIds) {
Task task = taskService.newTask();
task.setName(currentTask.getName() + "[加签]");
task.setAssignee(String.valueOf(userId));
task.setCategory("ADD_SIGN");
taskService.saveTask(task);
// 设置任务变量
taskService.setVariableLocal(task.getId(),
"addSignParentTaskId", currentTask.getId());
newTasks.add(task);
}
// 3. 处理任务关系
if (type == AddSignType.BEFORE) {
// 前加签逻辑
handleBeforeAddSign(currentTask, newTasks);
} else {
// 后加签逻辑
handleAfterAddSign(currentTask, newTasks);
}
// 4. 记录操作日志
recordAddSignLog(currentTask, newTasks, reason);
}
3.4 加签的并发处理
当多个加签操作同时发生时,需要注意以下问题:
- 使用乐观锁控制并发:
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
if (task == null) {
throw new FlowableException("任务不存在或已被处理");
}
- 对关键操作添加适当锁机制:
@Transactional
public synchronized void handleAddSign(...) {
// 加签处理逻辑
}
4. 前后端协同开发实战
4.1 前端页面实现
回退功能的前端实现主要涉及两个组件:
- 回退节点选择对话框 :
<el-dialog title="任务回退" :visible.sync="showReturnDialog">
<el-form :model="returnForm">
<el-form-item label="回退节点">
<el-select v-model="returnForm.targetNodeKey">
<el-option
v-for="node in returnNodes"
:key="node.key"
:label="node.name"
:value="node.key">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="回退原因">
<el-input type="textarea" v-model="returnForm.reason"/>
</el-form-item>
</el-form>
</el-dialog>
- 加签人员选择组件 :
<user-select
v-model="addSignUserIds"
:multiple="true"
:dept-id="currentDeptId"/>
4.2 后端API设计
RuoYi-Vue-Pro遵循RESTful风格设计工作流API:
| 端点 | 方法 | 描述 |
|---|---|---|
/bpm/task/{taskId}/return |
POST | 执行任务回退 |
/bpm/task/{taskId}/add-sign |
POST | 执行任务加签 |
/bpm/task/{taskId}/returnable-nodes |
GET | 获取可回退节点列表 |
回退接口的请求示例:
{
"taskId": "12345",
"targetDefinitionKey": "userTask1",
"reason": "材料不完整,请补充"
}
4.3 联调常见问题解决
- 跨域问题 :
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.maxAge(3600);
}
}
- 日期格式统一 :
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
- 接口权限控制 :
@PreAuthorize("@ss.hasPermission('bpm:task:return')")
@PostMapping("/task/{taskId}/return")
public CommonResult<Boolean> returnTask(
@PathVariable String taskId,
@Valid @RequestBody BpmTaskReturnReqVO reqVO) {
// 业务逻辑
}
5. 高级功能与性能优化
5.1 历史数据一致性保障
回退操作会影响以下历史表:
ACT_HI_TASKINSTACT_HI_ACTINSTACT_HI_PROCINST
为确保历史数据准确,需要:
- 自定义历史记录处理器:
public class CustomHistoryManager extends DefaultHistoryManager {
@Override
public void recordTaskEnd(TaskEntity task, String deleteReason) {
// 特殊处理回退任务的结束原因
if (deleteReason != null && deleteReason.startsWith("return:")) {
deleteReason = "任务回退:" + deleteReason.substring(7);
}
super.recordTaskEnd(task, deleteReason);
}
}
- 配置自定义历史管理器:
@Bean
public HistoryService historyService(ProcessEngineConfiguration config) {
config.setHistoryManager(new CustomHistoryManager());
return config.getHistoryService();
}
5.2 批量操作性能优化
当处理大批量任务回退时,可采用以下优化策略:
- 批量查询优化 :
List<Task> tasks = taskService.createTaskQuery()
.processInstanceIdIn(processInstanceIds)
.list()
.stream()
.filter(task -> !task.isSuspended())
.collect(Collectors.toList());
- 批量更新优化 :
jdbcTemplate.batchUpdate(
"UPDATE bpm_task_ext SET result = ? WHERE task_id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) {
ps.setInt(1, results.get(i));
ps.setString(2, taskIds.get(i));
}
public int getBatchSize() {
return taskIds.size();
}
});
5.3 分布式事务处理
在微服务架构下,需要考虑跨服务的事务一致性:
- 使用Seata实现分布式事务:
@GlobalTransactional
public void distributedReturnTask(String taskId) {
// 调用工作流服务
bpmService.returnTask(taskId);
// 调用业务服务
businessService.updateStatus(taskId);
}
- 配置Seata数据源代理:
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DataSourceProxy(druidDataSource());
}
6. 测试策略与质量保障
6.1 单元测试设计
针对回退功能的单元测试要点:
- 可回退节点查询测试 :
@Test
public void testGetReturnableNodes() {
// 准备测试流程
deployProcess("sequential-process.bpmn");
// 启动流程实例
ProcessInstance instance = runtimeService.startProcessInstanceByKey("sequentialProcess");
// 查询当前任务
Task currentTask = taskService.createTaskQuery()
.processInstanceId(instance.getId())
.singleResult();
// 测试可回退节点
List<BpmTaskSimpleRespVO> returnableNodes = taskService.getReturnTaskList(currentTask.getId());
assertEquals(1, returnableNodes.size());
assertEquals("startTask", returnableNodes.get(0).getDefinitionKey());
}
- 回退操作测试 :
@Test
@Transactional
public void testReturnTask() {
// 准备测试数据
String taskId = createTestTask();
// 执行回退
BpmTaskReturnReqVO reqVO = new BpmTaskReturnReqVO();
reqVO.setTaskId(taskId);
reqVO.setTargetDefinitionKey("startTask");
reqVO.setReason("测试回退");
taskService.returnTask(reqVO);
// 验证结果
Task currentTask = taskService.createTaskQuery().taskId(taskId).singleResult();
assertNull("原任务应已结束", currentTask);
Task newTask = taskService.createTaskQuery()
.processInstanceId(instance.getId())
.singleResult();
assertEquals("startTask", newTask.getTaskDefinitionKey());
}
6.2 集成测试方案
集成测试需要覆盖以下场景:
-
正常回退流程 :
- 串行流程的回退
- 包含网关的流程回退
- 子流程的回退处理
-
异常情况测试 :
- 回退到并行网关的非法操作
- 已结束流程的回退尝试
- 不存在的节点回退
-
并发测试 :
- 多线程同时回退不同任务
- 同一任务的并发回退冲突
6.3 性能测试指标
使用JMeter进行压力测试,重点关注:
-
单节点回退性能 :
- 平均响应时间 < 500ms
- 99%线 < 1s
- 吞吐量 > 100TPS
-
批量回退性能 :
- 100个任务批量回退时间 < 3s
- CPU利用率 < 70%
- 内存占用波动 < 20%
-
稳定性测试 :
- 持续运行8小时无内存泄漏
- 错误率 < 0.1%
- 数据库连接池无耗尽
7. 生产环境部署建议
7.1 服务器配置推荐
根据实际业务量考虑以下配置:
| 业务规模 | CPU | 内存 | 磁盘 | 推荐部署方式 |
|---|---|---|---|---|
| 小型(100TPS以下) | 4核 | 8GB | 100GB SSD | 单体应用 |
| 中型(100-500TPS) | 8核 | 16GB | 200GB SSD | 集群(2节点) |
| 大型(500TPS以上) | 16核+ | 32GB+ | 500GB+ SSD | 微服务架构 |
7.2 数据库优化参数
针对Flowable的MySQL优化建议:
[mysqld]
# 连接池配置
max_connections = 500
thread_cache_size = 50
# InnoDB优化
innodb_buffer_pool_size = 4G
innodb_log_file_size = 512M
innodb_flush_log_at_trx_commit = 2
innodb_file_per_table = ON
# 查询缓存
query_cache_type = 0
query_cache_size = 0
7.3 监控与告警配置
建议监控以下关键指标:
-
Flowable引擎指标 :
- 活动流程实例数
- 任务队列长度
- 平均任务处理时间
-
系统资源指标 :
- JVM内存使用
- 数据库连接池使用率
- CPU负载
-
业务指标 :
- 回退操作成功率
- 平均回退处理时间
- 加签任务占比
Prometheus监控示例配置:
scrape_configs:
- job_name: 'flowable'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
8. 典型业务场景解决方案
8.1 采购审批流程回退
场景特点 :
- 多级审批(部门经理→财务→总经理)
- 可能需要补充材料
- 金额阈值影响审批路径
解决方案 :
public void handlePurchaseReturn(String taskId, String reason) {
// 1. 获取当前任务和流程实例
Task task = validateTask(taskId);
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.singleResult();
// 2. 根据业务规则确定回退目标
String targetKey = determineReturnTarget(task, instance);
// 3. 执行回退
returnTask(task, targetKey, reason);
// 4. 发送通知
sendReturnNotification(task, targetKey, reason);
}
8.2 合同审批加签场景
场景特点 :
- 需要法务人员临时参与
- 可能多人会签
- 加签意见需要特别记录
解决方案设计 :
- 前端提供加签人员选择界面
- 后端记录加签原因和特殊意见
- 加签任务标记特殊类别
- 最终审批汇总所有意见
关键代码:
public void addContractSigners(String taskId, List<Long> userIds,
String reason, String legalOpinion) {
// 1. 基础校验
Task task = validateTask(taskId);
// 2. 创建加签任务
for (Long userId : userIds) {
Task signTask = taskService.newTask();
signTask.setName(task.getName() + "[法务加签]");
signTask.setAssignee(userId.toString());
signTask.setCategory("LEGAL_REVIEW");
taskService.saveTask(signTask);
// 设置法律意见变量
taskService.setVariableLocal(signTask.getId(),
"legalOpinion", legalOpinion);
}
// 3. 更新主任务状态
taskService.setVariable(taskId, "waitingLegalReview", true);
taskService.saveTask(task);
}
8.3 跨系统集成方案
当需要与外部系统集成时,考虑以下模式:
- REST API集成 :
@FeignClient(name = "hr-service")
public interface HrServiceClient {
@GetMapping("/employees/{userId}/approval-auth")
Boolean checkApprovalAuthority(@PathVariable Long userId);
}
- 消息队列异步处理 :
@RabbitListener(queues = "bpm.task.return")
public void handleReturnMessage(ReturnTaskMessage message) {
// 处理回退消息
taskService.returnTask(message.getTaskId(),
message.getTargetNode(), message.getReason());
}
- 分布式事务方案 :
@GlobalTransactional
public void crossSystemReturn(String taskId) {
// 1. 执行工作流回退
bpmService.returnTask(taskId);
// 2. 调用外部系统API
erpService.updateWorkflowStatus(taskId, "RETURNED");
// 3. 记录审计日志
auditService.logOperation("TASK_RETURN", taskId);
}
更多推荐


所有评论(0)