创新的做网站,网站建设详细教程,为什么做腾讯网站,如何自已建网站Flowable 6.6.0 实战#xff1a;构建企业级工作流任意节点驳回引擎 在复杂的企业业务流程中#xff0c;审批流很少能一帆风顺地走完。一个采购申请#xff0c;可能在财务审核环节被要求退回给部门经理补充信息#xff1b;一个项目立项流程#xff0c;可能在最终审批前被发…Flowable 6.6.0 实战构建企业级工作流任意节点驳回引擎在复杂的企业业务流程中审批流很少能一帆风顺地走完。一个采购申请可能在财务审核环节被要求退回给部门经理补充信息一个项目立项流程可能在最终审批前被发回给技术负责人重新评估风险。这种“驳回”或“退回”操作是工作流引擎必须具备的核心能力也是衡量一个流程平台是否灵活、是否贴合实际业务的关键指标。然而实现一个健壮、通用的驳回功能远比想象中复杂。它不仅仅是让流程“倒车”那么简单。你需要考虑当前节点能退回到哪些历史节点如果流程中存在并行审批会签是退回到整个并行网关之前还是只退回到其中一个分支子流程内部的驳回如何影响主流程这些场景交织在一起构成了一个充满挑战的技术问题。本文将聚焦于 Flowable 6.6.0深入探讨如何设计并实现一个能够优雅处理上述所有复杂场景的任意节点驳回引擎。我们将从核心概念入手逐步拆解技术难点并提供一套可直接集成、高度可复用的完整解决方案。无论你是正在为现有 Flowable 项目添加驳回功能还是正在评估工作流引擎的选型这篇文章都将为你提供清晰的路径和扎实的代码基础。1. 理解驳回不仅仅是“回退一步”在开始编码之前我们必须先厘清“驳回”在流程引擎中的本质。很多开发者最初的理解是简单的“回到上一个任务”但在包含网关、多实例、子流程的 BPMN 2.0 标准流程中这种理解是片面且危险的。驳回的核心是改变流程实例的运行时状态。Flowable 引擎通过Execution执行流对象来推进流程。当流程经过一个并行网关时一个执行流会分裂成多个当这些分支在汇聚网关处合并时多个执行流又会汇合成一个。驳回操作实质上是要将当前一个或多个Execution的活跃节点Current Activity重新定位到历史中的某个节点上并期望引擎能从这个历史节点开始正确地重新驱动流程。这引出了两个关键挑战状态回溯的合法性并非所有历史节点都适合作为驳回目标。例如你不能将一个处于并行网关某一分支上的任务直接驳回到另一个未执行过的分支节点上这会导致流程状态混乱。上下文的重建驳回后目标节点被重新激活。这时该节点关联的表单数据、变量、任务处理人等信息是否需要、以及如何恢复是需要仔细设计的业务逻辑。因此我们的解决方案将分为两大步首先智能计算可驳回节点列表其次安全执行状态迁移。本文重点解决第一个也是最复杂的部分。2. 核心数据结构与查询策略要计算可驳回节点我们需要从 Flowable 的运行时和历史表中提取信息。主要涉及以下核心实体ACT_RU_ACTINST(运行时活动实例表)记录当前正在运行或刚刚结束的活动实例。END_TIME_为 NULL 表示活动正在进行中。ACT_RU_EXECUTION(运行时执行流表)代表流程的执行路径是流程推进的载体。ACT_HI_ACTINST(历史活动实例表)记录所有已完成的活动实例是我们查找历史驳回目标的主要数据源。ACT_HI_TASKINST(历史任务实例表)记录所有已完成的任务实例用于关联任务处理人等信息。我们的目标是给定一个当前任务 ID找出流程实例历史上所有“理论上”可以安全退回到的用户任务节点。2.1 定义可驳回节点的规则什么样的历史节点是“可驳回”的我们制定如下规则已完成节点只有已经执行完毕END_TIME_不为空的节点才可能被退回。在相同或上游执行流路径上驳回目标节点必须位于当前执行流或其父级执行流的路径上。不能跳转到并行网关产生的、从未涉足过的其他分支。支持串行与并行场景串行路线简单顺序流中的历史用户任务都是可驳回候选。并行网关后在并行网关的多个分支中只能驳回到本分支历史上经过的节点或驳回到并行网关本身这将取消所有其他分支。并行网关前在并行网关之前的节点可以驳回到更早的串行节点。排除当前节点显然不能驳回到自己。基于这些规则我们不能简单地查询所有历史用户任务。我们需要分析流程的拓扑结构结合运行时执行流信息进行判断。2.2 构建节点信息传输对象我们需要一个对象来承载返回给前端的可驳回节点信息。除了基本的节点ID和名称审批人和完成时间对于用户选择至关重要。import lombok.Data; import java.util.Date; /** * 可驳回节点信息传输对象 */ Data public class RejectableNodeDTO { /** * 节点定义ID (对应BPMN XML中的activityId) */ private String activityId; /** * 节点名称 */ private String activityName; /** * 节点类型 (如 userTask, parallelGateway) */ private String activityType; /** * 该节点上次完成的时间 */ private Date endTime; /** * 上次处理该节点任务的人员姓名多个会签人员会拼接 */ private String assigneeNames; /** * 附加信息用于前端展示或逻辑判断 * 例如对于并行网关可标识其分支信息 */ private String extraInfo; }3. 实现可驳回节点查询服务这是整个引擎最核心的部分。我们将实现一个RejectableNodeService其核心方法findRejectableNodes负责执行复杂的查询与逻辑判断。3.1 方法骨架与基础查询首先我们注入必要的 Flowable 服务并定义主方法。Service Slf4j public class RejectableNodeService { Autowired private RuntimeService runtimeService; Autowired private HistoryService historyService; Autowired private TaskService taskService; Autowired private RepositoryService repositoryService; /** * 获取当前任务可驳回到的节点列表 * param currentTaskId 当前待办任务ID * return 按结束时间倒序排列的可驳回节点列表最近的在前 */ public ListRejectableNodeDTO findRejectableNodes(String currentTaskId) { // 1. 基础信息获取 Task currentTask taskService.createTaskQuery().taskId(currentTaskId).singleResult(); if (currentTask null) { throw new RuntimeException(任务不存在或已完成: currentTaskId); } String processInstanceId currentTask.getProcessInstanceId(); String currentActivityId currentTask.getTaskDefinitionKey(); // 2. 获取流程定义模型用于分析拓扑 BpmnModel bpmnModel repositoryService.getBpmnModel(currentTask.getProcessDefinitionId()); FlowElement currentFlowElement bpmnModel.getFlowElement(currentActivityId); // 3. 查询所有历史活动实例用户任务和并行网关 ListHistoricActivityInstance historicInstances fetchRelevantHistory(processInstanceId, currentActivityId); // 4. 核心逻辑基于流程模型和历史数据筛选出可驳回节点 ListRejectableNodeDTO candidateNodes analyzeAndFilterNodes( processInstanceId, currentActivityId, currentFlowElement, historicInstances, bpmnModel ); // 5. 为节点填充审批人信息 enrichWithAssigneeInfo(candidateNodes, processInstanceId); // 6. 排序最近完成的节点排在前面方便用户选择 candidateNodes.sort(Comparator.comparing(RejectableNodeDTO::getEndTime).reversed()); return candidateNodes; } }3.2 获取相关历史数据我们不仅需要用户任务的历史还需要并行网关的历史因为网关是处理分支逻辑的关键。/** * 获取与驳回逻辑相关的历史活动实例 */ private ListHistoricActivityInstance fetchRelevantHistory(String processInstanceId, String currentActivityId) { // 查询所有已完成的用户任务 ListHistoricActivityInstance userTaskHistory historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .activityType(userTask) .finished() .orderByHistoricActivityInstanceEndTime().asc() .list(); // 查询所有已完成的并行网关 ListHistoricActivityInstance parallelGatewayHistory historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .activityType(parallelGateway) .finished() .orderByHistoricActivityInstanceEndTime().asc() .list(); // 合并列表排除当前活动本身不能驳回到自己 ListHistoricActivityInstance allHistory new ArrayList(); allHistory.addAll(userTaskHistory); allHistory.addAll(parallelGatewayHistory); return allHistory.stream() .filter(hi - !hi.getActivityId().equals(currentActivityId)) .collect(Collectors.toList()); }3.3 核心分析筛选逻辑这是算法的核心。我们需要遍历历史记录并利用 BPMN 模型判断每个历史节点是否在当前执行流的上游路径上。这里提供一个简化但核心的逻辑框架实际实现可能需要根据流程复杂度进行递归或图遍历。private ListRejectableNodeDTO analyzeAndFilterNodes( String processInstanceId, String currentActivityId, FlowElement currentElement, ListHistoricActivityInstance historicInstances, BpmnModel bpmnModel) { ListRejectableNodeDTO result new ArrayList(); // 获取当前所有的执行流用于判断分支上下文 ListExecution executions runtimeService.createExecutionQuery() .processInstanceId(processInstanceId) .list(); // 假设我们有一个方法能判断一个历史节点是否在“可退回路径”上 // 这需要基于流程定义图进行回溯分析是算法最复杂的部分 MapString, Execution activityIdToExecutionMap buildActivityExecutionMap(executions); for (HistoricActivityInstance instance : historicInstances) { String histActivityId instance.getActivityId(); FlowElement histElement bpmnModel.getFlowElement(histActivityId); boolean isReachable isHistoricallyReachable( histElement, currentElement, activityIdToExecutionMap, bpmnModel ); if (isReachable) { RejectableNodeDTO dto new RejectableNodeDTO(); dto.setActivityId(histActivityId); dto.setActivityName(instance.getActivityName()); dto.setActivityType(instance.getActivityType()); dto.setEndTime(instance.getEndTime()); // 对于并行网关可以设置extraInfo如“并行网关-分支起点” if (parallelGateway.equals(instance.getActivityType())) { dto.setExtraInfo(驳回到此将取消所有并行分支); } result.add(dto); } } return result; } /** * 判断历史节点是否可以从当前节点通过历史执行路径回溯到达。 * 这是一个简化的示意方法真实实现需要复杂的图算法。 */ private boolean isHistoricallyReachable(FlowElement target, FlowElement source, MapString, Execution executionMap, BpmnModel model) { // 实现逻辑包括 // 1. 如果当前节点和目标节点在同一个执行流分支上且目标在上游则可达。 // 2. 如果当前在并行网关的一个分支上目标在网关之前或同一分支的历史上则可达。 // 3. 如果目标是并行网关本身则总是可达驳回至网关将汇聚所有分支。 // 4. 需要处理子流程边界事件等复杂情况。 // 此处返回true仅作演示。 // 实际项目中建议使用Flowable的ProcessDiagramGenerator或自定义的图遍历工具。 return true; }注意isHistoricallyReachable方法的完整实现是本文的“技术制高点”它需要对 BPMN 有深入理解并具备较强的图算法能力。一个可行的替代方案是不完全依赖动态计算而是在流程设计时通过扩展属性为每个节点预定义其“允许驳回到的节点列表”但这牺牲了灵活性。3.4 填充审批人信息用户在选择驳回节点时需要知道上次是谁处理的这个任务。private void enrichWithAssigneeInfo(ListRejectableNodeDTO nodes, String processInstanceId) { if (nodes.isEmpty()) { return; } // 收集所有需要查询的activityId SetString activityIds nodes.stream() .map(RejectableNodeDTO::getActivityId) .collect(Collectors.toSet()); // 批量查询这些活动对应的历史任务 MapString, ListHistoricTaskInstance tasksByActivityId new HashMap(); ListHistoricTaskInstance allHistoricTasks historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .finished() .list(); for (HistoricTaskInstance task : allHistoricTasks) { String key task.getTaskDefinitionKey(); tasksByActivityId.computeIfAbsent(key, k - new ArrayList()).add(task); } // 假设有一个用户服务能根据userId获取用户显示名 // MapString, String userIdToNameMap userService.getDisplayNameMap(userIds); for (RejectableNodeDTO node : nodes) { ListHistoricTaskInstance tasks tasksByActivityId.get(node.getActivityId()); if (tasks ! null !tasks.isEmpty()) { // 拼接处理人姓名。对于会签这里会有多个。 String names tasks.stream() .map(HistoricTaskInstance::getAssignee) .distinct() //.map(userId - userIdToNameMap.getOrDefault(userId, userId)) // 转换用户名 .collect(Collectors.joining(, )); node.setAssigneeNames(names); } else { node.setAssigneeNames(无); } } }4. 前端交互与API设计后端服务准备好后我们需要设计清晰的 API 供前端调用。4.1 查询可驳回节点APIRestController RequestMapping(/api/workflow/reject) public class WorkflowRejectController { Autowired private RejectableNodeService rejectableNodeService; GetMapping(/candidate-nodes) public ApiResponseListRejectableNodeDTO getRejectableNodes(RequestParam String taskId) { try { ListRejectableNodeDTO nodes rejectableNodeService.findRejectableNodes(taskId); return ApiResponse.success(nodes); } catch (Exception e) { log.error(获取可驳回节点失败任务ID: {}, taskId, e); return ApiResponse.error(查询失败: e.getMessage()); } } }前端在用户点击“驳回”按钮时调用此接口获取一个节点列表并以时间线或下拉列表的形式展示给用户。展示信息应包括节点名称、处理时间、上次处理人。4.2 执行驳回操作API当用户选择了目标节点后调用执行驳回的API。PostMapping(/execute) public ApiResponseString executeReject(RequestBody RejectRequest request) { // RejectRequest 包含 taskId, targetActivityId, 驳回意见等 try { // 1. 再次校验安全起见 ListRejectableNodeDTO validNodes rejectableNodeService.findRejectableNodes(request.getTaskId()); boolean isValidTarget validNodes.stream() .anyMatch(node - node.getActivityId().equals(request.getTargetActivityId())); if (!isValidTarget) { return ApiResponse.error(非法驳回目标节点); } // 2. 执行Flowable的状态变更 Task task taskService.createTaskQuery().taskId(request.getTaskId()).singleResult(); runtimeService.createChangeActivityStateBuilder() .processInstanceId(task.getProcessInstanceId()) .moveActivityIdTo(task.getTaskDefinitionKey(), request.getTargetActivityId()) .changeState(); // 3. 记录驳回日志、更新业务数据等后续操作 // businessService.recordReject(task, request); return ApiResponse.success(驳回成功); } catch (FlowableException e) { log.error(Flowable引擎驳回操作失败, e); return ApiResponse.error(流程引擎操作失败: e.getMessage()); } }5. 高级场景与边界情况处理上面的框架解决了主要问题但在企业级应用中我们还需要考虑更多细节。5.1 并行网关与多实例会签的处理这是驳回中最棘手的部分。我们的RejectableNodeService需要特别处理两种并行场景场景A从并行分支驳回到网关前。这需要找到本分支对应的执行流ID并可能需要使用moveExecutionsToSingleActivityId方法将多个分支的执行流一起移动。场景B驳回到并行网关的某个特定分支。这需要精确识别目标分支对应的历史执行流并只移动当前执行流。在analyzeAndFilterNodes方法中对于并行网关类型的历史节点我们需要在extraInfo中提供更明确的提示并可能在执行驳回时采用不同的changeState策略。5.2 子流程的驳回子流程内部的驳回Intra-subprocess相对简单可以将其视为一个独立的流程片段来处理。复杂的是跨边界驳回从子流程内驳回到主流程需要找到子流程调用活动Call Activity对应的节点ID。从主流程驳回到子流程内需要确定具体驳回到子流程中的哪个节点并确保子流程实例被正确重新激活。这要求我们的isHistoricallyReachable方法能够识别子流程边界并在流程定义模型中进行跨层次的路径搜索。5.3 数据上下文与变量处理驳回后目标节点被重新激活。一个关键问题是流程变量和任务表单数据如何处理策略一全部保留。驳回后所有现有的流程变量保持不变。这简单但可能导致目标节点基于“未来”的数据做判断逻辑上可能有问题。策略二回滚到历史快照。在历史表中查询当目标节点上次完成时的变量快照并恢复。这更符合“回到过去”的语义但实现复杂且可能丢失在后续节点中产生的必要数据。策略三混合策略。保留大部分变量但重置与目标节点业务逻辑强相关的特定变量。这需要业务方明确规则。通常在驳回操作发生时除了调用引擎API还需要同步调用一个业务层的回调服务来处理业务数据的版本或状态回退。// 在执行changeState之后 businessDataService.onProcessRejected( processInstanceId, currentActivityId, targetActivityId, request.getRejectComment() );5.4 性能优化建议当流程实例非常长时查询和分析所有历史活动可能会影响性能。可以考虑以下优化缓存流程定义模型BpmnModel 的获取和解析可以缓存起来避免每次请求都去查询数据库。限制历史查询深度业务上驳回通常只发生到最近的若干步骤。可以提供一个配置项限制查询历史活动的范围例如只查最近30天或最近50个活动。异步计算与预加载对于关键流程可以在任务创建时就异步计算其可能的驳回路径并缓存起来。使用原生SQL进行复杂查询对于非常复杂的路径判断直接编写优化的原生SQL查询ACT_HI_ACTINST表关联关系可能比在Java内存中做图遍历更高效正如原始资料中所示。但这牺牲了代码的可读性和对Flowable API的封装性需要谨慎使用。最后记得为你的驳回引擎编写全面的单元测试和集成测试。测试用例应覆盖串行、并行网关、子流程等各种BPMN元素组合成的流程场景。模拟各种驳回操作断言流程状态和任务列表是否符合预期。只有通过严格的测试才能保证这套引擎在复杂的生产环境中稳定可靠。