From d715184ee445cd3fb1491feba3bc5957f6c1d3be Mon Sep 17 00:00:00 2001 From: gulongcheng <474084054@qq.com> Date: Mon, 1 Dec 2025 12:16:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96flowable=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=92=8C=E8=8A=82=E7=82=B9=E7=8A=B6=E6=80=81=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ProcessController.java | 8 +- .../com/sdm/flowable/dto/NodeDetailInfo.java | 18 + .../sdm/flowable/dto/NodeStructureInfo.java | 14 + .../sdm/flowable/dto/ProcessInstanceInfo.java | 20 ++ .../resp/ProcessInstanceDetailResponse.java | 13 + .../sdm/flowable/process/ProcessService.java | 313 ++++++++++-------- 6 files changed, 247 insertions(+), 139 deletions(-) create mode 100644 flowable/src/main/java/com/sdm/flowable/dto/NodeDetailInfo.java create mode 100644 flowable/src/main/java/com/sdm/flowable/dto/NodeStructureInfo.java create mode 100644 flowable/src/main/java/com/sdm/flowable/dto/ProcessInstanceInfo.java create mode 100644 flowable/src/main/java/com/sdm/flowable/dto/resp/ProcessInstanceDetailResponse.java diff --git a/flowable/src/main/java/com/sdm/flowable/controller/ProcessController.java b/flowable/src/main/java/com/sdm/flowable/controller/ProcessController.java index ed6adad9..8b23e53b 100644 --- a/flowable/src/main/java/com/sdm/flowable/controller/ProcessController.java +++ b/flowable/src/main/java/com/sdm/flowable/controller/ProcessController.java @@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sdm.common.common.SdmResponse; import com.sdm.common.feign.inter.flowable.IFlowableFeignClient; import com.sdm.flowable.delegate.handler.HpcHandler; +import com.sdm.flowable.dto.NodeStructureInfo; import com.sdm.flowable.dto.ProcessDefinitionDTO; import com.sdm.flowable.dto.req.AsyncCallbackRequest; import com.sdm.flowable.dto.req.CompleteTaskReq; import com.sdm.flowable.dto.req.RetryRequest; import com.sdm.common.entity.resp.flowable.ProcessInstanceResp; import com.sdm.flowable.dto.resp.DeployFlowableResp; +import com.sdm.flowable.dto.resp.ProcessInstanceDetailResponse; import com.sdm.flowable.process.ProcessService; import com.sdm.flowable.service.IProcessNodeParamService; import lombok.extern.slf4j.Slf4j; @@ -81,7 +83,7 @@ public class ProcessController implements IFlowableFeignClient { * 获取流程定义节点详细信息(直接传流程定义ID) */ @GetMapping("/listNodesByProcessDefinitionId") - public List> listNodesByProcessDefinitionId( + public SdmResponse> listNodesByProcessDefinitionId( @RequestParam String processDefinitionId) { return processService.getNodesByProcessDefinitionId(processDefinitionId); @@ -140,8 +142,8 @@ public class ProcessController implements IFlowableFeignClient { * 根据流程实例 ID 查询流程状态以及节点状态 */ @GetMapping("/getProcessAndNodeDetailByInstanceId") - public Map getProcessAndNodeDetailByInstanceId(@RequestParam String processInstanceId) { - return processService.getProcessAndNodeDetailByInstanceId(processInstanceId); + public SdmResponse getProcessAndNodeDetailByInstanceId(@RequestParam String processInstanceId) { + return processService.getProcessAndNodeDetailByInstanceIdV2(processInstanceId); } diff --git a/flowable/src/main/java/com/sdm/flowable/dto/NodeDetailInfo.java b/flowable/src/main/java/com/sdm/flowable/dto/NodeDetailInfo.java new file mode 100644 index 00000000..2e96075c --- /dev/null +++ b/flowable/src/main/java/com/sdm/flowable/dto/NodeDetailInfo.java @@ -0,0 +1,18 @@ +package com.sdm.flowable.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +@Data +public class NodeDetailInfo extends NodeStructureInfo{ + private String status; // "active" | "finished" | "pending" + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date startTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date endTime; + private Long durationInMillis; + private String durationFormatted; +} \ No newline at end of file diff --git a/flowable/src/main/java/com/sdm/flowable/dto/NodeStructureInfo.java b/flowable/src/main/java/com/sdm/flowable/dto/NodeStructureInfo.java new file mode 100644 index 00000000..3d451c9c --- /dev/null +++ b/flowable/src/main/java/com/sdm/flowable/dto/NodeStructureInfo.java @@ -0,0 +1,14 @@ +package com.sdm.flowable.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class NodeStructureInfo { + protected String id; + protected String name; + protected String type; // 如 "UserTask", "ExclusiveGateway" + protected List nextNodeIds; // 后续节点 ID 列表 + protected String executeConfig; // 扩展属性内容 +} diff --git a/flowable/src/main/java/com/sdm/flowable/dto/ProcessInstanceInfo.java b/flowable/src/main/java/com/sdm/flowable/dto/ProcessInstanceInfo.java new file mode 100644 index 00000000..f1fd2f67 --- /dev/null +++ b/flowable/src/main/java/com/sdm/flowable/dto/ProcessInstanceInfo.java @@ -0,0 +1,20 @@ +package com.sdm.flowable.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +@Data +public class ProcessInstanceInfo { + private String processInstanceId; + private String processDefinitionId; + private String businessKey; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date startTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date endTime; + private Long durationInMillis; + private String durationFormatted; + private String status; // "running" 或 "completed" +} diff --git a/flowable/src/main/java/com/sdm/flowable/dto/resp/ProcessInstanceDetailResponse.java b/flowable/src/main/java/com/sdm/flowable/dto/resp/ProcessInstanceDetailResponse.java new file mode 100644 index 00000000..0e84c8ea --- /dev/null +++ b/flowable/src/main/java/com/sdm/flowable/dto/resp/ProcessInstanceDetailResponse.java @@ -0,0 +1,13 @@ +package com.sdm.flowable.dto.resp; + +import com.sdm.flowable.dto.NodeDetailInfo; +import com.sdm.flowable.dto.ProcessInstanceInfo; +import lombok.Data; + +import java.util.List; + +@Data +public class ProcessInstanceDetailResponse { + private ProcessInstanceInfo processInfo; + private List nodes; +} diff --git a/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java b/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java index b9b63c17..89bbd947 100644 --- a/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java +++ b/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java @@ -3,9 +3,13 @@ package com.sdm.flowable.process; import com.sdm.common.common.SdmResponse; import com.sdm.flowable.constants.FlowableConfig; import com.sdm.flowable.delegate.UniversalDelegate; +import com.sdm.flowable.dto.NodeDetailInfo; +import com.sdm.flowable.dto.NodeStructureInfo; import com.sdm.flowable.dto.ProcessDefinitionDTO; +import com.sdm.flowable.dto.ProcessInstanceInfo; import com.sdm.flowable.dto.req.AsyncCallbackRequest; import com.sdm.flowable.dto.resp.DeployFlowableResp; +import com.sdm.flowable.dto.resp.ProcessInstanceDetailResponse; import com.sdm.flowable.enums.FlowElementTypeEnums; import com.sdm.flowable.util.Dto2BpmnConverter; import com.sdm.flowable.dto.req.CompleteTaskReq; @@ -31,6 +35,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestBody; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; @@ -102,64 +107,34 @@ public class ProcessService { /** * 根据流程定义ID获取节点信息 */ - public List> getNodesByProcessDefinitionId(String processDefinitionId) { - return getFlowStructureWithExtensions(processDefinitionId); + public SdmResponse> getNodesByProcessDefinitionId(String processDefinitionId) { + List orderedNodes = getOrderedFlowNodes(processDefinitionId); + return SdmResponse.success(orderedNodes.stream() + .map(this::buildNodeStructureInfo) + .collect(Collectors.toList())); } + private NodeStructureInfo buildNodeStructureInfo(FlowNode node) { + NodeStructureInfo info = new NodeStructureInfo(); + info.setId(node.getId()); + info.setName(node.getName() != null ? node.getName() : ""); + info.setType(node.getClass().getSimpleName()); - /** - * 带 extensionElements 的流程结构解析 - * - * @param processDefinitionId - * @return - */ - private List> getFlowStructureWithExtensions(String processDefinitionId) { + // 后续节点 + info.setNextNodeIds( + node.getOutgoingFlows().stream() + .map(SequenceFlow::getTargetRef) + .collect(Collectors.toList()) + ); - BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId); - if (bpmnModel == null) return Collections.emptyList(); - - Process process = bpmnModel.getMainProcess(); - - // 找开始事件 - StartEvent startEvent = process.findFlowElementsOfType(StartEvent.class, false) - .stream().findFirst().orElse(null); - - if (startEvent == null) return Collections.emptyList(); - - List> result = new ArrayList<>(); - Queue queue = new LinkedList<>(); - Set visited = new HashSet<>(); - - queue.add(startEvent); - - while (!queue.isEmpty()) { - FlowNode node = queue.poll(); - if (!visited.add(node.getId())) continue; - - Map info = new LinkedHashMap<>(); - info.put("id", node.getId()); - info.put("name", node.getName()); - info.put("type", node.getClass().getSimpleName()); - - // 后继节点 - List nextNodeIds = node.getOutgoingFlows() - .stream().map(SequenceFlow::getTargetRef).collect(Collectors.toList()); - info.put("next", nextNodeIds); - - // ⭐ 添加扩展属性 extensionElements ⭐ - info.put("extensionElements", parseExtensionElements(node.getExtensionElements())); - - result.add(info); - - // BFS 的推进逻辑 - for (String nextId : nextNodeIds) { - FlowElement nextElement = process.getFlowElement(nextId); - if (nextElement instanceof FlowNode nextNode) { - queue.add(nextNode); - } + // 扩展属性 + if (node.getExtensionElements() != null) { + List extList = node.getExtensionElements().get(FlowableConfig.EXECUTECONFIG); + if (extList != null && !extList.isEmpty()) { + info.setExecuteConfig(extList.get(0).getElementText()); } } - return result; + return info; } // 将 extensionElements 转成可阅读的 map @@ -218,115 +193,181 @@ public class ProcessService { } /** - * 查询流程的所有节点状态 - * - * @param processInstanceId - * @return + * 查询流程实例及所有节点的详细状态(返回结构化 DTO) */ - public Map getProcessAndNodeDetailByInstanceId(String processInstanceId) { - return buildProcessNodeDetail(processInstanceId); + public SdmResponse getProcessAndNodeDetailByInstanceIdV2(String processInstanceId) { + ProcessInstanceInfo processInfo = buildProcessInstanceInfo(processInstanceId); + List nodes = buildNodeDetails(processInstanceId, processInfo.getProcessDefinitionId()); + + ProcessInstanceDetailResponse response = new ProcessInstanceDetailResponse(); + response.setProcessInfo(processInfo); + response.setNodes(nodes); + return SdmResponse.success(response); } - /** - * 根据流程实例ID构建流程节点状态(支持 UserTask 任务信息和扩展属性) - */ - private Map buildProcessNodeDetail(String processInstanceId) { + // 构建流程实例信息 + private ProcessInstanceInfo buildProcessInstanceInfo(String processInstanceId) { + HistoricProcessInstance historicInstance = historyService.createHistoricProcessInstanceQuery() + .processInstanceId(processInstanceId) + .singleResult(); - Map result = new HashMap<>(); - - // 1. 查询流程实例历史信息 - HistoricProcessInstance processInstance = - historyService.createHistoricProcessInstanceQuery() - .processInstanceId(processInstanceId) - .singleResult(); - - if (processInstance == null) { + if (historicInstance == null) { throw new RuntimeException("流程实例不存在: " + processInstanceId); } - String processDefinitionId = processInstance.getProcessDefinitionId(); - - // 2. 获取 BPMN 模型 - BpmnModel model = repositoryService.getBpmnModel(processDefinitionId); - Process process = model.getMainProcess(); - Collection flowElements = process.getFlowElements(); - - // 3. 查询正在执行的节点 - // 判断流程是否正在运行中 boolean isRunning = runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId) - .singleResult() != null; + .count() > 0; - List activeIds; - Map activeTaskMap = new HashMap<>(); - if (isRunning) { - activeIds = runtimeService.getActiveActivityIds(processInstanceId); + ProcessInstanceInfo info = new ProcessInstanceInfo(); + info.setProcessInstanceId(historicInstance.getId()); + info.setProcessDefinitionId(historicInstance.getProcessDefinitionId()); + info.setBusinessKey(historicInstance.getBusinessKey()); + info.setStartTime(historicInstance.getStartTime()); + info.setEndTime(historicInstance.getEndTime()); // 可能为 null - // 查询当前活跃任务(UserTask) - List tasks = taskService.createTaskQuery() - .processInstanceId(processInstanceId) - .list(); - activeTaskMap = tasks.stream().collect(Collectors.toMap(org.flowable.task.api.Task::getTaskDefinitionKey, task -> task)); - - } else { - activeIds = Collections.emptyList(); + Long duration = historicInstance.getDurationInMillis(); + if (duration == null && historicInstance.getStartTime() != null && isRunning) { + duration = System.currentTimeMillis() - historicInstance.getStartTime().getTime(); } + info.setDurationInMillis(duration); + info.setDurationFormatted(duration != null ? formatDuration(duration) : null); + info.setStatus(isRunning ? "running" : "completed"); - // 4. 查询已完成节点 - List finished = - historyService.createHistoricActivityInstanceQuery() - .processInstanceId(processInstanceId) - .finished() - .list(); + return info; + } - Set finishedIds = finished.stream() - .map(HistoricActivityInstance::getActivityId) - .collect(Collectors.toSet()); + //构建节点列表 + private List buildNodeDetails(String processInstanceId, String processDefinitionId) { + List orderedNodes = getOrderedFlowNodes(processDefinitionId); - // 5. 组装节点状态 - List> nodeList = new ArrayList<>(); + // 查询运行时 & 历史数据 + List historicActivities = historyService.createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId) + .list(); - for (FlowElement element : flowElements) { - if (!(element instanceof FlowNode)) continue; + Map activityMap = historicActivities.stream() + .collect(Collectors.toMap( + HistoricActivityInstance::getActivityId, + Function.identity(), + (a, b) -> a + )); - FlowNode node = (FlowNode) element; + List activeActivityIds = isProcessRunning(processInstanceId) + ? runtimeService.getActiveActivityIds(processInstanceId) + : Collections.emptyList(); - Map nodeInfo = new HashMap<>(); - nodeInfo.put("id", node.getId()); - nodeInfo.put("name", node.getName()); - nodeInfo.put("type", node.getClass().getSimpleName()); + // 按有序节点构建详情 + return orderedNodes.stream() + .map(node -> buildNodeDetailInfo(node, activityMap, activeActivityIds)) + .collect(Collectors.toList()); + } - // 节点状态 - if (activeIds.contains(node.getId())) { - nodeInfo.put("status", "active"); + // --- 判断流程是否运行中 --- + private boolean isProcessRunning(String processInstanceId) { + return runtimeService.createProcessInstanceQuery() + .processInstanceId(processInstanceId) + .count() > 0; + } - // 如果是 UserTask,返回任务信息 - if (node instanceof UserTask && activeTaskMap.containsKey(node.getId())) { - org.flowable.task.api.Task task = activeTaskMap.get(node.getId()); - nodeInfo.put("taskId", task.getId()); - nodeInfo.put("taskName", task.getName()); - nodeInfo.put("assignee", task.getAssignee()); // 可能为 null - } - } else if (finishedIds.contains(node.getId())) { - nodeInfo.put("status", "finished"); - } else { - nodeInfo.put("status", "pending"); - } + // --- 构建单个节点详情(返回 NodeDetailInfo)--- + private NodeDetailInfo buildNodeDetailInfo( + FlowNode node, + Map activityMap, + List activeActivityIds) { - // 读取扩展属性(executeConfig) - if (node.getExtensionElements() != null && node.getExtensionElements().containsKey(FlowableConfig.EXECUTECONFIG)) { - List elements = node.getExtensionElements().get(FlowableConfig.EXECUTECONFIG); - if (elements != null && !elements.isEmpty()) { - nodeInfo.put("executeConfig", elements.get(0).getElementText()); + // 先构建静态结构 + NodeStructureInfo base = buildNodeStructureInfo(node); + + // 再填充动态信息 + NodeDetailInfo detail = new NodeDetailInfo(); + detail.setId(base.getId()); + detail.setName(base.getName()); + detail.setType(base.getType()); + detail.setNextNodeIds(base.getNextNodeIds()); + detail.setExecuteConfig(base.getExecuteConfig()); + + // 动态状态逻辑 + String nodeId = node.getId(); + HistoricActivityInstance historicActivity = activityMap.get(nodeId); + boolean isActive = activeActivityIds.contains(nodeId); + boolean isFinished = historicActivity != null && historicActivity.getEndTime() != null; + + detail.setStatus(isFinished ? "finished" : (isActive ? "active" : "pending")); + + if (historicActivity != null) { + detail.setStartTime(historicActivity.getStartTime()); + if (isFinished) { + detail.setEndTime(historicActivity.getEndTime()); + detail.setDurationInMillis(historicActivity.getDurationInMillis()); + if (historicActivity.getDurationInMillis() != null) { + detail.setDurationFormatted(formatDuration(historicActivity.getDurationInMillis())); } } - - nodeList.add(nodeInfo); } - result.put("processInstanceId", processInstanceId); - result.put("nodes", nodeList); - return result; + return detail; + } + + // --- 工具方法:格式化耗时(毫秒 → 可读字符串)--- + private String formatDuration(long millis) { + long seconds = millis / 1000; + long mins = seconds / 60; + long hours = mins / 60; + long days = hours / 24; + + if (days > 0) return String.format("%dd %dh %dm %ds", days, hours % 24, mins % 60, seconds % 60); + if (hours > 0) return String.format("%dh %dm %ds", hours, mins % 60, seconds % 60); + if (mins > 0) return String.format("%dm %ds", mins, seconds % 60); + return String.format("%ds", seconds); + } + + /** + * 根据流程定义 ID,按流程执行顺序(BFS)返回所有可达的 FlowNode + */ + private List getOrderedFlowNodes(String processDefinitionId) { + BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId); + if (bpmnModel == null || bpmnModel.getMainProcess() == null) { + return Collections.emptyList(); + } + + Process process = bpmnModel.getMainProcess(); + + // 找开始事件 + StartEvent startEvent = process.findFlowElementsOfType(StartEvent.class, false) + .stream() + .findFirst() + .orElse(null); + + if (startEvent == null) { + return Collections.emptyList(); + } + + // BFS 遍历 + List orderedNodes = new ArrayList<>(); + Queue queue = new LinkedList<>(); + Set visited = new HashSet<>(); + + queue.offer(startEvent); + visited.add(startEvent.getId()); + + while (!queue.isEmpty()) { + FlowNode current = queue.poll(); + orderedNodes.add(current); + + // 对 outgoingFlows 排序可选(保证并行分支顺序稳定) + current.getOutgoingFlows().stream() + .sorted(Comparator.comparing(SequenceFlow::getId)) // 可选:提升顺序稳定性 + .forEach(flow -> { + String targetRef = flow.getTargetRef(); + FlowElement element = process.getFlowElement(targetRef); + if (element instanceof FlowNode && visited.add(targetRef)) { + queue.offer((FlowNode) element); + } + }); + } + + return orderedNodes; } public void continueServiceTask(@RequestBody CompleteTaskReq req) {