diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java index b699ef3c..a3c613a3 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java @@ -1,9 +1,6 @@ package com.qqchen.deploy.backend.deploy.api; -import com.qqchen.deploy.backend.deploy.dto.DeployRequestDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployResultDTO; -import com.qqchen.deploy.backend.deploy.dto.UserDeployableDTO; +import com.qqchen.deploy.backend.deploy.dto.*; import com.qqchen.deploy.backend.deploy.service.IDeployRecordService; import com.qqchen.deploy.backend.deploy.service.IDeployService; import com.qqchen.deploy.backend.framework.api.Response; @@ -16,6 +13,8 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + /** * 部署管理API控制器 @@ -67,5 +66,26 @@ public class DeployApiController { ) { return Response.success(deployRecordService.getDeployFlowGraph(deployRecordId)); } + + /** + * 获取当前用户的部署审批任务列表 + */ + @Operation(summary = "获取我的部署审批任务", description = "查询当前登录用户待审批的部署任务,包含完整的部署业务上下文信息") + @GetMapping("/my-approval-tasks") + @PreAuthorize("isAuthenticated()") + public Response> getMyApprovalTasks() { + return Response.success(deployService.getMyApprovalTasks()); + } + + /** + * 完成部署审批 + */ + @Operation(summary = "完成部署审批", description = "完成部署审批任务,后续节点(Jenkins构建、通知等)将由Flowable异步执行") + @PostMapping("/complete") + @PreAuthorize("isAuthenticated()") + public Response completeApproval(@Validated @RequestBody DeployApprovalCompleteRequest request) { + deployService.completeApproval(request); + return Response.success(); + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployApprovalCompleteRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployApprovalCompleteRequest.java new file mode 100644 index 00000000..25c5e4bc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployApprovalCompleteRequest.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 部署审批完成请求 + * + *

用于审批部署任务,支持异步处理 + * + * @author qqchen + * @since 2025-11-05 + */ +@Data +@Schema(description = "部署审批完成请求") +public class DeployApprovalCompleteRequest { + + @NotBlank(message = "任务ID不能为空") + @Schema(description = "Flowable任务ID", required = true, example = "c527e234-ba0a-11f0-a005-00ffaa86ab68") + private String taskId; + + @NotNull(message = "审批结果不能为空") + @Schema(description = "审批结果: APPROVED=通过, REJECTED=拒绝", required = true, example = "APPROVED") + private ApprovalResultEnum result; + + @Schema(description = "审批意见", example = "同意部署到生产环境") + private String comment; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployApprovalTaskDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployApprovalTaskDTO.java new file mode 100644 index 00000000..610d24cb --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployApprovalTaskDTO.java @@ -0,0 +1,109 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 部署审批任务DTO + * + *

扩展了审批任务的基本信息,增加部署相关的业务上下文 + *

用于部署审批列表,让审批人员快速了解审批内容 + * + * @author qqchen + * @since 2025-11-05 + */ +@Data +@Schema(description = "部署审批任务信息") +public class DeployApprovalTaskDTO { + + // ============ 审批任务基本信息 ============ + + @Schema(description = "任务ID") + private String taskId; + + @Schema(description = "任务名称") + private String taskName; + + @Schema(description = "任务描述") + private String taskDescription; + + @Schema(description = "流程实例ID") + private String processInstanceId; + + @Schema(description = "流程定义ID") + private String processDefinitionId; + + @Schema(description = "审批人") + private String assignee; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "到期时间") + private LocalDateTime dueDate; + + @Schema(description = "审批标题") + private String approvalTitle; + + @Schema(description = "审批内容") + private String approvalContent; + + @Schema(description = "审批模式: SINGLE-单人审批, MULTI-多人会签, OR-多人或签") + private String approvalMode; + + @Schema(description = "是否允许转交") + private Boolean allowDelegate; + + @Schema(description = "是否允许加签") + private Boolean allowAddSign; + + @Schema(description = "是否必须填写意见") + private Boolean requireComment; + + // ============ 部署业务上下文信息 ============ + + @Schema(description = "部署记录ID") + private Long deployRecordId; + + @Schema(description = "业务标识(UUID)") + private String businessKey; + + @Schema(description = "团队ID") + private Long teamId; + + @Schema(description = "团队名称") + private String teamName; + + @Schema(description = "应用ID") + private Long applicationId; + + @Schema(description = "应用编码") + private String applicationCode; + + @Schema(description = "应用名称") + private String applicationName; + + @Schema(description = "环境ID") + private Long environmentId; + + @Schema(description = "环境编码") + private String environmentCode; + + @Schema(description = "环境名称") + private String environmentName; + + @Schema(description = "发起人") + private String deployBy; + + @Schema(description = "部署备注") + private String deployRemark; + + @Schema(description = "部署开始时间") + private LocalDateTime deployStartTime; + + @Schema(description = "待审批时长(毫秒)- 从任务创建到现在的时长") + private Long pendingDuration; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java index 8d59a273..03cf389f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java @@ -3,9 +3,11 @@ package com.qqchen.deploy.backend.deploy.dto; import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeInstanceDTO; import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph; +import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.time.LocalDateTime; import java.util.List; /** @@ -19,22 +21,89 @@ import java.util.List; @Schema(description = "部署记录流程图信息") public class DeployRecordFlowGraphDTO { + // ============ 部署记录基本信息 ============ + @Schema(description = "部署记录ID") private Long deployRecordId; + @Schema(description = "业务标识(UUID)") + private String businessKey; + + @Schema(description = "部署状态") + private DeployRecordStatusEnums deployStatus; + + @Schema(description = "部署人") + private String deployBy; + + @Schema(description = "部署备注") + private String deployRemark; + + @Schema(description = "部署开始时间") + private LocalDateTime deployStartTime; + + @Schema(description = "部署结束时间") + private LocalDateTime deployEndTime; + + @Schema(description = "部署总时长(毫秒)") + private Long deployDuration; + + // ============ 业务上下文信息 ============ + + @Schema(description = "应用名称") + private String applicationName; + + @Schema(description = "应用编码") + private String applicationCode; + + @Schema(description = "环境名称") + private String environmentName; + + @Schema(description = "团队名称") + private String teamName; + + // ============ 工作流实例信息 ============ + @Schema(description = "工作流实例ID") private Long workflowInstanceId; @Schema(description = "流程实例ID(Flowable)") private String processInstanceId; - @Schema(description = "部署状态") - private DeployRecordStatusEnums deployStatus; + @Schema(description = "工作流实例状态") + private WorkflowInstanceStatusEnums workflowStatus; + + @Schema(description = "工作流开始时间") + private LocalDateTime workflowStartTime; + + @Schema(description = "工作流结束时间") + private LocalDateTime workflowEndTime; + + @Schema(description = "工作流总时长(毫秒)") + private Long workflowDuration; + + // ============ 执行统计信息 ============ + + @Schema(description = "总节点数") + private Integer totalNodeCount; + + @Schema(description = "已执行节点数") + private Integer executedNodeCount; + + @Schema(description = "成功节点数") + private Integer successNodeCount; + + @Schema(description = "失败节点数") + private Integer failedNodeCount; + + @Schema(description = "运行中节点数") + private Integer runningNodeCount; + + // ============ 流程图数据 ============ @Schema(description = "流程图数据(画布快照,包含节点和边的位置信息)") private WorkflowDefinitionGraph graph; - @Schema(description = "节点执行状态列表(用于标记每个节点的执行状态)") + @Schema(description = "节点执行状态列表(用于标记每个节点的执行状态,包含已执行和未执行节点)") private List nodeInstances; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IDeployRecordRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IDeployRecordRepository.java index 6079b43b..9c268b8b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IDeployRecordRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IDeployRecordRepository.java @@ -25,6 +25,11 @@ public interface IDeployRecordRepository extends IBaseRepository findByWorkflowInstanceIdAndDeletedFalse(Long workflowInstanceId); + /** + * 根据业务标识查询部署记录 + */ + Optional findByBusinessKeyAndDeletedFalse(String businessKey); + /** * 根据团队应用ID查询最新部署记录 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployService.java index 2ee5f964..2f580c9a 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployService.java @@ -1,8 +1,8 @@ package com.qqchen.deploy.backend.deploy.service; -import com.qqchen.deploy.backend.deploy.dto.DeployRequestDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployResultDTO; -import com.qqchen.deploy.backend.deploy.dto.UserDeployableDTO; +import com.qqchen.deploy.backend.deploy.dto.*; + +import java.util.List; /** * 部署服务接口 @@ -26,5 +26,22 @@ public interface IDeployService { * @return 部署结果 */ DeployResultDTO executeDeploy(DeployRequestDTO request); + + /** + * 获取当前用户的部署审批任务列表 + *

查询当前登录用户待审批的部署任务,包含完整的部署业务上下文信息 + * + * @return 部署审批任务列表 + */ + List getMyApprovalTasks(); + + /** + * 完成部署审批 + *

完成部署审批任务,支持通过/拒绝两种结果 + *

后续节点(如Jenkins构建、通知)将由Flowable异步执行,不会阻塞HTTP请求 + * + * @param request 审批完成请求 + */ + void completeApproval(DeployApprovalCompleteRequest request); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java index d4825963..3d513139 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java @@ -4,14 +4,19 @@ import com.qqchen.deploy.backend.deploy.converter.DeployRecordConverter; import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO; import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO; import com.qqchen.deploy.backend.deploy.entity.DeployRecord; +import com.qqchen.deploy.backend.deploy.entity.Team; +import com.qqchen.deploy.backend.deploy.entity.Application; +import com.qqchen.deploy.backend.deploy.entity.Environment; import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; import com.qqchen.deploy.backend.deploy.query.DeployRecordQuery; import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository; +import com.qqchen.deploy.backend.deploy.repository.ITeamRepository; +import com.qqchen.deploy.backend.deploy.repository.IApplicationRepository; +import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository; import com.qqchen.deploy.backend.deploy.service.IDeployRecordService; import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.enums.ResponseCode; -import com.qqchen.deploy.backend.workflow.converter.WorkflowNodeInstanceConverter; import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeInstanceDTO; import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeInstance; @@ -23,18 +28,23 @@ import com.qqchen.deploy.backend.workflow.util.FlowableUtils; import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.Process; -import org.flowable.engine.HistoryService; import org.flowable.engine.RepositoryService; -import org.flowable.variable.api.history.HistoricVariableInstance; +import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph; +import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraphNode; +import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraphEdge; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; /** @@ -60,14 +70,17 @@ public class DeployRecordServiceImpl extends BaseServiceImpl new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"部署记录"})); - // 2. 查询工作流实例(包含流程图快照) + // 1.2 查询工作流实例 WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId()) .orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"工作流实例"})); - // 3. 查询节点实例列表(只查询实际执行过的节点) + // 1.3 获取流程图快照 + WorkflowDefinitionGraph graphSnapshot = workflowInstance.getGraphSnapshot(); + if (graphSnapshot == null || graphSnapshot.getNodes() == null) { + log.warn("工作流实例 {} 的流程图快照为空", workflowInstance.getId()); + return buildEmptyFlowGraphDTO(deployRecord, workflowInstance); + } + + // 1.4 查询业务上下文信息 + Team team = teamRepository.findById(deployRecord.getTeamId()).orElse(null); + Application application = applicationRepository.findById(deployRecord.getApplicationId()).orElse(null); + Environment environment = environmentRepository.findById(deployRecord.getEnvironmentId()).orElse(null); + + // ============ 2. 查询并组装节点执行状态 ============ + + // 2.1 查询已执行的节点实例列表 List nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId()); - // 4. 从BPMN模型中获取流程元素顺序(用于排序) + // 2.2 构建已执行节点的映射(按nodeId索引) + Map nodeInstanceMap = nodeInstances.stream() + .collect(Collectors.toMap(WorkflowNodeInstance::getNodeId, instance -> instance, (a, b) -> a)); + + // 2.3 从BPMN模型中获取流程元素顺序(用于排序) BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId()); Process process = bpmnModel.getMainProcess(); List flowElements = FlowableUtils.sortFlowElements(process); - // 5. 构建节点ID到流程顺序的映射(用于排序) + // 2.4 构建节点ID到流程顺序的映射 Map nodeOrderMap = new HashMap<>(); for (int i = 0; i < flowElements.size(); i++) { nodeOrderMap.put(flowElements.get(i).getId(), i); } - // 6. 按流程顺序排序节点实例(只包含实际执行过的节点) - List orderedNodeInstances = nodeInstances.stream() - .sorted((a, b) -> { - Integer orderA = nodeOrderMap.getOrDefault(a.getNodeId(), Integer.MAX_VALUE); - Integer orderB = nodeOrderMap.getOrDefault(b.getNodeId(), Integer.MAX_VALUE); - return orderA.compareTo(orderB); - }) + // 2.5 遍历流程图中的所有节点,组装节点DTO(包括已执行和未执行的) + List nodeInstanceDTOs = graphSnapshot.getNodes().stream() + .sorted(Comparator.comparingInt(node -> nodeOrderMap.getOrDefault(node.getId(), Integer.MAX_VALUE))) + .map(graphNode -> buildNodeInstanceDTO(graphNode, nodeInstanceMap.get(graphNode.getId()), workflowInstance)) .collect(Collectors.toList()); - // 7. 从历史流程变量中获取每个节点的 outputs 数据 - Map> nodeOutputsMap = getNodeOutputsFromHistory(workflowInstance.getProcessInstanceId()); + // ============ 3. 计算执行统计 ============ + + int totalNodeCount = nodeInstanceDTOs.size(); + int executedNodeCount = (int) nodeInstanceDTOs.stream() + .filter(node -> node.getStatus() != WorkflowNodeInstanceStatusEnums.NOT_STARTED) + .count(); + int successNodeCount = (int) nodeInstanceDTOs.stream() + .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.COMPLETED) + .count(); + int failedNodeCount = (int) nodeInstanceDTOs.stream() + .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.FAILED) + .count(); + int runningNodeCount = (int) nodeInstanceDTOs.stream() + .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.RUNNING) + .count(); - // 8. 转换为 DTO 并填充 outputs 数据 - List nodeInstanceDTOs = workflowNodeInstanceConverter.toDtoList(orderedNodeInstances); - nodeInstanceDTOs.forEach(dto -> { - // 从流程变量中获取该节点的 outputs 数据 - Map nodeOutputs = nodeOutputsMap.get(dto.getNodeId()); - if (nodeOutputs != null) { - // 提取 outputs 部分(格式:{nodeId: {outputs: {...}}} - @SuppressWarnings("unchecked") - Map nodeData = (Map) nodeOutputs; - @SuppressWarnings("unchecked") - Map outputs = (Map) nodeData.get("outputs"); - if (outputs != null) { - dto.setOutputs(outputs); - } - } - }); - - // 9. 组装DTO + // ============ 4. 组装完整的DTO ============ + DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO(); + + // 4.1 部署记录基本信息 dto.setDeployRecordId(deployRecord.getId()); + dto.setBusinessKey(deployRecord.getBusinessKey()); + dto.setDeployStatus(deployRecord.getStatus()); + dto.setDeployBy(deployRecord.getDeployBy()); + dto.setDeployRemark(deployRecord.getDeployRemark()); + dto.setDeployStartTime(deployRecord.getStartTime()); + dto.setDeployEndTime(deployRecord.getEndTime()); + dto.setDeployDuration(calculateDuration(deployRecord.getStartTime(), deployRecord.getEndTime())); + + // 4.2 业务上下文信息 + dto.setApplicationName(application != null ? application.getAppName() : null); + dto.setApplicationCode(application != null ? application.getAppCode() : null); + dto.setEnvironmentName(environment != null ? environment.getEnvName() : null); + dto.setTeamName(team != null ? team.getTeamName() : null); + + // 4.3 工作流实例信息 dto.setWorkflowInstanceId(workflowInstance.getId()); dto.setProcessInstanceId(workflowInstance.getProcessInstanceId()); - dto.setDeployStatus(deployRecord.getStatus()); - dto.setGraph(workflowInstance.getGraphSnapshot()); // 流程图结构数据 - dto.setNodeInstances(nodeInstanceDTOs); // 节点执行状态(包含 outputs) + dto.setWorkflowStatus(workflowInstance.getStatus()); + dto.setWorkflowStartTime(workflowInstance.getStartTime()); + dto.setWorkflowEndTime(workflowInstance.getEndTime()); + dto.setWorkflowDuration(calculateDuration(workflowInstance.getStartTime(), workflowInstance.getEndTime())); + + // 4.4 执行统计信息 + dto.setTotalNodeCount(totalNodeCount); + dto.setExecutedNodeCount(executedNodeCount); + dto.setSuccessNodeCount(successNodeCount); + dto.setFailedNodeCount(failedNodeCount); + dto.setRunningNodeCount(runningNodeCount); + + // 4.5 流程图数据 + dto.setGraph(graphSnapshot); + dto.setNodeInstances(nodeInstanceDTOs); + + log.info("获取部署流程图成功: deployRecordId={}, 总节点数={}, 已执行={}, 成功={}, 失败={}, 运行中={}", + deployRecordId, totalNodeCount, executedNodeCount, successNodeCount, failedNodeCount, runningNodeCount); return dto; } /** - * 从历史流程变量中获取所有节点的 outputs 数据 + * 构建节点实例DTO * - * @param processInstanceId 流程实例ID - * @return 节点ID到节点数据的映射(格式:{nodeId: {outputs: {...}}}) + * @param graphNode 流程图节点 + * @param nodeInstance 节点实例(可能为null,表示未执行) + * @param workflowInstance 工作流实例 + * @return 节点实例DTO */ - private Map> getNodeOutputsFromHistory(String processInstanceId) { - Map> nodeOutputsMap = new HashMap<>(); + private WorkflowNodeInstanceDTO buildNodeInstanceDTO( + WorkflowDefinitionGraphNode graphNode, + WorkflowNodeInstance nodeInstance, + WorkflowInstance workflowInstance) { - try { - // 查询历史流程变量 - List variables = historyService - .createHistoricVariableInstanceQuery() - .processInstanceId(processInstanceId) - .list(); - - // 遍历变量,查找节点相关的变量(格式:{nodeId: {outputs: {...}}} - for (HistoricVariableInstance variable : variables) { - String variableName = variable.getVariableName(); - Object variableValue = variable.getValue(); - - // 检查是否是节点数据(节点ID通常是 sid_ 开头) - if (variableName != null && variableValue instanceof Map) { - @SuppressWarnings("unchecked") - Map nodeData = (Map) variableValue; - - // 检查是否包含 outputs 字段(这是节点数据的标识) - if (nodeData.containsKey("outputs")) { - nodeOutputsMap.put(variableName, nodeData); - } - } - } - - log.debug("从历史流程变量中获取节点 outputs: processInstanceId={}, nodeCount={}", - processInstanceId, nodeOutputsMap.size()); - } catch (Exception e) { - log.warn("获取节点 outputs 失败: processInstanceId={}", processInstanceId, e); + WorkflowNodeInstanceDTO dto = new WorkflowNodeInstanceDTO(); + + if (nodeInstance != null) { + // ✅ 已执行的节点:使用节点实例的数据 + dto.setId(nodeInstance.getId()); + dto.setNodeId(nodeInstance.getNodeId()); + dto.setNodeName(nodeInstance.getNodeName()); + dto.setNodeType(nodeInstance.getNodeType()); + dto.setStatus(nodeInstance.getStatus()); + dto.setStartTime(nodeInstance.getStartTime()); + dto.setEndTime(nodeInstance.getEndTime()); + dto.setDuration(calculateDuration(nodeInstance.getStartTime(), nodeInstance.getEndTime())); + dto.setErrorMessage(nodeInstance.getErrorMessage()); // ✅ 包含错误信息 + dto.setProcessInstanceId(nodeInstance.getProcessInstanceId()); + dto.setCreateTime(nodeInstance.getCreateTime()); + dto.setUpdateTime(nodeInstance.getUpdateTime()); + } else { + // ✅ 未执行的节点:从流程图节点获取基本信息 + dto.setId(null); + dto.setNodeId(graphNode.getId()); + dto.setNodeName(graphNode.getNodeName()); + dto.setNodeType(graphNode.getNodeType() != null ? graphNode.getNodeType().name() : null); + dto.setStatus(WorkflowNodeInstanceStatusEnums.NOT_STARTED); + dto.setStartTime(null); + dto.setEndTime(null); + dto.setDuration(null); + dto.setErrorMessage(null); + dto.setProcessInstanceId(workflowInstance.getProcessInstanceId()); + dto.setCreateTime(null); + dto.setUpdateTime(null); } + + return dto; + } - return nodeOutputsMap; + /** + * 计算时长(毫秒) + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 时长(毫秒),如果任一时间为 null 则返回 null + */ + private Long calculateDuration(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return null; + } + return java.time.Duration.between(startTime, endTime).toMillis(); + } + + + /** + * 构建空的流程图DTO(当流程图快照为空时) + */ + private DeployRecordFlowGraphDTO buildEmptyFlowGraphDTO(DeployRecord deployRecord, WorkflowInstance workflowInstance) { + DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO(); + + // 部署记录基本信息 + dto.setDeployRecordId(deployRecord.getId()); + dto.setBusinessKey(deployRecord.getBusinessKey()); + dto.setDeployStatus(deployRecord.getStatus()); + dto.setDeployBy(deployRecord.getDeployBy()); + dto.setDeployRemark(deployRecord.getDeployRemark()); + dto.setDeployStartTime(deployRecord.getStartTime()); + dto.setDeployEndTime(deployRecord.getEndTime()); + dto.setDeployDuration(calculateDuration(deployRecord.getStartTime(), deployRecord.getEndTime())); + + // 工作流实例信息 + dto.setWorkflowInstanceId(workflowInstance.getId()); + dto.setProcessInstanceId(workflowInstance.getProcessInstanceId()); + dto.setWorkflowStatus(workflowInstance.getStatus()); + dto.setWorkflowStartTime(workflowInstance.getStartTime()); + dto.setWorkflowEndTime(workflowInstance.getEndTime()); + dto.setWorkflowDuration(calculateDuration(workflowInstance.getStartTime(), workflowInstance.getEndTime())); + + // 执行统计信息(都为0) + dto.setTotalNodeCount(0); + dto.setExecutedNodeCount(0); + dto.setSuccessNodeCount(0); + dto.setFailedNodeCount(0); + dto.setRunningNodeCount(0); + + // 流程图数据(空) + dto.setGraph(new WorkflowDefinitionGraph()); + dto.setNodeInstances(new ArrayList<>()); + + return dto; } /** diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployServiceImpl.java index fd2d931f..9ab58e90 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployServiceImpl.java @@ -7,13 +7,17 @@ import com.qqchen.deploy.backend.deploy.service.IDeployService; import com.qqchen.deploy.backend.framework.security.SecurityUtils; import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver; import com.qqchen.deploy.backend.system.entity.User; import com.qqchen.deploy.backend.system.repository.IUserRepository; import com.fasterxml.jackson.databind.ObjectMapper; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceStartRequest; +import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping; import com.qqchen.deploy.backend.workflow.dto.inputmapping.JenkinsBuildInputMapping; +import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; +import com.qqchen.deploy.backend.workflow.model.NodeContext; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService; import com.qqchen.deploy.backend.deploy.service.IDeployRecordService; @@ -23,11 +27,17 @@ import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; +import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.*; import java.util.stream.Collectors; @@ -82,6 +92,12 @@ public class DeployServiceImpl implements IDeployService { @Resource private ObjectMapper objectMapper; + @Resource + private TaskService taskService; + + @Resource + private RuntimeService runtimeService; + /** * 数据组装上下文(封装所有需要的Map,避免方法参数过多) */ @@ -551,77 +567,482 @@ public class DeployServiceImpl implements IDeployService { @Override @Transactional public DeployResultDTO executeDeploy(DeployRequestDTO request) { - // 1. 查询团队应用配置 - TeamApplication teamApp = teamApplicationRepository.findById(request.getTeamApplicationId()).orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND)); - - // 2. 查询工作流定义(获取 processKey) - WorkflowDefinition workflowDefinition = workflowDefinitionRepository.findById(teamApp.getWorkflowDefinitionId()).orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[] {"工作流定义"})); - - // 3. 查询应用信息 - Application application = applicationRepository.findById(teamApp.getApplicationId()).orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[] {"应用"})); - - // 4. 查询环境信息 - Environment environment = environmentRepository.findById(teamApp.getEnvironmentId()).orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[] {"环境"})); - - // 5. 生成业务标识(UUID) + log.info("开始执行部署: teamApplicationId={}, remark={}", request.getTeamApplicationId(), request.getRemark()); + + // 1. 加载部署上下文数据 + DeployContext context = loadDeployContext(request.getTeamApplicationId()); + + // 2. 生成业务标识(UUID) String businessKey = UUID.randomUUID().toString(); + + // 3. 构造流程变量 + Map variables = buildDeployVariables(context, request, businessKey); + + // 4. 启动工作流并创建记录 + WorkflowInstanceDTO workflowInstance = startWorkflowAndCreateRecord(context, businessKey, variables, request.getRemark()); + + // 5. 返回结果 + return buildDeployResult(workflowInstance, businessKey, context); + } - // 6. 构造流程变量 + /** + * 加载部署上下文数据 + */ + private DeployContext loadDeployContext(Long teamApplicationId) { + // 查询团队应用配置 + TeamApplication teamApp = teamApplicationRepository.findById(teamApplicationId) + .orElseThrow(() -> new BusinessException(ResponseCode.TEAM_APPLICATION_NOT_FOUND)); + + // 查询工作流定义 + WorkflowDefinition workflowDefinition = workflowDefinitionRepository.findById(teamApp.getWorkflowDefinitionId()) + .orElseThrow(() -> new BusinessException(ResponseCode.DEPLOY_WORKFLOW_NOT_CONFIGURED)); + + // 查询应用信息 + Application application = applicationRepository.findById(teamApp.getApplicationId()) + .orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND, new Object[]{"应用"})); + + // 查询环境信息 + Environment environment = environmentRepository.findById(teamApp.getEnvironmentId()) + .orElseThrow(() -> new BusinessException(ResponseCode.DATA_NOT_FOUND, new Object[]{"环境"})); + + // 查询团队环境配置 + TeamEnvironmentConfig teamEnvConfig = teamEnvironmentConfigRepository + .findByTeamIdAndEnvironmentId(teamApp.getTeamId(), teamApp.getEnvironmentId()) + .orElseThrow(() -> new BusinessException(ResponseCode.DEPLOY_ENVIRONMENT_CONFIG_MISSING, + new Object[]{teamApp.getTeamId(), teamApp.getEnvironmentId()})); + + return new DeployContext(teamApp, workflowDefinition, application, environment, teamEnvConfig); + } + + /** + * 构建部署流程变量 + */ + private Map buildDeployVariables(DeployContext context, DeployRequestDTO request, String businessKey) { Map variables = new HashMap<>(); - + // 部署上下文 + variables.put("deploy", buildDeployContextMap(context, request)); + + // Jenkins 配置 + Map jenkinsConfig = buildJenkinsConfig(context.teamApp); + if (jenkinsConfig != null) { + variables.put("jenkins", jenkinsConfig); + } + + // 审批配置 + variables.put("approval", buildApprovalConfig(context.teamEnvConfig)); + + // 通知配置 + variables.put("notification", buildNotificationConfig(context.teamEnvConfig)); + + return variables; + } + + /** + * 构建部署上下文Map + */ + private Map buildDeployContextMap(DeployContext context, DeployRequestDTO request) { Map deployContext = new HashMap<>(); - deployContext.put("teamApplicationId", teamApp.getId()); - deployContext.put("teamId", teamApp.getTeamId()); - deployContext.put("applicationId", teamApp.getApplicationId()); - deployContext.put("applicationCode", application.getAppCode()); - deployContext.put("applicationName", application.getAppName()); - deployContext.put("environmentId", teamApp.getEnvironmentId()); - deployContext.put("environmentCode", environment.getEnvCode()); - deployContext.put("environmentName", environment.getEnvName()); + deployContext.put("teamApplicationId", context.teamApp.getId()); + deployContext.put("teamId", context.teamApp.getTeamId()); + deployContext.put("applicationId", context.teamApp.getApplicationId()); + deployContext.put("applicationCode", context.application.getAppCode()); + deployContext.put("applicationName", context.application.getAppName()); + deployContext.put("environmentId", context.teamApp.getEnvironmentId()); + deployContext.put("environmentCode", context.environment.getEnvCode()); + deployContext.put("environmentName", context.environment.getEnvName()); deployContext.put("by", SecurityUtils.getCurrentUsername()); deployContext.put("remark", request.getRemark()); - variables.put("deploy", deployContext); + return deployContext; + } - // Jenkins 配置(使用强类型 JenkinsBuildInputMapping) - if (teamApp.getDeploySystemId() != null && teamApp.getDeployJob() != null) { - JenkinsBuildInputMapping jenkinsInput = new JenkinsBuildInputMapping(); - jenkinsInput.setServerId(teamApp.getDeploySystemId()); - jenkinsInput.setJobName(teamApp.getDeployJob()); - if (teamApp.getBranch() != null) { - jenkinsInput.setBranch(teamApp.getBranch()); - } - - // 转换为 Map(Flowable 只支持基本类型) - variables.put("jenkins", objectMapper.convertValue(jenkinsInput, Map.class)); - variables.put("approval", Map.of("required", true, "userIds", "admin")); - variables.put("notification", Map.of("channelId", 1)); + /** + * 构建Jenkins配置 + */ + private Map buildJenkinsConfig(TeamApplication teamApp) { + if (teamApp.getDeploySystemId() == null || teamApp.getDeployJob() == null) { + log.warn("未配置Jenkins构建任务: teamApplicationId={}", teamApp.getId()); + return null; } + + JenkinsBuildInputMapping jenkinsInput = new JenkinsBuildInputMapping(); + jenkinsInput.setServerId(teamApp.getDeploySystemId()); + jenkinsInput.setJobName(teamApp.getDeployJob()); + if (teamApp.getBranch() != null) { + jenkinsInput.setBranch(teamApp.getBranch()); + } + + // 转换为 Map(Flowable 只支持基本类型) + return objectMapper.convertValue(jenkinsInput, Map.class); + } + /** + * 构建审批配置 + */ + private Map buildApprovalConfig(TeamEnvironmentConfig teamEnvConfig) { + Map approvalConfig = new HashMap<>(); + Boolean approvalRequired = teamEnvConfig.getApprovalRequired() != null ? teamEnvConfig.getApprovalRequired() : false; + approvalConfig.put("required", approvalRequired); + + // 处理审批人ID列表:转换为逗号分隔的用户名字符串 + if (teamEnvConfig.getApproverUserIds() != null && !teamEnvConfig.getApproverUserIds().isEmpty()) { + List approvers = userRepository.findAllById(teamEnvConfig.getApproverUserIds()); + if (approvers.isEmpty()) { + throw new BusinessException(ResponseCode.USER_NOT_FOUND); + } + + String userIds = approvers.stream() + .map(User::getUsername) + .collect(Collectors.joining(",")); + approvalConfig.put("userIds", userIds); + log.debug("审批配置: required={}, userIds={}", approvalRequired, userIds); + } else { + // 如果要求审批但未配置审批人,抛出专属异常 + if (Boolean.TRUE.equals(approvalRequired)) { + throw new BusinessException(ResponseCode.DEPLOY_APPROVER_NOT_CONFIGURED); + } + approvalConfig.put("userIds", ""); + } + + return approvalConfig; + } - // 7. 构造工作流启动请求 + /** + * 构建通知配置 + */ + private Map buildNotificationConfig(TeamEnvironmentConfig teamEnvConfig) { + Map notificationConfig = new HashMap<>(); + + if (teamEnvConfig.getNotificationChannelId() == null) { + throw new BusinessException(ResponseCode.DEPLOY_NOTIFICATION_CONFIG_MISSING); + } + + notificationConfig.put("channelId", teamEnvConfig.getNotificationChannelId()); + log.debug("通知配置: channelId={}", teamEnvConfig.getNotificationChannelId()); + + return notificationConfig; + } + + /** + * 启动工作流并创建部署记录 + */ + private WorkflowInstanceDTO startWorkflowAndCreateRecord(DeployContext context, String businessKey, + Map variables, String remark) { + // 构造工作流启动请求 WorkflowInstanceStartRequest workflowRequest = new WorkflowInstanceStartRequest(); - workflowRequest.setProcessKey(workflowDefinition.getKey()); + workflowRequest.setProcessKey(context.workflowDefinition.getKey()); workflowRequest.setBusinessKey(businessKey); workflowRequest.setVariables(variables); - - // 8. 启动工作流 + + // 启动工作流 WorkflowInstanceDTO workflowInstance = workflowInstanceService.startWorkflow(workflowRequest); + + log.info("部署流程已启动: businessKey={}, workflowInstanceId={}, application={}, environment={}", + businessKey, workflowInstance.getId(), context.application.getAppCode(), context.environment.getEnvCode()); + + // 创建部署记录 + deployRecordService.createDeployRecord( + workflowInstance.getId(), + businessKey, + context.teamApp.getId(), + context.teamApp.getTeamId(), + context.teamApp.getApplicationId(), + context.teamApp.getEnvironmentId(), + SecurityUtils.getCurrentUsername(), + remark + ); + + return workflowInstance; + } - log.info("部署流程已启动: businessKey={}, workflowInstanceId={}, application={}, environment={}", businessKey, workflowInstance.getId(), application.getAppCode(), environment.getEnvCode()); - - // 9. 创建部署记录(此时已有实例ID) - deployRecordService.createDeployRecord(workflowInstance.getId(), businessKey, teamApp.getId(), teamApp.getTeamId(), teamApp.getApplicationId(), teamApp.getEnvironmentId(), SecurityUtils.getCurrentUsername(), request.getRemark()); - - // 10. 返回结果 + /** + * 构建部署结果DTO + */ + private DeployResultDTO buildDeployResult(WorkflowInstanceDTO workflowInstance, String businessKey, DeployContext context) { DeployResultDTO result = new DeployResultDTO(); result.setWorkflowInstanceId(workflowInstance.getId()); result.setBusinessKey(businessKey); result.setProcessInstanceId(workflowInstance.getProcessInstanceId()); result.setStatus(workflowInstance.getStatus().name()); result.setMessage("部署流程已启动"); - + + log.info("部署请求处理完成: businessKey={}, workflowInstanceId={}, application={}, environment={}", + businessKey, workflowInstance.getId(), context.application.getAppCode(), context.environment.getEnvCode()); + return result; } + + /** + * 部署上下文内部类 + */ + private static class DeployContext { + final TeamApplication teamApp; + final WorkflowDefinition workflowDefinition; + final Application application; + final Environment environment; + final TeamEnvironmentConfig teamEnvConfig; + + DeployContext(TeamApplication teamApp, WorkflowDefinition workflowDefinition, + Application application, Environment environment, TeamEnvironmentConfig teamEnvConfig) { + this.teamApp = teamApp; + this.workflowDefinition = workflowDefinition; + this.application = application; + this.environment = environment; + this.teamEnvConfig = teamEnvConfig; + } + } + + @Override + public List getMyApprovalTasks() { + // 1. 获取当前登录用户 + String currentUsername = SecurityUtils.getCurrentUsername(); + log.info("查询用户 {} 的部署审批任务", currentUsername); + + // 2. 查询用户的所有待办任务(模仿 ApprovalTaskService.getMyTasks) + List tasks = taskService.createTaskQuery() + .taskAssignee(currentUsername) + .orderByTaskCreateTime() + .desc() + .list(); + + if (tasks.isEmpty()) { + log.info("用户 {} 当前没有待办审批任务", currentUsername); + return Collections.emptyList(); + } + + // 3. 过滤出部署相关的任务并转换为DTO + List result = new ArrayList<>(); + for (Task task : tasks) { + try { + DeployApprovalTaskDTO dto = convertToDeployApprovalDTO(task); + if (dto != null) { + result.add(dto); + } + } catch (Exception e) { + log.warn("转换审批任务失败: taskId={}, error={}", task.getId(), e.getMessage()); + } + } + + log.info("用户 {} 共有 {} 个部署审批任务", currentUsername, result.size()); + return result; + } + + /** + * 转换 Flowable Task 为部署审批 DTO + *

模仿 ApprovalTaskService.convertToDTO 方法 + * + * @param task Flowable 任务 + * @return 部署审批DTO,如果不是部署任务则返回 null + */ + private DeployApprovalTaskDTO convertToDeployApprovalDTO(Task task) { + // 1. 获取流程变量 + Map variables = taskService.getVariables(task.getId()); + if (variables == null || variables.isEmpty()) { + log.debug("任务 {} 没有流程变量,跳过", task.getId()); + return null; + } + + // 2. 检查是否为部署流程(通过 deploy 变量判断) + @SuppressWarnings("unchecked") + Map deployContext = (Map) variables.get("deploy"); + + if (deployContext == null) { + log.debug("任务 {} 不包含部署上下文,跳过", task.getId()); + return null; + } + + // 3. 获取 BusinessKey(需要通过 RuntimeService 查询) + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() + .processInstanceId(task.getProcessInstanceId()) + .singleResult(); + + if (processInstance == null) { + log.debug("流程实例 {} 已结束,跳过", task.getProcessInstanceId()); + return null; + } + + String businessKey = processInstance.getBusinessKey(); + if (businessKey == null) { + log.debug("任务 {} 的流程实例没有 businessKey,跳过", task.getId()); + return null; + } + + // 4. 查询部署记录 + Optional deployRecordOpt = deployRecordRepository + .findByBusinessKeyAndDeletedFalse(businessKey); + + if (deployRecordOpt.isEmpty()) { + log.warn("找不到业务标识为 {} 的部署记录", businessKey); + return null; + } + + DeployRecord deployRecord = deployRecordOpt.get(); + + // 5. 构建 DTO + DeployApprovalTaskDTO dto = new DeployApprovalTaskDTO(); + + // 5.1 审批任务基本信息 + dto.setTaskId(task.getId()); + dto.setTaskName(task.getName()); + dto.setTaskDescription(task.getDescription()); + dto.setProcessInstanceId(task.getProcessInstanceId()); + dto.setProcessDefinitionId(task.getProcessDefinitionId()); + dto.setAssignee(task.getAssignee()); + + if (task.getCreateTime() != null) { + dto.setCreateTime(LocalDateTime.ofInstant( + task.getCreateTime().toInstant(), + ZoneId.systemDefault())); + } + + if (task.getDueDate() != null) { + dto.setDueDate(LocalDateTime.ofInstant( + task.getDueDate().toInstant(), + ZoneId.systemDefault())); + } + + // 5.2 审批配置信息(从流程变量获取) + dto.setApprovalTitle((String) variables.get("approvalTitle")); + dto.setApprovalContent((String) variables.get("approvalContent")); + dto.setApprovalMode((String) variables.get("approvalMode")); + dto.setAllowDelegate((Boolean) variables.get("allowDelegate")); + dto.setAllowAddSign((Boolean) variables.get("allowAddSign")); + dto.setRequireComment((Boolean) variables.get("requireComment")); + + // 5.3 部署业务上下文信息 + dto.setDeployRecordId(deployRecord.getId()); + dto.setBusinessKey(businessKey); + dto.setTeamId(getLongValue(deployContext, "teamId")); + dto.setApplicationId(getLongValue(deployContext, "applicationId")); + dto.setApplicationCode((String) deployContext.get("applicationCode")); + dto.setApplicationName((String) deployContext.get("applicationName")); + dto.setEnvironmentId(getLongValue(deployContext, "environmentId")); + dto.setEnvironmentCode((String) deployContext.get("environmentCode")); + dto.setEnvironmentName((String) deployContext.get("environmentName")); + dto.setDeployBy((String) deployContext.get("by")); + dto.setDeployRemark((String) deployContext.get("remark")); + dto.setDeployStartTime(deployRecord.getStartTime()); + + // 5.4 查询团队名称 + Long teamId = dto.getTeamId(); + if (teamId != null) { + teamRepository.findById(teamId).ifPresent(team -> + dto.setTeamName(team.getTeamName()) + ); + } + + // 5.5 计算待审批时长 + if (dto.getCreateTime() != null) { + long pendingMillis = Duration.between(dto.getCreateTime(), LocalDateTime.now()).toMillis(); + dto.setPendingDuration(pendingMillis); + } + + // 5.6 自动解析 EL 表达式(如 ${deploy.applicationName}) + SpelExpressionResolver.resolveObject(dto, variables); + + return dto; + } + + /** + * 从 Map 中安全获取 Long 值 + */ + private Long getLongValue(Map map, String key) { + Object value = map.get(key); + if (value == null) { + return null; + } + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Integer) { + return ((Integer) value).longValue(); + } + if (value instanceof String) { + try { + return Long.parseLong((String) value); + } catch (NumberFormatException e) { + log.warn("无法将 {} 转换为 Long: {}", key, value); + return null; + } + } + return null; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeApproval(DeployApprovalCompleteRequest request) { + String taskId = request.getTaskId(); + log.info("开始处理部署审批: taskId={}, result={}", taskId, request.getResult()); + + try { + // 1. 验证任务是否存在 + Task task = taskService.createTaskQuery() + .taskId(taskId) + .singleResult(); + + if (task == null) { + log.error("审批任务不存在: taskId={}", taskId); + throw new BusinessException(ResponseCode.DATA_NOT_FOUND); + } + + // 2. 获取当前用户 + String currentUsername = SecurityUtils.getCurrentUsername(); + log.debug("审批人: {}, 任务负责人: {}", currentUsername, task.getAssignee()); + + // 3. 验证审批权限 + if (!currentUsername.equals(task.getAssignee())) { + log.error("无权审批该任务: taskId={}, 当前用户={}, 任务负责人={}", + taskId, currentUsername, task.getAssignee()); + throw new BusinessException(ResponseCode.PERMISSION_DENIED); + } + + // 4. 获取节点ID并更新 NodeContext(与 ApprovalTaskService 保持一致) + String nodeId = task.getTaskDefinitionKey(); + + // 5. 读取现有 NodeContext + Object nodeDataObj = taskService.getVariable(task.getId(), nodeId); + NodeContext nodeContext; + if (nodeDataObj instanceof Map) { + @SuppressWarnings("unchecked") + Map nodeDataMap = (Map) nodeDataObj; + nodeContext = NodeContext.fromMap(nodeDataMap, + ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper); + } else { + nodeContext = new NodeContext<>(); + } + + // 6. 创建审批结果(只设置核心字段,其他由 ApprovalEndExecutionListener 自动装配) + ApprovalOutputs outputs = new ApprovalOutputs(); + outputs.setApprovalResult(request.getResult()); + outputs.setApprover(currentUsername); + outputs.setApprovalTime(LocalDateTime.now()); + outputs.setApprovalComment(request.getComment()); + // ✅ 不需要手动设置 status 和 approvalDuration,监听器会自动装配 + + // 7. 设置到 NodeContext + nodeContext.setOutputs(outputs); + + // 8. 保存回流程变量 + taskService.setVariable(task.getId(), nodeId, nodeContext.toMap(objectMapper)); + + // 9. 添加任务评论(供历史查询) + if (request.getComment() != null) { + taskService.addComment(taskId, task.getProcessInstanceId(), request.getComment()); + } + + // 10. 完成任务(触发 ApprovalEndExecutionListener,监听器会自动装配其他字段) + taskService.complete(taskId); + + log.info("部署审批已提交,后续节点将异步执行: taskId={}, result={}, comment={}", + taskId, request.getResult(), request.getComment()); + + } catch (BusinessException e) { + log.error("审批失败: taskId={}, error={}", taskId, e.getMessage()); + throw e; + } catch (Exception e) { + log.error("审批处理异常: taskId={}", taskId, e); + throw new BusinessException(ResponseCode.ERROR); + } + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index bb6cddce..40c4ad24 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -206,7 +206,22 @@ public enum ResponseCode { SERVER_IP_EXISTS(2951, "server.ip.exists"), SERVER_CATEGORY_NOT_FOUND(2952, "server.category.not.found"), SERVER_CATEGORY_CODE_EXISTS(2953, "server.category.code.exists"), - SERVER_CATEGORY_HAS_SERVERS(2954, "server.category.has.servers"); + SERVER_CATEGORY_HAS_SERVERS(2954, "server.category.has.servers"), + + // 部署相关错误码 (3000-3099) + DEPLOY_CONFIG_NOT_FOUND(3000, "deploy.config.not.found"), + DEPLOY_APPROVAL_CONFIG_MISSING(3001, "deploy.approval.config.missing"), + DEPLOY_APPROVER_NOT_CONFIGURED(3002, "deploy.approver.not.configured"), + DEPLOY_NOTIFICATION_CONFIG_MISSING(3003, "deploy.notification.config.missing"), + DEPLOY_JENKINS_CONFIG_MISSING(3004, "deploy.jenkins.config.missing"), + DEPLOY_WORKFLOW_NOT_CONFIGURED(3005, "deploy.workflow.not.configured"), + DEPLOY_ENVIRONMENT_CONFIG_MISSING(3006, "deploy.environment.config.missing"), + DEPLOY_APPLICATION_NOT_CONFIGURED(3007, "deploy.application.not.configured"), + DEPLOY_ALREADY_RUNNING(3008, "deploy.already.running"), + DEPLOY_PERMISSION_DENIED(3009, "deploy.permission.denied"), + DEPLOY_ENVIRONMENT_LOCKED(3010, "deploy.environment.locked"), + DEPLOY_APPROVAL_REQUIRED(3011, "deploy.approval.required"), + DEPLOY_RECORD_NOT_FOUND(3012, "deploy.record.not.found"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java index b4befdb5..62c2624d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/service/impl/NotificationChannelServiceImpl.java @@ -76,7 +76,7 @@ public class NotificationChannelServiceImpl try { adapter.testConnection(channel.getConfig()); log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName()); - return true; + return true; } catch (Exception e) { log.error("通知渠道连接测试失败: id={}, name={}, 错误: {}", id, channel.getName(), e.getMessage(), e); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java index 4485926c..4cb96982 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java @@ -10,7 +10,7 @@ import java.util.Map; @Data public class WorkflowNodeInstanceDTO extends BaseDTO { - private Long id; + // 注意:id, createTime, updateTime 继承自 BaseDTO private String processInstanceId; @@ -28,9 +28,17 @@ public class WorkflowNodeInstanceDTO extends BaseDTO { private LocalDateTime endTime; - private LocalDateTime createTime; + /** + * 执行时长(毫秒) + * 如果节点未执行或正在执行,则为null + */ + private Long duration; - private LocalDateTime updateTime; + /** + * 错误信息 + * 当节点执行失败时,记录失败原因 + */ + private String errorMessage; /** * 节点执行结果(outputs) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java index 085e56ef..2ec1f400 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java @@ -396,6 +396,10 @@ public class BpmnConverter { serviceTask.setImplementation(delegateExpression); } + // ✅ 设置异步执行(关键修复:防止阻塞) + serviceTask.setAsynchronous(true); + log.debug("节点 {} 已配置为异步执行", validId); + // ✅ 添加 field 字段(nodeId, configs, inputMapping) addExecutionVariables(extensionElements, node, validId); diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 0b0c0eb1..31f7526e 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -229,3 +229,18 @@ schedule.job.executor.not.found=找不到任务执行器:{0} # 团队配置相关错误消息 (2920-2949) team.config.not.found=团队ID为 {0} 的配置不存在 + +# 部署相关错误码 (3000-3099) +deploy.config.not.found=部署配置不存在 +deploy.approval.config.missing=未配置审批策略,无法执行部署 +deploy.approver.not.configured=该环境需要审批,但未配置审批人 +deploy.notification.config.missing=未配置通知渠道,无法发送部署通知 +deploy.jenkins.config.missing=未配置Jenkins构建任务,无法执行部署 +deploy.workflow.not.configured=应用未配置部署工作流 +deploy.environment.config.missing=团队环境配置不存在:teamId={0}, environmentId={1} +deploy.application.not.configured=应用未配置到此环境 +deploy.already.running=该应用正在部署中,请等待当前部署完成 +deploy.permission.denied=无权限在此环境部署应用 +deploy.environment.locked=环境已锁定,禁止部署 +deploy.approval.required=该环境需要审批才能部署 +deploy.record.not.found=部署记录不存在 \ No newline at end of file diff --git a/backend/src/main/resources/messages_en_US.properties b/backend/src/main/resources/messages_en_US.properties index 3d146169..dcb6fa6c 100644 --- a/backend/src/main/resources/messages_en_US.properties +++ b/backend/src/main/resources/messages_en_US.properties @@ -170,3 +170,18 @@ form.definition.not.found=Form definition not found or has been deleted form.data.not.found=Form data not found or has been deleted form.definition.key.exists=Form key {0} already exists, please use a different key form.definition.key.version.exists=Form key {0} version {1} already exists + +# Deploy related error codes (3000-3099) +deploy.config.not.found=Deployment configuration not found +deploy.approval.config.missing=Approval policy not configured, cannot execute deployment +deploy.approver.not.configured=Approval required for this environment, but no approvers configured +deploy.notification.config.missing=Notification channel not configured, cannot send deployment notifications +deploy.jenkins.config.missing=Jenkins build job not configured, cannot execute deployment +deploy.workflow.not.configured=Application deployment workflow not configured +deploy.environment.config.missing=Team environment configuration not found: teamId={0}, environmentId={1} +deploy.application.not.configured=Application not configured for this environment +deploy.already.running=Application deployment already in progress, please wait +deploy.permission.denied=No permission to deploy application in this environment +deploy.environment.locked=Environment is locked, deployment prohibited +deploy.approval.required=Approval required for deployment in this environment +deploy.record.not.found=Deployment record not found diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties index 3e4e84bf..05507ad8 100644 --- a/backend/src/main/resources/messages_zh_CN.properties +++ b/backend/src/main/resources/messages_zh_CN.properties @@ -170,3 +170,18 @@ form.definition.not.found=表单定义不存在或已删除 form.data.not.found=表单数据不存在或已删除 form.definition.key.exists=表单标识{0}已存在,请使用不同的标识 form.definition.key.version.exists=表单标识{0}的版本{1}已存在 + +# 部署相关错误码 (3000-3099) +deploy.config.not.found=部署配置不存在 +deploy.approval.config.missing=未配置审批策略,无法执行部署 +deploy.approver.not.configured=该环境需要审批,但未配置审批人 +deploy.notification.config.missing=未配置通知渠道,无法发送部署通知 +deploy.jenkins.config.missing=未配置Jenkins构建任务,无法执行部署 +deploy.workflow.not.configured=应用未配置部署工作流 +deploy.environment.config.missing=团队环境配置不存在:teamId={0}, environmentId={1} +deploy.application.not.configured=应用未配置到此环境 +deploy.already.running=该应用正在部署中,请等待当前部署完成 +deploy.permission.denied=无权限在此环境部署应用 +deploy.environment.locked=环境已锁定,禁止部署 +deploy.approval.required=该环境需要审批才能部署 +deploy.record.not.found=部署记录不存在 \ No newline at end of file