diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployRecordConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployRecordConverter.java new file mode 100644 index 00000000..c38de43f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployRecordConverter.java @@ -0,0 +1,17 @@ +package com.qqchen.deploy.backend.deploy.converter; + +import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO; +import com.qqchen.deploy.backend.deploy.entity.DeployRecord; +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import org.mapstruct.Mapper; + +/** + * 部署记录转换器 + * + * @author qqchen + * @since 2025-11-02 + */ +@Mapper(config = BaseConverter.class) +public interface DeployRecordConverter extends BaseConverter { +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordDTO.java new file mode 100644 index 00000000..c4227814 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordDTO.java @@ -0,0 +1,55 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 部署记录DTO + * + * @author qqchen + * @since 2025-11-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "部署记录信息") +public class DeployRecordDTO extends BaseDTO { + + @Schema(description = "工作流实例ID") + private Long workflowInstanceId; + + @Schema(description = "业务标识") + private String businessKey; + + @Schema(description = "团队应用配置ID") + private Long teamApplicationId; + + @Schema(description = "团队ID") + private Long teamId; + + @Schema(description = "应用ID") + private Long applicationId; + + @Schema(description = "环境ID") + private Long environmentId; + + @Schema(description = "部署人") + private String deployBy; + + @Schema(description = "部署备注") + private String deployRemark; + + @Schema(description = "部署状态") + private DeployRecordStatusEnums status; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordSummaryDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordSummaryDTO.java new file mode 100644 index 00000000..b2048162 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordSummaryDTO.java @@ -0,0 +1,40 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 部署记录摘要DTO + * + * @author qqchen + * @since 2025-11-02 + */ +@Data +@Schema(description = "部署记录摘要") +public class DeployRecordSummaryDTO { + + @Schema(description = "部署记录ID") + private Long id; + + @Schema(description = "部署状态") + private DeployRecordStatusEnums status; + + @Schema(description = "部署人") + private String deployBy; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; + + @Schema(description = "部署备注") + private String deployRemark; + + @Schema(description = "持续时间(毫秒)") + private Long duration; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployStatisticsDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployStatisticsDTO.java new file mode 100644 index 00000000..f87f09f2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployStatisticsDTO.java @@ -0,0 +1,40 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 部署统计信息DTO + * + * @author qqchen + * @since 2025-11-02 + */ +@Data +@Schema(description = "部署统计信息") +public class DeployStatisticsDTO { + + @Schema(description = "总部署次数") + private Long totalCount; + + @Schema(description = "成功次数") + private Long successCount; + + @Schema(description = "失败次数") + private Long failedCount; + + @Schema(description = "运行中次数") + private Long runningCount; + + @Schema(description = "最近部署时间") + private LocalDateTime lastDeployTime; + + @Schema(description = "最近部署人") + private String lastDeployBy; + + @Schema(description = "最新部署状态") + private DeployRecordStatusEnums latestStatus; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableApplicationDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableApplicationDTO.java index eeddcf27..8cb6985d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableApplicationDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableApplicationDTO.java @@ -1,8 +1,11 @@ package com.qqchen.deploy.backend.deploy.dto; +import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.List; + /** * 可部署应用DTO * @@ -48,5 +51,14 @@ public class DeployableApplicationDTO { @Schema(description = "工作流流程标识(processKey)") private String workflowDefinitionKey; + + @Schema(description = "部署统计信息") + private DeployStatisticsDTO deployStatistics; + + @Schema(description = "是否正在部署中") + private Boolean isDeploying; + + @Schema(description = "最近部署记录列表(最多10条)") + private List recentDeployRecords; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/DeployRecord.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/DeployRecord.java new file mode 100644 index 00000000..d29f50a3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/DeployRecord.java @@ -0,0 +1,95 @@ +package com.qqchen.deploy.backend.deploy.entity; + +import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 部署记录实体 + * + * @author qqchen + * @since 2025-11-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "deploy_record") +@LogicDelete +public class DeployRecord extends Entity { + + /** + * 工作流实例ID + */ + @Column(name = "workflow_instance_id", nullable = false) + private Long workflowInstanceId; + + /** + * 业务标识(UUID) + */ + @Column(name = "business_key", nullable = false, length = 64) + private String businessKey; + + /** + * 团队应用配置ID + */ + @Column(name = "team_application_id", nullable = false) + private Long teamApplicationId; + + /** + * 团队ID + */ + @Column(name = "team_id", nullable = false) + private Long teamId; + + /** + * 应用ID + */ + @Column(name = "application_id", nullable = false) + private Long applicationId; + + /** + * 环境ID + */ + @Column(name = "environment_id", nullable = false) + private Long environmentId; + + /** + * 部署人 + */ + @Column(name = "deploy_by", length = 50) + private String deployBy; + + /** + * 部署备注 + */ + @Column(name = "deploy_remark", length = 500) + private String deployRemark; + + /** + * 部署状态 + */ + @Column(name = "status", nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private DeployRecordStatusEnums status; + + /** + * 开始时间 + */ + @Column(name = "start_time") + private LocalDateTime startTime; + + /** + * 结束时间 + */ + @Column(name = "end_time") + private LocalDateTime endTime; +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java new file mode 100644 index 00000000..9a41cbd2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java @@ -0,0 +1,78 @@ +package com.qqchen.deploy.backend.deploy.enums; + +import lombok.Getter; + +/** + * 部署记录状态枚举 + * + * @author qqchen + * @since 2025-11-02 + */ +@Getter +public enum DeployRecordStatusEnums { + + /** + * 已创建 + */ + CREATED("CREATED", "已创建"), + + /** + * 运行中 + */ + RUNNING("RUNNING", "运行中"), + + /** + * 部署成功 + */ + SUCCESS("SUCCESS", "部署成功"), + + /** + * 部署失败 + */ + FAILED("FAILED", "部署失败"), + + /** + * 部分成功(工作流完成但存在失败的节点) + */ + PARTIAL_SUCCESS("PARTIAL_SUCCESS", "部分成功"), + + /** + * 已取消 + */ + CANCELLED("CANCELLED", "已取消"), + + /** + * 已终止 + */ + TERMINATED("TERMINATED", "已终止"), + + /** + * 已暂停 + */ + SUSPENDED("SUSPENDED", "已暂停"); + + private final String code; + private final String description; + + DeployRecordStatusEnums(String code, String description) { + this.code = code; + this.description = description; + } + + /** + * 将工作流状态转换为部署记录状态 + */ + public static DeployRecordStatusEnums fromWorkflowStatus(String workflowStatus) { + return switch (workflowStatus) { + case "CREATED" -> CREATED; + case "RUNNING" -> RUNNING; + case "COMPLETED" -> SUCCESS; + case "COMPLETED_WITH_ERRORS" -> PARTIAL_SUCCESS; + case "FAILED" -> FAILED; + case "TERMINATED" -> TERMINATED; + case "SUSPENDED" -> SUSPENDED; + default -> CREATED; + }; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/DeployRecordQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/DeployRecordQuery.java new file mode 100644 index 00000000..4e8591f6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/DeployRecordQuery.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.deploy.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 部署记录查询条件 + * + * @author qqchen + * @since 2025-11-02 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DeployRecordQuery extends BaseQuery { + + @QueryField(field = "teamApplicationId", type = QueryType.EQUAL) + private Long teamApplicationId; + + @QueryField(field = "applicationId", type = QueryType.EQUAL) + private Long applicationId; + + @QueryField(field = "environmentId", type = QueryType.EQUAL) + private Long environmentId; + + @QueryField(field = "teamId", type = QueryType.EQUAL) + private Long teamId; + + @QueryField(field = "status", type = QueryType.EQUAL) + private String status; + + @QueryField(field = "deployBy", type = QueryType.LIKE) + private String deployBy; +} + 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 new file mode 100644 index 00000000..c5d10b30 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IDeployRecordRepository.java @@ -0,0 +1,102 @@ +package com.qqchen.deploy.backend.deploy.repository; + +import com.qqchen.deploy.backend.deploy.entity.DeployRecord; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 部署记录仓库 + * + * @author qqchen + * @since 2025-11-02 + */ +@Repository +public interface IDeployRecordRepository extends IBaseRepository { + + /** + * 根据工作流实例ID查询部署记录 + */ + Optional findByWorkflowInstanceIdAndDeletedFalse(Long workflowInstanceId); + + /** + * 根据团队应用ID查询最新部署记录 + */ + Optional findFirstByTeamApplicationIdAndDeletedFalseOrderByCreateTimeDesc(Long teamApplicationId); + + /** + * 根据应用ID和环境ID查询最新部署记录 + */ + Optional findFirstByApplicationIdAndEnvironmentIdAndDeletedFalseOrderByCreateTimeDesc( + Long applicationId, Long environmentId); + + /** + * 根据团队应用ID分页查询部署记录 + */ + Page findByTeamApplicationIdAndDeletedFalseOrderByCreateTimeDesc( + Long teamApplicationId, Pageable pageable); + + /** + * 根据应用ID和环境ID查询部署记录列表 + */ + List findByApplicationIdAndEnvironmentIdAndDeletedFalseOrderByCreateTimeDesc( + Long applicationId, Long environmentId); + + /** + * 批量查询部署统计信息(按团队应用ID分组) + */ + @Query("SELECT dr.teamApplicationId, " + + "COUNT(dr.id) as totalCount, " + + "SUM(CASE WHEN dr.status = 'SUCCESS' THEN 1 ELSE 0 END) as successCount, " + + "SUM(CASE WHEN dr.status IN ('FAILED', 'CANCELLED', 'TERMINATED') THEN 1 ELSE 0 END) as failedCount, " + + "SUM(CASE WHEN dr.status IN ('CREATED', 'RUNNING') THEN 1 ELSE 0 END) as runningCount, " + + "MAX(dr.startTime) as lastDeployTime " + + "FROM DeployRecord dr " + + "WHERE dr.teamApplicationId IN :teamApplicationIds AND dr.deleted = false " + + "GROUP BY dr.teamApplicationId") + List findDeployStatisticsByTeamApplicationIds( + @Param("teamApplicationIds") List teamApplicationIds); + + /** + * 批量查询每个团队应用的最新部署记录(用于获取最新状态和部署人) + * 使用原生SQL避免JPQL类型问题 + */ + @Query(value = "SELECT dr.* FROM deploy_record dr " + + "INNER JOIN (" + + " SELECT team_application_id, MAX(id) as max_id " + + " FROM deploy_record " + + " WHERE team_application_id IN (:teamApplicationIds) " + + " AND deleted = false " + + " GROUP BY team_application_id" + + ") latest ON dr.id = latest.max_id " + + "WHERE dr.deleted = false " + + "ORDER BY dr.create_time DESC", + nativeQuery = true) + List findLatestDeployRecordsByTeamApplicationIds( + @Param("teamApplicationIds") List teamApplicationIds); + + /** + * 批量查询每个团队应用的最近N条部署记录 + * 使用原生SQL实现(MySQL支持窗口函数) + */ + @Query(value = "SELECT * FROM (" + + " SELECT dr.*, " + + " ROW_NUMBER() OVER (PARTITION BY dr.team_application_id ORDER BY dr.create_time DESC) as rn " + + " FROM deploy_record dr " + + " WHERE dr.team_application_id IN :teamApplicationIds " + + " AND dr.deleted = false" + + ") ranked " + + "WHERE ranked.rn <= :limit " + + "ORDER BY ranked.team_application_id, ranked.create_time DESC", + nativeQuery = true) + List findRecentDeployRecordsByTeamApplicationIds( + @Param("teamApplicationIds") List teamApplicationIds, + @Param("limit") int limit); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java new file mode 100644 index 00000000..d4f4031b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java @@ -0,0 +1,56 @@ +package com.qqchen.deploy.backend.deploy.service; + +import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO; +import com.qqchen.deploy.backend.deploy.entity.DeployRecord; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums; + +/** + * 部署记录服务接口 + * + * @author qqchen + * @since 2025-11-02 + */ +public interface IDeployRecordService { + + /** + * 创建部署记录 + * + * @param workflowInstanceId 工作流实例ID + * @param businessKey 业务标识 + * @param teamApplicationId 团队应用配置ID + * @param teamId 团队ID + * @param applicationId 应用ID + * @param environmentId 环境ID + * @param deployBy 部署人 + * @param deployRemark 部署备注 + * @return 部署记录DTO + */ + DeployRecordDTO createDeployRecord( + Long workflowInstanceId, + String businessKey, + Long teamApplicationId, + Long teamId, + Long applicationId, + Long environmentId, + String deployBy, + String deployRemark + ); + + /** + * 根据工作流实例同步部署记录状态 + * + * @param instance 工作流实例 + * @param status 工作流状态 + */ + void syncStatusFromWorkflowInstance(WorkflowInstance instance, WorkflowInstanceStatusEnums status); + + /** + * 根据工作流实例ID查询部署记录 + * + * @param workflowInstanceId 工作流实例ID + * @return 部署记录DTO + */ + DeployRecordDTO findByWorkflowInstanceId(Long workflowInstanceId); +} + 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 new file mode 100644 index 00000000..b97da6c1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java @@ -0,0 +1,116 @@ +package com.qqchen.deploy.backend.deploy.service.impl; + +import com.qqchen.deploy.backend.deploy.converter.DeployRecordConverter; +import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO; +import com.qqchen.deploy.backend.deploy.entity.DeployRecord; +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.service.IDeployRecordService; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance; +import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 部署记录服务实现 + * + * @author qqchen + * @since 2025-11-02 + */ +@Slf4j +@Service +public class DeployRecordServiceImpl extends BaseServiceImpl + implements IDeployRecordService { + + @Resource + private IDeployRecordRepository deployRecordRepository; + + @Resource + private DeployRecordConverter deployRecordConverter; + + @Override + @Transactional + public DeployRecordDTO createDeployRecord( + Long workflowInstanceId, + String businessKey, + Long teamApplicationId, + Long teamId, + Long applicationId, + Long environmentId, + String deployBy, + String deployRemark + ) { + // 检查是否已存在 + DeployRecord existing = deployRecordRepository + .findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId) + .orElse(null); + + if (existing != null) { + log.warn("部署记录已存在: workflowInstanceId={}", workflowInstanceId); + return deployRecordConverter.toDto(existing); + } + + // 创建新记录 + DeployRecord record = new DeployRecord(); + record.setWorkflowInstanceId(workflowInstanceId); + record.setBusinessKey(businessKey); + record.setTeamApplicationId(teamApplicationId); + record.setTeamId(teamId); + record.setApplicationId(applicationId); + record.setEnvironmentId(environmentId); + record.setDeployBy(deployBy); + record.setDeployRemark(deployRemark); + record.setStatus(DeployRecordStatusEnums.CREATED); + record.setStartTime(LocalDateTime.now()); + + DeployRecord saved = deployRecordRepository.save(record); + log.info("创建部署记录成功: id={}, workflowInstanceId={}, applicationId={}, environmentId={}", + saved.getId(), workflowInstanceId, applicationId, environmentId); + + return deployRecordConverter.toDto(saved); + } + + @Override + @Transactional + public void syncStatusFromWorkflowInstance(WorkflowInstance instance, WorkflowInstanceStatusEnums status) { + DeployRecord record = deployRecordRepository + .findByWorkflowInstanceIdAndDeletedFalse(instance.getId()) + .orElse(null); + + if (record == null) { + log.warn("部署记录不存在,无法同步状态: workflowInstanceId={}", instance.getId()); + return; + } + + // 转换状态 + DeployRecordStatusEnums deployStatus = DeployRecordStatusEnums.fromWorkflowStatus(status.name()); + + // 更新状态和时间 + record.setStatus(deployStatus); + if (instance.getStartTime() != null && record.getStartTime() == null) { + record.setStartTime(instance.getStartTime()); + } + if (instance.getEndTime() != null) { + record.setEndTime(instance.getEndTime()); + } + + deployRecordRepository.save(record); + log.debug("同步部署记录状态: id={}, workflowInstanceId={}, status={}", + record.getId(), instance.getId(), deployStatus); + } + + @Override + public DeployRecordDTO findByWorkflowInstanceId(Long workflowInstanceId) { + return deployRecordRepository + .findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId) + .map(deployRecordConverter::toDto) + .orElse(null); + } +} + 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 0f9182b2..0493eb24 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 @@ -16,11 +16,16 @@ import com.qqchen.deploy.backend.workflow.dto.inputmapping.JenkinsBuildInputMapp import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService; +import com.qqchen.deploy.backend.deploy.service.IDeployRecordService; +import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository; +import com.qqchen.deploy.backend.deploy.entity.DeployRecord; +import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; 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.*; import java.util.stream.Collectors; @@ -66,6 +71,12 @@ public class DeployServiceImpl implements IDeployService { @Resource private IWorkflowInstanceService workflowInstanceService; + @Resource + private IDeployRecordService deployRecordService; + + @Resource + private IDeployRecordRepository deployRecordRepository; + @Resource private ObjectMapper objectMapper; @@ -178,7 +189,62 @@ public class DeployServiceImpl implements IDeployService { approverMap = Collections.emptyMap(); } - // 12. 组装团队数据 + // 12. 批量查询部署记录信息 + List teamApplicationIds = allTeamApps.stream() + .map(TeamApplication::getId) + .collect(toList()); + + // 12.1 批量查询部署统计信息 + final Map statisticsMap = new HashMap<>(); + final Map latestRecordMap = new HashMap<>(); + final Map> recentRecordsMap = new HashMap<>(); + + if (!teamApplicationIds.isEmpty()) { + // 查询统计信息 + List statisticsList = deployRecordRepository + .findDeployStatisticsByTeamApplicationIds(teamApplicationIds); + for (Object[] row : statisticsList) { + Long teamApplicationId = (Long) row[0]; + Long totalCount = ((Number) row[1]).longValue(); + Long successCount = ((Number) row[2]).longValue(); + Long failedCount = ((Number) row[3]).longValue(); + Long runningCount = ((Number) row[4]).longValue(); + LocalDateTime lastDeployTime = (LocalDateTime) row[5]; + + DeployStatisticsDTO stats = new DeployStatisticsDTO(); + stats.setTotalCount(totalCount); + stats.setSuccessCount(successCount); + stats.setFailedCount(failedCount); + stats.setRunningCount(runningCount); + stats.setLastDeployTime(lastDeployTime); + statisticsMap.put(teamApplicationId, stats); + } + + // 查询最新部署记录(用于获取最新状态和部署人) + List latestRecords = deployRecordRepository + .findLatestDeployRecordsByTeamApplicationIds(teamApplicationIds); + latestRecordMap.putAll(latestRecords.stream() + .collect(toMap(DeployRecord::getTeamApplicationId, r -> r))); + + // 更新统计信息中的最新状态和部署人 + latestRecordMap.forEach((teamAppId, record) -> { + DeployStatisticsDTO stats = statisticsMap.get(teamAppId); + if (stats == null) { + stats = new DeployStatisticsDTO(); + statisticsMap.put(teamAppId, stats); + } + stats.setLatestStatus(record.getStatus()); + stats.setLastDeployBy(record.getDeployBy()); + }); + + // 查询最近10条部署记录 + List recentRecords = deployRecordRepository + .findRecentDeployRecordsByTeamApplicationIds(teamApplicationIds, 10); + recentRecordsMap.putAll(recentRecords.stream() + .collect(groupingBy(DeployRecord::getTeamApplicationId))); + } + + // 13. 组装团队数据 Map teamMemberMap = teamMembers.stream() .collect(toMap(TeamMember::getTeamId, tm -> tm)); @@ -194,12 +260,15 @@ public class DeployServiceImpl implements IDeployService { appMap, systemMap, workflowMap, - approverMap + approverMap, + statisticsMap, + latestRecordMap, + recentRecordsMap )) .filter(Objects::nonNull) .collect(toList()); - // 13. 组装最终结果 + // 14. 组装最终结果 UserDeployableDTO result = new UserDeployableDTO(); result.setUserId(user.getId()); result.setUsername(user.getUsername()); @@ -236,7 +305,10 @@ public class DeployServiceImpl implements IDeployService { Map appMap, Map systemMap, Map workflowMap, - Map approverMap + Map approverMap, + Map statisticsMap, + Map latestRecordMap, + Map> recentRecordsMap ) { Team team = teamMap.get(teamId); if (team == null) { @@ -286,7 +358,10 @@ public class DeployServiceImpl implements IDeployService { appMap, systemMap, workflowMap, - approverMap + approverMap, + statisticsMap, + latestRecordMap, + recentRecordsMap ); envDTOs.add(envDTO); @@ -312,7 +387,10 @@ public class DeployServiceImpl implements IDeployService { Map appMap, Map systemMap, Map workflowMap, - Map approverMap + Map approverMap, + Map statisticsMap, + Map latestRecordMap, + Map> recentRecordsMap ) { DeployableEnvironmentDTO envDTO = new DeployableEnvironmentDTO(); envDTO.setEnvironmentId(env.getId()); @@ -349,7 +427,15 @@ public class DeployServiceImpl implements IDeployService { // 应用列表 List appDTOs = teamApps.stream() - .map(ta -> buildApplicationDTO(ta, appMap, systemMap, workflowMap)) + .map(ta -> buildApplicationDTO( + ta, + appMap, + systemMap, + workflowMap, + statisticsMap, + latestRecordMap, + recentRecordsMap + )) .filter(Objects::nonNull) .collect(toList()); @@ -381,7 +467,10 @@ public class DeployServiceImpl implements IDeployService { TeamApplication ta, Map appMap, Map systemMap, - Map workflowMap + Map workflowMap, + Map statisticsMap, + Map latestRecordMap, + Map> recentRecordsMap ) { Application app = appMap.get(ta.getApplicationId()); if (app == null) { @@ -416,9 +505,66 @@ public class DeployServiceImpl implements IDeployService { } } + // 部署统计信息和记录 + DeployStatisticsDTO statistics = statisticsMap.get(ta.getId()); + if (statistics != null) { + dto.setDeployStatistics(statistics); + + // 判断是否正在部署中 + DeployRecord latestRecord = latestRecordMap.get(ta.getId()); + if (latestRecord != null) { + DeployRecordStatusEnums status = latestRecord.getStatus(); + dto.setIsDeploying(status == DeployRecordStatusEnums.CREATED || + status == DeployRecordStatusEnums.RUNNING); + } else { + dto.setIsDeploying(false); + } + } else { + // 没有部署记录,设置默认值 + DeployStatisticsDTO emptyStats = new DeployStatisticsDTO(); + emptyStats.setTotalCount(0L); + emptyStats.setSuccessCount(0L); + emptyStats.setFailedCount(0L); + emptyStats.setRunningCount(0L); + dto.setDeployStatistics(emptyStats); + dto.setIsDeploying(false); + } + + // 最近部署记录列表 + List recentRecords = recentRecordsMap.getOrDefault(ta.getId(), Collections.emptyList()); + List recordSummaryList = recentRecords.stream() + .map(this::buildDeployRecordSummary) + .collect(toList()); + dto.setRecentDeployRecords(recordSummaryList); + return dto; } + /** + * 构建部署记录摘要DTO + */ + private DeployRecordSummaryDTO buildDeployRecordSummary(DeployRecord record) { + DeployRecordSummaryDTO summary = new DeployRecordSummaryDTO(); + summary.setId(record.getId()); + summary.setStatus(record.getStatus()); + summary.setDeployBy(record.getDeployBy()); + summary.setStartTime(record.getStartTime()); + summary.setEndTime(record.getEndTime()); + summary.setDeployRemark(record.getDeployRemark()); + + // 计算持续时间(毫秒) + if (record.getStartTime() != null && record.getEndTime() != null) { + long duration = java.time.Duration.between(record.getStartTime(), record.getEndTime()).toMillis(); + summary.setDuration(duration); + } else if (record.getStartTime() != null) { + // 如果正在运行,计算到目前为止的持续时间 + long duration = java.time.Duration.between(record.getStartTime(), LocalDateTime.now()).toMillis(); + summary.setDuration(duration); + } + + return summary; + } + @Override @Transactional public DeployResultDTO executeDeploy(DeployRequestDTO request) { @@ -483,7 +629,19 @@ public class DeployServiceImpl implements IDeployService { log.info("部署流程已启动: businessKey={}, workflowInstanceId={}, application={}, environment={}", businessKey, workflowInstance.getId(), application.getAppCode(), environment.getEnvCode()); - // 9. 返回结果 + // 9. 创建部署记录(此时已有实例ID) + deployRecordService.createDeployRecord( + workflowInstance.getId(), + businessKey, + teamApp.getId(), + teamApp.getTeamId(), + teamApp.getApplicationId(), + teamApp.getEnvironmentId(), + SecurityUtils.getCurrentUsername(), + request.getRemark() + ); + + // 10. 返回结果 DeployResultDTO result = new DeployResultDTO(); result.setWorkflowInstanceId(workflowInstance.getId()); result.setBusinessKey(businessKey); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java index cbbc2d8a..2d4230f1 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowInstanceServiceImpl.java @@ -23,6 +23,9 @@ import com.qqchen.deploy.backend.workflow.dto.query.WorkflowHistoricalInstancesQ import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowNodeInstanceRepository; +import com.qqchen.deploy.backend.workflow.repository.IWorkflowCategoryRepository; +import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory; +import com.qqchen.deploy.backend.deploy.service.IDeployRecordService; import com.qqchen.deploy.backend.workflow.service.IFormDataService; import com.qqchen.deploy.backend.workflow.service.IFormDefinitionService; import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService; @@ -86,13 +89,65 @@ public class WorkflowInstanceServiceImpl extends BaseServiceImpl new RuntimeException("Workflow instance not found: " + processInstanceId)); instance.setStatus(status); instance.setEndTime(endTime); - return workflowInstanceRepository.save(instance); + WorkflowInstance saved = workflowInstanceRepository.save(instance); + + // 同步部署记录状态(如果是部署类型的工作流) + try { + syncDeployRecordIfNeeded(saved, status); + } catch (Exception e) { + log.error("同步部署记录状态失败: workflowInstanceId={}, status={}", + saved.getId(), status, e); + // 不影响主流程,只记录错误 + } + + return saved; + } + + /** + * 如果需要,同步部署记录状态 + */ + private void syncDeployRecordIfNeeded(WorkflowInstance instance, WorkflowInstanceStatusEnums status) { + // 1. 查询工作流定义 + WorkflowDefinition definition = workflowDefinitionRepository.findById(instance.getWorkflowDefinitionId()) + .orElse(null); + + if (definition == null || definition.getCategoryId() == null) { + return; + } + + // 2. 查询分类 + WorkflowCategory category = workflowCategoryRepository.findById(definition.getCategoryId()) + .orElse(null); + + if (category == null) { + return; + } + + // 3. 判断是否是部署类型(通过分类code) + if (!DEPLOYMENT_CATEGORY_CODE.equals(category.getCode())) { + return; + } + + // 4. 同步部署记录状态 + deployRecordService.syncStatusFromWorkflowInstance(instance, status); + log.debug("部署记录状态同步成功: workflowInstanceId={}, status={}", instance.getId(), status); } @Override diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql index 9460975a..d7f60829 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql @@ -1070,3 +1070,36 @@ CREATE TABLE deploy_server CONSTRAINT fk_server_category FOREIGN KEY (category_id) REFERENCES deploy_server_category (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='服务器管理表'; +-- 部署记录表 +CREATE TABLE deploy_record +( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(100) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + update_by VARCHAR(100) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 1 COMMENT '版本号', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', + + workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID(关联workflow_instance)', + business_key VARCHAR(64) NOT NULL COMMENT '业务标识(UUID)', + team_application_id BIGINT NOT NULL COMMENT '团队应用配置ID(关联deploy_team_application)', + team_id BIGINT NOT NULL COMMENT '团队ID', + application_id BIGINT NOT NULL COMMENT '应用ID', + environment_id BIGINT NOT NULL COMMENT '环境ID', + deploy_by VARCHAR(50) NULL COMMENT '部署人', + deploy_remark VARCHAR(500) NULL COMMENT '部署备注', + status VARCHAR(20) NOT NULL COMMENT '部署状态(CREATED/RUNNING/SUCCESS/FAILED等)', + start_time DATETIME(6) NULL COMMENT '开始时间', + end_time DATETIME(6) NULL COMMENT '结束时间', + + UNIQUE INDEX uk_workflow_instance (workflow_instance_id), + INDEX idx_team_application (team_application_id), + INDEX idx_application (application_id), + INDEX idx_environment (environment_id), + INDEX idx_status (status), + INDEX idx_start_time (start_time), + CONSTRAINT fk_deploy_record_instance FOREIGN KEY (workflow_instance_id) REFERENCES workflow_instance (id), + CONSTRAINT fk_deploy_record_team_app FOREIGN KEY (team_application_id) REFERENCES deploy_team_application (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部署记录表'; +