打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-05 16:46:01 +08:00
parent d0997f9e9f
commit 3f102ae1b2
15 changed files with 1005 additions and 139 deletions

View File

@ -1,9 +1,6 @@
package com.qqchen.deploy.backend.deploy.api; package com.qqchen.deploy.backend.deploy.api;
import com.qqchen.deploy.backend.deploy.dto.DeployRequestDTO; import com.qqchen.deploy.backend.deploy.dto.*;
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.service.IDeployRecordService; import com.qqchen.deploy.backend.deploy.service.IDeployRecordService;
import com.qqchen.deploy.backend.deploy.service.IDeployService; import com.qqchen.deploy.backend.deploy.service.IDeployService;
import com.qqchen.deploy.backend.framework.api.Response; 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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 部署管理API控制器 * 部署管理API控制器
@ -67,5 +66,26 @@ public class DeployApiController {
) { ) {
return Response.success(deployRecordService.getDeployFlowGraph(deployRecordId)); return Response.success(deployRecordService.getDeployFlowGraph(deployRecordId));
} }
/**
* 获取当前用户的部署审批任务列表
*/
@Operation(summary = "获取我的部署审批任务", description = "查询当前登录用户待审批的部署任务,包含完整的部署业务上下文信息")
@GetMapping("/my-approval-tasks")
@PreAuthorize("isAuthenticated()")
public Response<List<DeployApprovalTaskDTO>> getMyApprovalTasks() {
return Response.success(deployService.getMyApprovalTasks());
}
/**
* 完成部署审批
*/
@Operation(summary = "完成部署审批", description = "完成部署审批任务后续节点Jenkins构建、通知等将由Flowable异步执行")
@PostMapping("/complete")
@PreAuthorize("isAuthenticated()")
public Response<Void> completeApproval(@Validated @RequestBody DeployApprovalCompleteRequest request) {
deployService.completeApproval(request);
return Response.success();
}
} }

View File

@ -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;
/**
* 部署审批完成请求
*
* <p>用于审批部署任务支持异步处理
*
* @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;
}

View File

@ -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
*
* <p>扩展了审批任务的基本信息增加部署相关的业务上下文
* <p>用于部署审批列表让审批人员快速了解审批内容
*
* @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;
}

View File

@ -3,9 +3,11 @@ package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums;
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeInstanceDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph; 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
@ -19,22 +21,89 @@ import java.util.List;
@Schema(description = "部署记录流程图信息") @Schema(description = "部署记录流程图信息")
public class DeployRecordFlowGraphDTO { public class DeployRecordFlowGraphDTO {
// ============ 部署记录基本信息 ============
@Schema(description = "部署记录ID") @Schema(description = "部署记录ID")
private Long deployRecordId; 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") @Schema(description = "工作流实例ID")
private Long workflowInstanceId; private Long workflowInstanceId;
@Schema(description = "流程实例IDFlowable") @Schema(description = "流程实例IDFlowable")
private String processInstanceId; private String processInstanceId;
@Schema(description = "部署状态") @Schema(description = "工作流实例状态")
private DeployRecordStatusEnums deployStatus; 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 = "流程图数据(画布快照,包含节点和边的位置信息)") @Schema(description = "流程图数据(画布快照,包含节点和边的位置信息)")
private WorkflowDefinitionGraph graph; private WorkflowDefinitionGraph graph;
@Schema(description = "节点执行状态列表(用于标记每个节点的执行状态)") @Schema(description = "节点执行状态列表(用于标记每个节点的执行状态,包含已执行和未执行节点")
private List<WorkflowNodeInstanceDTO> nodeInstances; private List<WorkflowNodeInstanceDTO> nodeInstances;
} }

View File

@ -25,6 +25,11 @@ public interface IDeployRecordRepository extends IBaseRepository<DeployRecord, L
*/ */
Optional<DeployRecord> findByWorkflowInstanceIdAndDeletedFalse(Long workflowInstanceId); Optional<DeployRecord> findByWorkflowInstanceIdAndDeletedFalse(Long workflowInstanceId);
/**
* 根据业务标识查询部署记录
*/
Optional<DeployRecord> findByBusinessKeyAndDeletedFalse(String businessKey);
/** /**
* 根据团队应用ID查询最新部署记录 * 根据团队应用ID查询最新部署记录
*/ */

View File

@ -1,8 +1,8 @@
package com.qqchen.deploy.backend.deploy.service; package com.qqchen.deploy.backend.deploy.service;
import com.qqchen.deploy.backend.deploy.dto.DeployRequestDTO; import com.qqchen.deploy.backend.deploy.dto.*;
import com.qqchen.deploy.backend.deploy.dto.DeployResultDTO;
import com.qqchen.deploy.backend.deploy.dto.UserDeployableDTO; import java.util.List;
/** /**
* 部署服务接口 * 部署服务接口
@ -26,5 +26,22 @@ public interface IDeployService {
* @return 部署结果 * @return 部署结果
*/ */
DeployResultDTO executeDeploy(DeployRequestDTO request); DeployResultDTO executeDeploy(DeployRequestDTO request);
/**
* 获取当前用户的部署审批任务列表
* <p>查询当前登录用户待审批的部署任务包含完整的部署业务上下文信息
*
* @return 部署审批任务列表
*/
List<DeployApprovalTaskDTO> getMyApprovalTasks();
/**
* 完成部署审批
* <p>完成部署审批任务支持通过/拒绝两种结果
* <p>后续节点如Jenkins构建通知将由Flowable异步执行不会阻塞HTTP请求
*
* @param request 审批完成请求
*/
void completeApproval(DeployApprovalCompleteRequest request);
} }

View File

@ -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.DeployRecordDTO;
import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO; import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO;
import com.qqchen.deploy.backend.deploy.entity.DeployRecord; 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.enums.DeployRecordStatusEnums;
import com.qqchen.deploy.backend.deploy.query.DeployRecordQuery; import com.qqchen.deploy.backend.deploy.query.DeployRecordQuery;
import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository; 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.deploy.service.IDeployRecordService;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; 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.dto.WorkflowNodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeInstance; 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.BpmnModel;
import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.Process;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService; 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 jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -60,14 +70,17 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
@Resource @Resource
private IWorkflowNodeInstanceRepository workflowNodeInstanceRepository; private IWorkflowNodeInstanceRepository workflowNodeInstanceRepository;
@Resource
private WorkflowNodeInstanceConverter workflowNodeInstanceConverter;
@Resource @Resource
private RepositoryService repositoryService; private RepositoryService repositoryService;
@Resource @Resource
private HistoryService historyService; private ITeamRepository teamRepository;
@Resource
private IApplicationRepository applicationRepository;
@Resource
private IEnvironmentRepository environmentRepository;
@Override @Override
@Transactional @Transactional
@ -217,110 +230,218 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
} }
@Override @Override
@Transactional(readOnly = true)
public DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId) { public DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId) {
// 1. 查询部署记录 log.info("查询部署流程图: deployRecordId={}", deployRecordId);
// ============ 1. 查询基础数据 ============
// 1.1 查询部署记录
DeployRecord deployRecord = deployRecordRepository.findById(deployRecordId) DeployRecord deployRecord = deployRecordRepository.findById(deployRecordId)
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"部署记录"})); .orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"部署记录"}));
// 2. 查询工作流实例包含流程图快照 // 1.2 查询工作流实例
WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId()) WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId())
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"工作流实例"})); .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<WorkflowNodeInstance> nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId()); List<WorkflowNodeInstance> nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId());
// 4. 从BPMN模型中获取流程元素顺序用于排序 // 2.2 构建已执行节点的映射按nodeId索引
Map<String, WorkflowNodeInstance> nodeInstanceMap = nodeInstances.stream()
.collect(Collectors.toMap(WorkflowNodeInstance::getNodeId, instance -> instance, (a, b) -> a));
// 2.3 从BPMN模型中获取流程元素顺序用于排序
BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId()); BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId());
Process process = bpmnModel.getMainProcess(); Process process = bpmnModel.getMainProcess();
List<FlowElement> flowElements = FlowableUtils.sortFlowElements(process); List<FlowElement> flowElements = FlowableUtils.sortFlowElements(process);
// 5. 构建节点ID到流程顺序的映射用于排序 // 2.4 构建节点ID到流程顺序的映射
Map<String, Integer> nodeOrderMap = new HashMap<>(); Map<String, Integer> nodeOrderMap = new HashMap<>();
for (int i = 0; i < flowElements.size(); i++) { for (int i = 0; i < flowElements.size(); i++) {
nodeOrderMap.put(flowElements.get(i).getId(), i); nodeOrderMap.put(flowElements.get(i).getId(), i);
} }
// 6. 按流程顺序排序节点实例只包含实际执行过的节点 // 2.5 遍历流程图中的所有节点组装节点DTO包括已执行和未执行的
List<WorkflowNodeInstance> orderedNodeInstances = nodeInstances.stream() List<WorkflowNodeInstanceDTO> nodeInstanceDTOs = graphSnapshot.getNodes().stream()
.sorted((a, b) -> { .sorted(Comparator.comparingInt(node -> nodeOrderMap.getOrDefault(node.getId(), Integer.MAX_VALUE)))
Integer orderA = nodeOrderMap.getOrDefault(a.getNodeId(), Integer.MAX_VALUE); .map(graphNode -> buildNodeInstanceDTO(graphNode, nodeInstanceMap.get(graphNode.getId()), workflowInstance))
Integer orderB = nodeOrderMap.getOrDefault(b.getNodeId(), Integer.MAX_VALUE);
return orderA.compareTo(orderB);
})
.collect(Collectors.toList()); .collect(Collectors.toList());
// 7. 从历史流程变量中获取每个节点的 outputs 数据 // ============ 3. 计算执行统计 ============
Map<String, Map<String, Object>> nodeOutputsMap = getNodeOutputsFromHistory(workflowInstance.getProcessInstanceId());
// 8. 转换为 DTO 并填充 outputs 数据 int totalNodeCount = nodeInstanceDTOs.size();
List<WorkflowNodeInstanceDTO> nodeInstanceDTOs = workflowNodeInstanceConverter.toDtoList(orderedNodeInstances); int executedNodeCount = (int) nodeInstanceDTOs.stream()
nodeInstanceDTOs.forEach(dto -> { .filter(node -> node.getStatus() != WorkflowNodeInstanceStatusEnums.NOT_STARTED)
// 从流程变量中获取该节点的 outputs 数据 .count();
Map<String, Object> nodeOutputs = nodeOutputsMap.get(dto.getNodeId()); int successNodeCount = (int) nodeInstanceDTOs.stream()
if (nodeOutputs != null) { .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.COMPLETED)
// 提取 outputs 部分格式{nodeId: {outputs: {...}}} .count();
@SuppressWarnings("unchecked") int failedNodeCount = (int) nodeInstanceDTOs.stream()
Map<String, Object> nodeData = (Map<String, Object>) nodeOutputs; .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.FAILED)
@SuppressWarnings("unchecked") .count();
Map<String, Object> outputs = (Map<String, Object>) nodeData.get("outputs"); int runningNodeCount = (int) nodeInstanceDTOs.stream()
if (outputs != null) { .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.RUNNING)
dto.setOutputs(outputs); .count();
}
} // ============ 4. 组装完整的DTO ============
});
// 9. 组装DTO
DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO(); DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO();
// 4.1 部署记录基本信息
dto.setDeployRecordId(deployRecord.getId()); 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.setWorkflowInstanceId(workflowInstance.getId());
dto.setProcessInstanceId(workflowInstance.getProcessInstanceId()); dto.setProcessInstanceId(workflowInstance.getProcessInstanceId());
dto.setDeployStatus(deployRecord.getStatus()); dto.setWorkflowStatus(workflowInstance.getStatus());
dto.setGraph(workflowInstance.getGraphSnapshot()); // 流程图结构数据 dto.setWorkflowStartTime(workflowInstance.getStartTime());
dto.setNodeInstances(nodeInstanceDTOs); // 节点执行状态包含 outputs 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; return dto;
} }
/** /**
* 从历史流程变量中获取所有节点的 outputs 数据 * 构建节点实例DTO
* *
* @param processInstanceId 流程实例ID * @param graphNode 流程图节点
* @return 节点ID到节点数据的映射格式{nodeId: {outputs: {...}}} * @param nodeInstance 节点实例可能为null表示未执行
* @param workflowInstance 工作流实例
* @return 节点实例DTO
*/ */
private Map<String, Map<String, Object>> getNodeOutputsFromHistory(String processInstanceId) { private WorkflowNodeInstanceDTO buildNodeInstanceDTO(
Map<String, Map<String, Object>> nodeOutputsMap = new HashMap<>(); WorkflowDefinitionGraphNode graphNode,
WorkflowNodeInstance nodeInstance,
WorkflowInstance workflowInstance) {
try { WorkflowNodeInstanceDTO dto = new WorkflowNodeInstanceDTO();
// 查询历史流程变量
List<HistoricVariableInstance> variables = historyService
.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.list();
// 遍历变量查找节点相关的变量格式{nodeId: {outputs: {...}}} if (nodeInstance != null) {
for (HistoricVariableInstance variable : variables) { // 已执行的节点使用节点实例的数据
String variableName = variable.getVariableName(); dto.setId(nodeInstance.getId());
Object variableValue = variable.getValue(); dto.setNodeId(nodeInstance.getNodeId());
dto.setNodeName(nodeInstance.getNodeName());
// 检查是否是节点数据节点ID通常是 sid_ 开头 dto.setNodeType(nodeInstance.getNodeType());
if (variableName != null && variableValue instanceof Map) { dto.setStatus(nodeInstance.getStatus());
@SuppressWarnings("unchecked") dto.setStartTime(nodeInstance.getStartTime());
Map<String, Object> nodeData = (Map<String, Object>) variableValue; dto.setEndTime(nodeInstance.getEndTime());
dto.setDuration(calculateDuration(nodeInstance.getStartTime(), nodeInstance.getEndTime()));
// 检查是否包含 outputs 字段这是节点数据的标识 dto.setErrorMessage(nodeInstance.getErrorMessage()); // 包含错误信息
if (nodeData.containsKey("outputs")) { dto.setProcessInstanceId(nodeInstance.getProcessInstanceId());
nodeOutputsMap.put(variableName, nodeData); dto.setCreateTime(nodeInstance.getCreateTime());
} dto.setUpdateTime(nodeInstance.getUpdateTime());
} } else {
} // 未执行的节点从流程图节点获取基本信息
dto.setId(null);
log.debug("从历史流程变量中获取节点 outputs: processInstanceId={}, nodeCount={}", dto.setNodeId(graphNode.getId());
processInstanceId, nodeOutputsMap.size()); dto.setNodeName(graphNode.getNodeName());
} catch (Exception e) { dto.setNodeType(graphNode.getNodeType() != null ? graphNode.getNodeType().name() : null);
log.warn("获取节点 outputs 失败: processInstanceId={}", processInstanceId, e); 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 nodeOutputsMap; return dto;
}
/**
* 计算时长毫秒
*
* @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;
} }
/** /**

View File

@ -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.security.SecurityUtils;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; 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.entity.User;
import com.qqchen.deploy.backend.system.repository.IUserRepository; import com.qqchen.deploy.backend.system.repository.IUserRepository;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceStartRequest; 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.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.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.model.NodeContext;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService; import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
import com.qqchen.deploy.backend.deploy.service.IDeployRecordService; 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 jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -82,6 +92,12 @@ public class DeployServiceImpl implements IDeployService {
@Resource @Resource
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@Resource
private TaskService taskService;
@Resource
private RuntimeService runtimeService;
/** /**
* 数据组装上下文封装所有需要的Map避免方法参数过多 * 数据组装上下文封装所有需要的Map避免方法参数过多
*/ */
@ -551,69 +567,198 @@ public class DeployServiceImpl implements IDeployService {
@Override @Override
@Transactional @Transactional
public DeployResultDTO executeDeploy(DeployRequestDTO request) { public DeployResultDTO executeDeploy(DeployRequestDTO request) {
// 1. 查询团队应用配置 log.info("开始执行部署: teamApplicationId={}, remark={}", request.getTeamApplicationId(), request.getRemark());
TeamApplication teamApp = teamApplicationRepository.findById(request.getTeamApplicationId()).orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND));
// 2. 查询工作流定义获取 processKey // 1. 加载部署上下文数据
WorkflowDefinition workflowDefinition = workflowDefinitionRepository.findById(teamApp.getWorkflowDefinitionId()).orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[] {"工作流定义"})); DeployContext context = loadDeployContext(request.getTeamApplicationId());
// 3. 查询应用信息 // 2. 生成业务标识UUID
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
String businessKey = UUID.randomUUID().toString(); String businessKey = UUID.randomUUID().toString();
// 6. 构造流程变量 // 3. 构造流程变量
Map<String, Object> variables = buildDeployVariables(context, request, businessKey);
// 4. 启动工作流并创建记录
WorkflowInstanceDTO workflowInstance = startWorkflowAndCreateRecord(context, businessKey, variables, request.getRemark());
// 5. 返回结果
return buildDeployResult(workflowInstance, businessKey, context);
}
/**
* 加载部署上下文数据
*/
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<String, Object> buildDeployVariables(DeployContext context, DeployRequestDTO request, String businessKey) {
Map<String, Object> variables = new HashMap<>(); Map<String, Object> variables = new HashMap<>();
// 部署上下文 // 部署上下文
Map<String, Object> deployContext = new HashMap<>(); variables.put("deploy", buildDeployContextMap(context, request));
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("by", SecurityUtils.getCurrentUsername());
deployContext.put("remark", request.getRemark());
variables.put("deploy", deployContext);
// Jenkins 配置使用强类型 JenkinsBuildInputMapping // Jenkins 配置
if (teamApp.getDeploySystemId() != null && teamApp.getDeployJob() != null) { Map<String, Object> jenkinsConfig = buildJenkinsConfig(context.teamApp);
JenkinsBuildInputMapping jenkinsInput = new JenkinsBuildInputMapping(); if (jenkinsConfig != null) {
jenkinsInput.setServerId(teamApp.getDeploySystemId()); variables.put("jenkins", jenkinsConfig);
jenkinsInput.setJobName(teamApp.getDeployJob());
if (teamApp.getBranch() != null) {
jenkinsInput.setBranch(teamApp.getBranch());
}
// 转换为 MapFlowable 只支持基本类型
variables.put("jenkins", objectMapper.convertValue(jenkinsInput, Map.class));
variables.put("approval", Map.of("required", true, "userIds", "admin"));
variables.put("notification", Map.of("channelId", 1));
} }
// 审批配置
variables.put("approval", buildApprovalConfig(context.teamEnvConfig));
// 7. 构造工作流启动请求 // 通知配置
variables.put("notification", buildNotificationConfig(context.teamEnvConfig));
return variables;
}
/**
* 构建部署上下文Map
*/
private Map<String, Object> buildDeployContextMap(DeployContext context, DeployRequestDTO request) {
Map<String, Object> deployContext = new HashMap<>();
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());
return deployContext;
}
/**
* 构建Jenkins配置
*/
private Map<String, Object> 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());
}
// 转换为 MapFlowable 只支持基本类型
return objectMapper.convertValue(jenkinsInput, Map.class);
}
/**
* 构建审批配置
*/
private Map<String, Object> buildApprovalConfig(TeamEnvironmentConfig teamEnvConfig) {
Map<String, Object> approvalConfig = new HashMap<>();
Boolean approvalRequired = teamEnvConfig.getApprovalRequired() != null ? teamEnvConfig.getApprovalRequired() : false;
approvalConfig.put("required", approvalRequired);
// 处理审批人ID列表转换为逗号分隔的用户名字符串
if (teamEnvConfig.getApproverUserIds() != null && !teamEnvConfig.getApproverUserIds().isEmpty()) {
List<User> 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;
}
/**
* 构建通知配置
*/
private Map<String, Object> buildNotificationConfig(TeamEnvironmentConfig teamEnvConfig) {
Map<String, Object> 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<String, Object> variables, String remark) {
// 构造工作流启动请求
WorkflowInstanceStartRequest workflowRequest = new WorkflowInstanceStartRequest(); WorkflowInstanceStartRequest workflowRequest = new WorkflowInstanceStartRequest();
workflowRequest.setProcessKey(workflowDefinition.getKey()); workflowRequest.setProcessKey(context.workflowDefinition.getKey());
workflowRequest.setBusinessKey(businessKey); workflowRequest.setBusinessKey(businessKey);
workflowRequest.setVariables(variables); workflowRequest.setVariables(variables);
// 8. 启动工作流 // 启动工作流
WorkflowInstanceDTO workflowInstance = workflowInstanceService.startWorkflow(workflowRequest); WorkflowInstanceDTO workflowInstance = workflowInstanceService.startWorkflow(workflowRequest);
log.info("部署流程已启动: businessKey={}, workflowInstanceId={}, application={}, environment={}", businessKey, workflowInstance.getId(), application.getAppCode(), environment.getEnvCode()); log.info("部署流程已启动: businessKey={}, workflowInstanceId={}, application={}, environment={}",
businessKey, workflowInstance.getId(), context.application.getAppCode(), context.environment.getEnvCode());
// 9. 创建部署记录此时已有实例ID // 创建部署记录
deployRecordService.createDeployRecord(workflowInstance.getId(), businessKey, teamApp.getId(), teamApp.getTeamId(), teamApp.getApplicationId(), teamApp.getEnvironmentId(), SecurityUtils.getCurrentUsername(), request.getRemark()); deployRecordService.createDeployRecord(
workflowInstance.getId(),
businessKey,
context.teamApp.getId(),
context.teamApp.getTeamId(),
context.teamApp.getApplicationId(),
context.teamApp.getEnvironmentId(),
SecurityUtils.getCurrentUsername(),
remark
);
// 10. 返回结果 return workflowInstance;
}
/**
* 构建部署结果DTO
*/
private DeployResultDTO buildDeployResult(WorkflowInstanceDTO workflowInstance, String businessKey, DeployContext context) {
DeployResultDTO result = new DeployResultDTO(); DeployResultDTO result = new DeployResultDTO();
result.setWorkflowInstanceId(workflowInstance.getId()); result.setWorkflowInstanceId(workflowInstance.getId());
result.setBusinessKey(businessKey); result.setBusinessKey(businessKey);
@ -621,7 +766,283 @@ public class DeployServiceImpl implements IDeployService {
result.setStatus(workflowInstance.getStatus().name()); result.setStatus(workflowInstance.getStatus().name());
result.setMessage("部署流程已启动"); result.setMessage("部署流程已启动");
log.info("部署请求处理完成: businessKey={}, workflowInstanceId={}, application={}, environment={}",
businessKey, workflowInstance.getId(), context.application.getAppCode(), context.environment.getEnvCode());
return result; 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<DeployApprovalTaskDTO> getMyApprovalTasks() {
// 1. 获取当前登录用户
String currentUsername = SecurityUtils.getCurrentUsername();
log.info("查询用户 {} 的部署审批任务", currentUsername);
// 2. 查询用户的所有待办任务模仿 ApprovalTaskService.getMyTasks
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(currentUsername)
.orderByTaskCreateTime()
.desc()
.list();
if (tasks.isEmpty()) {
log.info("用户 {} 当前没有待办审批任务", currentUsername);
return Collections.emptyList();
}
// 3. 过滤出部署相关的任务并转换为DTO
List<DeployApprovalTaskDTO> 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
* <p>模仿 ApprovalTaskService.convertToDTO 方法
*
* @param task Flowable 任务
* @return 部署审批DTO如果不是部署任务则返回 null
*/
private DeployApprovalTaskDTO convertToDeployApprovalDTO(Task task) {
// 1. 获取流程变量
Map<String, Object> variables = taskService.getVariables(task.getId());
if (variables == null || variables.isEmpty()) {
log.debug("任务 {} 没有流程变量,跳过", task.getId());
return null;
}
// 2. 检查是否为部署流程通过 deploy 变量判断
@SuppressWarnings("unchecked")
Map<String, Object> deployContext = (Map<String, Object>) 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<DeployRecord> 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<String, Object> 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<ApprovalInputMapping, ApprovalOutputs> nodeContext;
if (nodeDataObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nodeDataMap = (Map<String, Object>) 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);
}
}
} }

View File

@ -206,7 +206,22 @@ public enum ResponseCode {
SERVER_IP_EXISTS(2951, "server.ip.exists"), SERVER_IP_EXISTS(2951, "server.ip.exists"),
SERVER_CATEGORY_NOT_FOUND(2952, "server.category.not.found"), SERVER_CATEGORY_NOT_FOUND(2952, "server.category.not.found"),
SERVER_CATEGORY_CODE_EXISTS(2953, "server.category.code.exists"), 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 int code;
private final String messageKey; // 国际化消息key private final String messageKey; // 国际化消息key

View File

@ -76,7 +76,7 @@ public class NotificationChannelServiceImpl
try { try {
adapter.testConnection(channel.getConfig()); adapter.testConnection(channel.getConfig());
log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName()); log.info("通知渠道连接测试成功: id={}, name={}", id, channel.getName());
return true; return true;
} catch (Exception e) { } catch (Exception e) {
log.error("通知渠道连接测试失败: id={}, name={}, 错误: {}", log.error("通知渠道连接测试失败: id={}, name={}, 错误: {}",
id, channel.getName(), e.getMessage(), e); id, channel.getName(), e.getMessage(), e);

View File

@ -10,7 +10,7 @@ import java.util.Map;
@Data @Data
public class WorkflowNodeInstanceDTO extends BaseDTO { public class WorkflowNodeInstanceDTO extends BaseDTO {
private Long id; // 注意id, createTime, updateTime 继承自 BaseDTO
private String processInstanceId; private String processInstanceId;
@ -28,9 +28,17 @@ public class WorkflowNodeInstanceDTO extends BaseDTO {
private LocalDateTime endTime; private LocalDateTime endTime;
private LocalDateTime createTime; /**
* 执行时长毫秒
* 如果节点未执行或正在执行则为null
*/
private Long duration;
private LocalDateTime updateTime; /**
* 错误信息
* 当节点执行失败时记录失败原因
*/
private String errorMessage;
/** /**
* 节点执行结果outputs * 节点执行结果outputs

View File

@ -396,6 +396,10 @@ public class BpmnConverter {
serviceTask.setImplementation(delegateExpression); serviceTask.setImplementation(delegateExpression);
} }
// 设置异步执行关键修复防止阻塞
serviceTask.setAsynchronous(true);
log.debug("节点 {} 已配置为异步执行", validId);
// 添加 field 字段nodeId, configs, inputMapping // 添加 field 字段nodeId, configs, inputMapping
addExecutionVariables(extensionElements, node, validId); addExecutionVariables(extensionElements, node, validId);

View File

@ -229,3 +229,18 @@ schedule.job.executor.not.found=找不到任务执行器:{0}
# 团队配置相关错误消息 (2920-2949) # 团队配置相关错误消息 (2920-2949)
team.config.not.found=团队ID为 {0} 的配置不存在 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=部署记录不存在

View File

@ -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.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.exists=Form key {0} already exists, please use a different key
form.definition.key.version.exists=Form key {0} version {1} already exists 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

View File

@ -170,3 +170,18 @@ form.definition.not.found=表单定义不存在或已删除
form.data.not.found=表单数据不存在或已删除 form.data.not.found=表单数据不存在或已删除
form.definition.key.exists=表单标识{0}已存在,请使用不同的标识 form.definition.key.exists=表单标识{0}已存在,请使用不同的标识
form.definition.key.version.exists=表单标识{0}的版本{1}已存在 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=部署记录不存在