深度解析:基于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 回退功能的业务场景分析

工作流回退功能主要解决以下典型场景:

  • 审批过程中发现提交材料不全或有误
  • 流程节点审批人选择错误需要重新指派
  • 业务流程规则变更需要重新审批

与简单的拒绝操作不同,回退功能需要保持流程实例的连续性,避免以下问题:

  1. 重新发起流程导致审批历史丢失
  2. 重复审批造成的效率低下
  3. 流程数据一致性难以维护

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 回退节点查询算法

获取可回退节点是回退功能的核心逻辑,主要涉及以下技术要点:

  1. BPMN模型解析 :通过Flowable的 BpmnModel 对象获取流程定义的结构信息
  2. 图遍历算法 :采用深度优先搜索(DFS)遍历流程节点
  3. 串行性校验 :排除并行网关等可能导致流程混乱的节点

关键代码实现:

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实现了两种加签模式:

  1. 前加签 :在当前任务前插入新任务
  2. 后加签 :在当前任务后追加新任务

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 加签的并发处理

当多个加签操作同时发生时,需要注意以下问题:

  1. 使用乐观锁控制并发:
Task task = taskService.createTaskQuery()
    .taskId(taskId)
    .singleResult();
if (task == null) {
    throw new FlowableException("任务不存在或已被处理");
}
  1. 对关键操作添加适当锁机制:
@Transactional
public synchronized void handleAddSign(...) {
    // 加签处理逻辑
}

4. 前后端协同开发实战

4.1 前端页面实现

回退功能的前端实现主要涉及两个组件:

  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>
  1. 加签人员选择组件
<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 联调常见问题解决

  1. 跨域问题
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("*")
            .allowedMethods("*")
            .maxAge(3600);
    }
}
  1. 日期格式统一
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  1. 接口权限控制
@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_TASKINST
  • ACT_HI_ACTINST
  • ACT_HI_PROCINST

为确保历史数据准确,需要:

  1. 自定义历史记录处理器:
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);
    }
}
  1. 配置自定义历史管理器:
@Bean
public HistoryService historyService(ProcessEngineConfiguration config) {
    config.setHistoryManager(new CustomHistoryManager());
    return config.getHistoryService();
}

5.2 批量操作性能优化

当处理大批量任务回退时,可采用以下优化策略:

  1. 批量查询优化
List<Task> tasks = taskService.createTaskQuery()
    .processInstanceIdIn(processInstanceIds)
    .list()
    .stream()
    .filter(task -> !task.isSuspended())
    .collect(Collectors.toList());
  1. 批量更新优化
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 分布式事务处理

在微服务架构下,需要考虑跨服务的事务一致性:

  1. 使用Seata实现分布式事务:
@GlobalTransactional
public void distributedReturnTask(String taskId) {
    // 调用工作流服务
    bpmService.returnTask(taskId);
    // 调用业务服务
    businessService.updateStatus(taskId);
}
  1. 配置Seata数据源代理:
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
    return new DataSourceProxy(druidDataSource());
}

6. 测试策略与质量保障

6.1 单元测试设计

针对回退功能的单元测试要点:

  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());
}
  1. 回退操作测试
@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 集成测试方案

集成测试需要覆盖以下场景:

  1. 正常回退流程

    • 串行流程的回退
    • 包含网关的流程回退
    • 子流程的回退处理
  2. 异常情况测试

    • 回退到并行网关的非法操作
    • 已结束流程的回退尝试
    • 不存在的节点回退
  3. 并发测试

    • 多线程同时回退不同任务
    • 同一任务的并发回退冲突

6.3 性能测试指标

使用JMeter进行压力测试,重点关注:

  1. 单节点回退性能

    • 平均响应时间 < 500ms
    • 99%线 < 1s
    • 吞吐量 > 100TPS
  2. 批量回退性能

    • 100个任务批量回退时间 < 3s
    • CPU利用率 < 70%
    • 内存占用波动 < 20%
  3. 稳定性测试

    • 持续运行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 监控与告警配置

建议监控以下关键指标:

  1. Flowable引擎指标

    • 活动流程实例数
    • 任务队列长度
    • 平均任务处理时间
  2. 系统资源指标

    • JVM内存使用
    • 数据库连接池使用率
    • CPU负载
  3. 业务指标

    • 回退操作成功率
    • 平均回退处理时间
    • 加签任务占比

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 合同审批加签场景

场景特点

  • 需要法务人员临时参与
  • 可能多人会签
  • 加签意见需要特别记录

解决方案设计

  1. 前端提供加签人员选择界面
  2. 后端记录加签原因和特殊意见
  3. 加签任务标记特殊类别
  4. 最终审批汇总所有意见

关键代码:

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 跨系统集成方案

当需要与外部系统集成时,考虑以下模式:

  1. REST API集成
@FeignClient(name = "hr-service")
public interface HrServiceClient {
    @GetMapping("/employees/{userId}/approval-auth")
    Boolean checkApprovalAuthority(@PathVariable Long userId);
}
  1. 消息队列异步处理
@RabbitListener(queues = "bpm.task.return")
public void handleReturnMessage(ReturnTaskMessage message) {
    // 处理回退消息
    taskService.returnTask(message.getTaskId(), 
        message.getTargetNode(), message.getReason());
}
  1. 分布式事务方案
@GlobalTransactional
public void crossSystemReturn(String taskId) {
    // 1. 执行工作流回退
    bpmService.returnTask(taskId);
    
    // 2. 调用外部系统API
    erpService.updateWorkflowStatus(taskId, "RETURNED");
    
    // 3. 记录审计日志
    auditService.logOperation("TASK_RETURN", taskId);
}

更多推荐