From e34248f3ea621c0274e6a06d640a24eaf7eedced Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 6 Nov 2025 18:07:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=8D=B0=E4=BA=86JENKINS=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deploy/converter/ApproverConverter.java | 68 ----- .../DeployableApplicationConverter.java | 169 ----------- .../DeployableEnvironmentConverter.java | 94 ------ .../backend/deploy/dto/ApproverDTO.java | 25 -- .../deploy/dto/DeployExecuteRequest.java | 4 +- .../deploy/dto/DeployableApplicationDTO.java | 64 ---- .../deploy/dto/DeployableEnvironmentDTO.java | 57 ---- .../backend/deploy/dto/TeamDeployableDTO.java | 36 --- .../dto/UserDeployableTeamEnvironmentDTO.java | 8 + .../service/TeamDeployConfigCacheService.java | 0 .../service/impl/DeployServiceImpl.java | 280 +++++++++++------- .../backend/framework/utils/MapUtils.java | 236 +++++++++++++++ 12 files changed, 416 insertions(+), 625 deletions(-) delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ApproverConverter.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableApplicationConverter.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableEnvironmentConverter.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ApproverDTO.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableApplicationDTO.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableEnvironmentDTO.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamDeployableDTO.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/service/TeamDeployConfigCacheService.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/utils/MapUtils.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ApproverConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ApproverConverter.java deleted file mode 100644 index 6a09bd80..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/ApproverConverter.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.qqchen.deploy.backend.deploy.converter; - -import com.qqchen.deploy.backend.deploy.dto.ApproverDTO; -import com.qqchen.deploy.backend.system.entity.User; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 审批人转换器 - * - *

职责:User → ApproverDTO 的转换 - *

遵循单一职责原则(SRP),只负责审批人相关的转换逻辑 - * - * @author qqchen - * @since 2025-11-06 - */ -@Mapper(componentModel = "spring") -@Component -public interface ApproverConverter { - - /** - * User 转换为 ApproverDTO - * - * @param user 用户实体 - * @return 审批人DTO - */ - @Mappings({ - @Mapping(source = "id", target = "userId"), - @Mapping(source = "username", target = "username"), - @Mapping(source = "nickname", target = "realName") - }) - ApproverDTO toDTO(User user); - - /** - * 批量转换 User 列表 - * - * @param users 用户列表 - * @return 审批人DTO列表 - */ - List toDTOList(List users); - - /** - * 从用户ID列表和用户Map构建审批人列表 - *

常用场景:从 TeamEnvironmentConfig.approverUserIds 构建审批人列表 - * - * @param userIds 用户ID列表 - * @param userMap 用户Map(key=userId, value=User) - * @return 审批人DTO列表(自动过滤 Map 中不存在的用户) - */ - default List fromUserIds(List userIds, Map userMap) { - if (userIds == null || userIds.isEmpty() || userMap == null || userMap.isEmpty()) { - return List.of(); - } - - return userIds.stream() - .map(userMap::get) // 从 Map 获取 User - .filter(user -> user != null) // 过滤 null(Map 中不存在的用户) - .map(this::toDTO) // 转换为 ApproverDTO - .collect(Collectors.toList()); - } -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableApplicationConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableApplicationConverter.java deleted file mode 100644 index 6f36f92c..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableApplicationConverter.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.qqchen.deploy.backend.deploy.converter; - -import com.qqchen.deploy.backend.deploy.dto.DeployRecordSummaryDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployStatisticsDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployableApplicationDTO; -import com.qqchen.deploy.backend.deploy.entity.*; -import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums; -import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -/** - * 可部署应用转换器 - * - *

职责:多源数据 → DeployableApplicationDTO 的转换 - *

遵循单一职责原则(SRP),只负责可部署应用相关的转换逻辑 - * - *

数据源: - *

- * - * @author qqchen - * @since 2025-11-06 - */ -@Mapper(componentModel = "spring") -@Component -public interface DeployableApplicationConverter { - - /** - * 基础转换:TeamApplication + Application → DeployableApplicationDTO - *

只处理简单字段映射 - */ - @Mappings({ - @Mapping(source = "teamApplication.id", target = "teamApplicationId"), - @Mapping(source = "application.id", target = "applicationId"), - @Mapping(source = "application.appCode", target = "applicationCode"), - @Mapping(source = "application.appName", target = "applicationName"), - @Mapping(source = "application.appDesc", target = "applicationDesc"), - @Mapping(source = "teamApplication.branch", target = "branch"), - @Mapping(source = "teamApplication.deploySystemId", target = "deploySystemId"), - @Mapping(source = "teamApplication.deployJob", target = "deployJob"), - @Mapping(source = "teamApplication.workflowDefinitionId", target = "workflowDefinitionId"), - @Mapping(target = "deploySystemName", ignore = true), - @Mapping(target = "workflowDefinitionName", ignore = true), - @Mapping(target = "workflowDefinitionKey", ignore = true), - @Mapping(target = "deployStatistics", ignore = true), - @Mapping(target = "isDeploying", ignore = true), - @Mapping(target = "recentDeployRecords", ignore = true) - }) - DeployableApplicationDTO toBaseDTO(TeamApplication teamApplication, Application application); - - /** - * 设置部署系统信息 - * - * @param dto 目标DTO - * @param system 部署系统(Jenkins) - */ - default void applyDeploySystem(DeployableApplicationDTO dto, ExternalSystem system) { - if (system != null) { - dto.setDeploySystemName(system.getName()); - } - } - - /** - * 设置工作流定义信息 - * - * @param dto 目标DTO - * @param workflow 工作流定义 - */ - default void applyWorkflowDefinition(DeployableApplicationDTO dto, WorkflowDefinition workflow) { - if (workflow != null) { - dto.setWorkflowDefinitionName(workflow.getName()); - dto.setWorkflowDefinitionKey(workflow.getKey()); - } - } - - /** - * 设置部署统计信息和部署状态 - * - * @param dto 目标DTO - * @param statistics 部署统计 - * @param latestRecord 最新部署记录(用于判断是否正在部署) - */ - default void applyDeployStatistics(DeployableApplicationDTO dto, DeployStatisticsDTO statistics, DeployRecord latestRecord) { - if (statistics != null) { - dto.setDeployStatistics(statistics); - - // 判断是否正在部署中 - if (latestRecord != null) { - DeployRecordStatusEnums status = latestRecord.getStatus(); - dto.setIsDeploying(status == DeployRecordStatusEnums.CREATED || status == DeployRecordStatusEnums.RUNNING); - } else { - dto.setIsDeploying(false); - } - } else { - // 没有部署记录,设置默认空统计 - dto.setDeployStatistics(createEmptyStatistics()); - dto.setIsDeploying(false); - } - } - - /** - * 设置最近部署记录列表 - * - * @param dto 目标DTO - * @param recentRecordSummaries 最近部署记录摘要列表 - */ - default void setRecentDeployRecords(DeployableApplicationDTO dto, List recentRecordSummaries) { - dto.setRecentDeployRecords(recentRecordSummaries != null ? recentRecordSummaries : List.of()); - } - - /** - * 创建空的部署统计(用于没有部署记录的应用) - */ - default DeployStatisticsDTO createEmptyStatistics() { - DeployStatisticsDTO statistics = new DeployStatisticsDTO(); - statistics.setTotalCount(0L); - statistics.setSuccessCount(0L); - statistics.setFailedCount(0L); - statistics.setRunningCount(0L); - return statistics; - } - - /** - * 完整转换:从上下文构建 DeployableApplicationDTO - *

封装所有转换逻辑,简化 Service 层调用 - * - * @param teamApplication 团队应用关联 - * @param application 应用实体 - * @param deploySystem 部署系统(可选) - * @param workflow 工作流定义(可选) - * @param statistics 部署统计(可选) - * @param latestRecord 最新部署记录(可选) - * @param recentRecordSummaries 最近部署记录摘要列表(可选) - * @return 完整的可部署应用DTO - */ - default DeployableApplicationDTO toFullDTO( - TeamApplication teamApplication, - Application application, - ExternalSystem deploySystem, - WorkflowDefinition workflow, - DeployStatisticsDTO statistics, - DeployRecord latestRecord, - List recentRecordSummaries - ) { - // 1. 基础转换 - DeployableApplicationDTO dto = toBaseDTO(teamApplication, application); - - // 2. 应用扩展信息 - applyDeploySystem(dto, deploySystem); - applyWorkflowDefinition(dto, workflow); - applyDeployStatistics(dto, statistics, latestRecord); - setRecentDeployRecords(dto, recentRecordSummaries); - - return dto; - } -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableEnvironmentConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableEnvironmentConverter.java deleted file mode 100644 index 0972a7f3..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/DeployableEnvironmentConverter.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.qqchen.deploy.backend.deploy.converter; - -import com.qqchen.deploy.backend.deploy.dto.ApproverDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployableApplicationDTO; -import com.qqchen.deploy.backend.deploy.dto.DeployableEnvironmentDTO; -import com.qqchen.deploy.backend.deploy.entity.Environment; -import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; -import org.springframework.stereotype.Component; - -import java.util.Collections; -import java.util.List; - -/** - * 可部署环境转换器 - * - *

采用 MapStruct + 自定义方法混合模式: - *

- * - * @author qqchen - * @since 2025-11-06 - */ -@Mapper(componentModel = "spring") -@Component -public interface DeployableEnvironmentConverter { - - /** - * 基础转换:Environment → DeployableEnvironmentDTO - *

仅处理简单字段映射 - */ - @Mappings({ - @Mapping(source = "id", target = "environmentId"), - @Mapping(source = "envCode", target = "environmentCode"), - @Mapping(source = "envName", target = "environmentName"), - @Mapping(source = "envDesc", target = "environmentDesc"), - @Mapping(target = "requiresApproval", ignore = true), - @Mapping(target = "approvers", ignore = true), - @Mapping(target = "notificationEnabled", ignore = true), - @Mapping(target = "notificationChannelId", ignore = true), - @Mapping(target = "requireCodeReview", ignore = true), - @Mapping(target = "remark", ignore = true), - @Mapping(target = "applications", ignore = true) - }) - DeployableEnvironmentDTO toBaseDTO(Environment environment); - - /** - * 合并配置信息到 DTO - *

从 TeamEnvironmentConfig 补充配置字段 - * - * @param dto 目标 DTO - * @param config 团队环境配置 - */ - default void applyConfig(DeployableEnvironmentDTO dto, TeamEnvironmentConfig config) { - if (config != null) { - dto.setRequiresApproval(config.getApprovalRequired() != null ? config.getApprovalRequired() : false); - dto.setRequireCodeReview(config.getRequireCodeReview() != null ? config.getRequireCodeReview() : false); - dto.setNotificationEnabled(config.getNotificationEnabled() != null ? config.getNotificationEnabled() : true); - dto.setNotificationChannelId(config.getNotificationChannelId()); - dto.setRemark(config.getRemark()); - } else { - // 默认值 - dto.setRequiresApproval(false); - dto.setRequireCodeReview(false); - dto.setNotificationEnabled(true); - dto.setApprovers(Collections.emptyList()); - } - } - - /** - * 设置审批人列表 - * - * @param dto 目标 DTO - * @param approvers 审批人列表 - */ - default void setApprovers(DeployableEnvironmentDTO dto, List approvers) { - dto.setApprovers(approvers != null ? approvers : Collections.emptyList()); - } - - /** - * 设置应用列表 - * - * @param dto 目标 DTO - * @param applications 应用列表 - */ - default void setApplications(DeployableEnvironmentDTO dto, List applications) { - dto.setApplications(applications != null ? applications : Collections.emptyList()); - } -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ApproverDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ApproverDTO.java deleted file mode 100644 index 5279132f..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ApproverDTO.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.qqchen.deploy.backend.deploy.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -/** - * 审批人DTO - * - * @author qqchen - * @since 2025-11-02 - */ -@Data -@Schema(description = "审批人信息") -public class ApproverDTO { - - @Schema(description = "用户ID") - private Long userId; - - @Schema(description = "用户名") - private String username; - - @Schema(description = "真实姓名") - private String realName; -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployExecuteRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployExecuteRequest.java index 869eb60c..5721c21f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployExecuteRequest.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployExecuteRequest.java @@ -6,6 +6,8 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.util.List; + /** * 部署执行请求 * @@ -95,7 +97,7 @@ public class DeployExecuteRequest { private Boolean required; @Schema(description = "审批人ID列表(逗号分隔)") - private String userIds; + private List userNames; } /** 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 deleted file mode 100644 index 8cb6985d..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableApplicationDTO.java +++ /dev/null @@ -1,64 +0,0 @@ -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 - * - * @author qqchen - * @since 2025-11-02 - */ -@Data -@Schema(description = "可部署应用信息") -public class DeployableApplicationDTO { - - @Schema(description = "团队应用关联ID") - private Long teamApplicationId; - - @Schema(description = "应用ID") - private Long applicationId; - - @Schema(description = "应用编码") - private String applicationCode; - - @Schema(description = "应用名称") - private String applicationName; - - @Schema(description = "应用描述") - private String applicationDesc; - - @Schema(description = "分支名称") - private String branch; - - @Schema(description = "部署系统ID(Jenkins系统)") - private Long deploySystemId; - - @Schema(description = "部署系统名称") - private String deploySystemName; - - @Schema(description = "部署任务ID(Jenkins Job)") - private String deployJob; - - @Schema(description = "工作流定义ID") - private Long workflowDefinitionId; - - @Schema(description = "工作流定义名称") - private String workflowDefinitionName; - - @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/dto/DeployableEnvironmentDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableEnvironmentDTO.java deleted file mode 100644 index 43e269b3..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployableEnvironmentDTO.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.qqchen.deploy.backend.deploy.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.List; - -/** - * 可部署环境DTO - * - * @author qqchen - * @since 2025-11-02 - */ -@Data -@Schema(description = "可部署环境信息") -public class DeployableEnvironmentDTO { - - @Schema(description = "环境ID") - private Long environmentId; - - @Schema(description = "环境编码") - private String environmentCode; - - @Schema(description = "环境名称") - private String environmentName; - - @Schema(description = "环境描述") - private String environmentDesc; - - @Schema(description = "是否启用") - private Boolean enabled; - - @Schema(description = "排序号") - private Integer sort; - - @Schema(description = "是否需要审批") - private Boolean requiresApproval; - - @Schema(description = "审批人列表") - private List approvers; - - @Schema(description = "是否启用部署通知") - private Boolean notificationEnabled; - - @Schema(description = "通知渠道ID") - private Long notificationChannelId; - - @Schema(description = "是否要求代码审查") - private Boolean requireCodeReview; - - @Schema(description = "备注信息") - private String remark; - - @Schema(description = "可部署应用列表") - private List applications; -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamDeployableDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamDeployableDTO.java deleted file mode 100644 index 2d2d1cf3..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamDeployableDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.qqchen.deploy.backend.deploy.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.List; - -/** - * 团队可部署环境DTO - * - * @author qqchen - * @since 2025-11-02 - */ -@Data -@Schema(description = "团队可部署环境信息") -public class TeamDeployableDTO { - - @Schema(description = "团队ID") - private Long teamId; - - @Schema(description = "团队编码") - private String teamCode; - - @Schema(description = "团队名称") - private String teamName; - - @Schema(description = "用户在团队中的角色") - private String teamRole; - - @Schema(description = "团队描述") - private String description; - - @Schema(description = "可部署环境列表") - private List environments; -} - diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/UserDeployableTeamEnvironmentDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/UserDeployableTeamEnvironmentDTO.java index d3ca39f3..7a338590 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/UserDeployableTeamEnvironmentDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/UserDeployableTeamEnvironmentDTO.java @@ -50,5 +50,13 @@ public class UserDeployableTeamEnvironmentDTO { @Schema(description = "可部署应用列表") private List applications; + + // ==================== 当前用户权限 ==================== + + @Schema(description = "当前用户是否可以发起部署") + private Boolean canDeploy; + + @Schema(description = "当前用户是否可以审批部署") + private Boolean canApprove; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/TeamDeployConfigCacheService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/TeamDeployConfigCacheService.java deleted file mode 100644 index e69de29b..00000000 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 754d072d..f585f526 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 @@ -1,8 +1,5 @@ package com.qqchen.deploy.backend.deploy.service.impl; -import com.qqchen.deploy.backend.deploy.converter.ApproverConverter; -import com.qqchen.deploy.backend.deploy.converter.DeployableApplicationConverter; -import com.qqchen.deploy.backend.deploy.converter.DeployableEnvironmentConverter; import com.qqchen.deploy.backend.deploy.dto.*; import com.qqchen.deploy.backend.deploy.entity.*; import com.qqchen.deploy.backend.deploy.repository.*; @@ -11,6 +8,7 @@ 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.JsonUtils; +import com.qqchen.deploy.backend.framework.utils.MapUtils; import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver; import com.qqchen.deploy.backend.system.entity.User; import com.qqchen.deploy.backend.system.repository.IUserRepository; @@ -18,7 +16,6 @@ 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; @@ -30,11 +27,11 @@ 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.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.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -102,17 +99,8 @@ public class DeployServiceImpl implements IDeployService { @Resource private RuntimeService runtimeService; - @Resource - private DeployableEnvironmentConverter deployableEnvironmentConverter; - - @Resource - private ApproverConverter approverConverter; - - @Resource - private DeployableApplicationConverter deployableApplicationConverter; @Override - @Transactional(readOnly = true) public List getDeployableEnvironments() { Long currentUserId = SecurityUtils.getCurrentUserId(); @@ -249,7 +237,7 @@ public class DeployServiceImpl implements IDeployService { List result = new ArrayList<>(); for (Long teamId : teamIds) { UserTeamDeployableDTO teamDTO = buildUserTeamDeployableDTO( - teamId, teamMap, ownerMap, membersByTeam, memberUserMap, + currentUserId, teamId, teamMap, ownerMap, membersByTeam, memberUserMap, teamAppsMap, envMap, appMap, systemMap, workflowMap, teamEnvConfigMap, approverMap, statisticsMap, latestRecordMap, recentRecordsMap @@ -267,6 +255,7 @@ public class DeployServiceImpl implements IDeployService { * 构建单个团队的可部署数据DTO */ private UserTeamDeployableDTO buildUserTeamDeployableDTO( + Long currentUserId, Long teamId, Map teamMap, Map ownerMap, @@ -342,6 +331,7 @@ public class DeployServiceImpl implements IDeployService { } UserDeployableTeamEnvironmentDTO envDTO = buildUserDeployableTeamEnvironmentDTO( + currentUserId, team.getOwnerId(), teamId, env, entry.getValue(), appMap, systemMap, workflowMap, teamEnvConfigMap, approverMap, @@ -364,6 +354,8 @@ public class DeployServiceImpl implements IDeployService { * 构建单个环境的可部署数据DTO */ private UserDeployableTeamEnvironmentDTO buildUserDeployableTeamEnvironmentDTO( + Long currentUserId, + Long teamOwnerId, Long teamId, Environment env, List teamApps, @@ -430,6 +422,18 @@ public class DeployServiceImpl implements IDeployService { .collect(Collectors.toList()); dto.setApplications(applications); + // ==================== 设置当前用户权限 ==================== + + // 1. 团队成员都可以发起部署 + dto.setCanDeploy(true); + + // 2. 判断是否可以审批(团队负责人 或 审批人列表中的任意一人) + List approverUserIds = config != null && config.getApproverUserIds() != null + ? config.getApproverUserIds() + : Collections.emptyList(); + boolean canApprove = hasApprovalPermission(currentUserId, teamOwnerId, approverUserIds); + dto.setCanApprove(canApprove); + return dto; } @@ -696,9 +700,9 @@ public class DeployServiceImpl implements IDeployService { String currentUsername = SecurityUtils.getCurrentUsername(); log.info("查询用户 {} 的部署审批任务", currentUsername); - // 2. 查询用户的所有待办任务(模仿 ApprovalTaskService.getMyTasks) + // 2. 查询用户的所有待办任务(包括候选人和分配人,支持或签模式) List tasks = taskService.createTaskQuery() - .taskAssignee(currentUsername) + .taskCandidateOrAssigned(currentUsername) .orderByTaskCreateTime() .desc() .list(); @@ -721,6 +725,9 @@ public class DeployServiceImpl implements IDeployService { } } + // 4. 批量查询团队信息(解决 N+1 查询问题) + enrichTeamInfo(result); + log.info("用户 {} 共有 {} 个部署审批任务", currentUsername, result.size()); return result; } @@ -740,19 +747,8 @@ public class DeployServiceImpl implements IDeployService { 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(); + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult(); if (processInstance == null) { log.debug("流程实例 {} 已结束,跳过", task.getProcessInstanceId()); @@ -766,15 +762,9 @@ public class DeployServiceImpl implements IDeployService { } // 4. 查询部署记录 - Optional deployRecordOpt = deployRecordRepository - .findByBusinessKeyAndDeletedFalse(businessKey); - - if (deployRecordOpt.isEmpty()) { - log.warn("找不到业务标识为 {} 的部署记录", businessKey); - return null; - } - - DeployRecord deployRecord = deployRecordOpt.get(); + DeployRecord deployRecord = deployRecordRepository.findByBusinessKeyAndDeletedFalse(businessKey) + .orElseThrow(() -> new BusinessException(ResponseCode.DEPLOY_RECORD_NOT_FOUND, + new Object[]{businessKey})); // 5. 构建 DTO DeployApprovalTaskDTO dto = new DeployApprovalTaskDTO(); @@ -799,37 +789,30 @@ public class DeployServiceImpl implements IDeployService { 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.2 审批配置信息(从审批节点的 NodeContext 中获取 ApprovalInputMapping) + String nodeId = task.getTaskDefinitionKey(); + ApprovalInputMapping approvalInputs = extractApprovalInputMapping(variables, nodeId); + if (approvalInputs != null) { + dto.setApprovalTitle(approvalInputs.getApprovalTitle()); + dto.setApprovalContent(approvalInputs.getApprovalContent()); + dto.setApprovalMode(approvalInputs.getApprovalMode() != null + ? approvalInputs.getApprovalMode().name() + : null); + dto.setAllowDelegate(approvalInputs.getAllowDelegate()); + dto.setAllowAddSign(approvalInputs.getAllowAddSign()); + dto.setRequireComment(approvalInputs.getRequireComment()); } - // 5.5 计算待审批时长 + // 5.3 部署业务上下文信息(从 variables 还原 DeployExecuteRequest) + DeployExecuteRequest deployRequest = JsonUtils.fromMap(variables, DeployExecuteRequest.class); + dto.setDeployRecordId(deployRecord.getId()); + dto.setBusinessKey(businessKey); + dto.setDeployStartTime(deployRecord.getStartTime()); + + // 使用 BeanUtils 批量复制同名字段 + BeanUtils.copyProperties(deployRequest, dto); + + // 5.4 计算待审批时长(团队名称通过批量查询填充) if (dto.getCreateTime() != null) { long pendingMillis = Duration.between(dto.getCreateTime(), LocalDateTime.now()).toMillis(); dto.setPendingDuration(pendingMillis); @@ -841,30 +824,6 @@ public class DeployServiceImpl implements IDeployService { 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) @@ -873,31 +832,29 @@ public class DeployServiceImpl implements IDeployService { log.info("开始处理部署审批: taskId={}, result={}", taskId, request.getResult()); try { - // 1. 验证任务是否存在 + // 1. 获取当前用户 + String currentUsername = SecurityUtils.getCurrentUsername(); + + // 2. 验证任务是否存在 && 当前用户是否有权限审批 + // taskCandidateOrAssigned 支持所有审批模式: + // - SINGLE 模式: 检查 assignee + // - ANY/ALL 模式: 检查 candidateUsers Task task = taskService.createTaskQuery() .taskId(taskId) + .taskCandidateOrAssigned(currentUsername) .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()); + log.error("审批任务不存在或无权审批: taskId={}, 当前用户={}", taskId, currentUsername); throw new BusinessException(ResponseCode.PERMISSION_DENIED); } - // 4. 获取节点ID并更新 NodeContext(与 ApprovalTaskService 保持一致) + log.debug("审批人: {}, 任务负责人: {}", currentUsername, task.getAssignee()); + + // 3. 获取节点ID并更新 NodeContext(与 ApprovalTaskService 保持一致) String nodeId = task.getTaskDefinitionKey(); - // 5. 读取现有 NodeContext + // 4. 读取现有 NodeContext Object nodeDataObj = taskService.getVariable(task.getId(), nodeId); NodeContext nodeContext; if (nodeDataObj instanceof Map) { @@ -909,7 +866,7 @@ public class DeployServiceImpl implements IDeployService { nodeContext = new NodeContext<>(); } - // 6. 创建审批结果(只设置核心字段,其他由 ApprovalEndExecutionListener 自动装配) + // 5. 创建审批结果(只设置核心字段,其他由 ApprovalEndExecutionListener 自动装配) ApprovalOutputs outputs = new ApprovalOutputs(); outputs.setApprovalResult(request.getResult()); outputs.setApprover(currentUsername); @@ -917,18 +874,18 @@ public class DeployServiceImpl implements IDeployService { outputs.setApprovalComment(request.getComment()); // ✅ 不需要手动设置 status 和 approvalDuration,监听器会自动装配 - // 7. 设置到 NodeContext + // 6. 设置到 NodeContext nodeContext.setOutputs(outputs); - // 8. 保存回流程变量 + // 7. 保存回流程变量 taskService.setVariable(task.getId(), nodeId, nodeContext.toMap(objectMapper)); - // 9. 添加任务评论(供历史查询) + // 8. 添加任务评论(供历史查询) if (request.getComment() != null) { taskService.addComment(taskId, task.getProcessInstanceId(), request.getComment()); } - // 10. 完成任务(触发 ApprovalEndExecutionListener,监听器会自动装配其他字段) + // 9. 完成任务(触发 ApprovalEndExecutionListener,监听器会自动装配其他字段) taskService.complete(taskId); log.info("部署审批已提交,后续节点将异步执行: taskId={}, result={}, comment={}", @@ -942,5 +899,106 @@ public class DeployServiceImpl implements IDeployService { throw new BusinessException(ResponseCode.ERROR); } } + + // ==================== 工具方法 ==================== + + /** + * 批量查询并填充团队信息(解决 N+1 查询问题) + * + * @param dtoList 待填充的 DTO 列表 + */ + private void enrichTeamInfo(List dtoList) { + if (dtoList == null || dtoList.isEmpty()) { + return; + } + + // 1. 收集所有 teamIds + Set teamIds = dtoList.stream() + .map(DeployApprovalTaskDTO::getTeamId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (teamIds.isEmpty()) { + return; + } + + // 2. 批量查询团队信息 + Map teamMap = teamRepository.findAllById(teamIds).stream() + .collect(Collectors.toMap(Team::getId, team -> team)); + + // 3. 填充团队名称 + for (DeployApprovalTaskDTO dto : dtoList) { + Long teamId = dto.getTeamId(); + if (teamId != null) { + Team team = teamMap.get(teamId); + if (team != null) { + dto.setTeamName(team.getTeamName()); + } + } + } + } + + /** + * 从流程变量中提取审批节点的 ApprovalInputMapping + *

从 NodeContext 的 inputs 字段中解析审批配置 + * + * @param variables 流程变量 Map + * @param nodeId 审批节点ID + * @return ApprovalInputMapping 对象,解析失败返回 null + */ + private ApprovalInputMapping extractApprovalInputMapping(Map variables, String nodeId) { + try { + // 1. 从 variables 中获取节点数据 + Object nodeData = variables.get(nodeId); + if (nodeData == null) { + log.warn("流程变量中没有找到节点 {} 的数据", nodeId); + return null; + } + + // 2. 解析为 NodeContext + if (nodeData instanceof Map) { + @SuppressWarnings("unchecked") + Map nodeDataMap = (Map) nodeData; + NodeContext nodeContext = + NodeContext.fromMap(nodeDataMap, ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper); + + // 3. 返回 inputMapping + return nodeContext.getInputMapping(); + } + + log.warn("节点 {} 的数据格式不正确,无法解析为 NodeContext", nodeId); + return null; + } catch (Exception e) { + log.error("提取审批节点 {} 的 ApprovalInputMapping 失败", nodeId, e); + return null; + } + } + + /** + * 判断用户是否有审批权限 + *

权限规则:团队负责人 或 审批人列表中的任意一人 + * + * @param currentUserId 当前用户ID + * @param teamOwnerId 团队负责人ID + * @param approverUserIds 审批人ID列表 + * @return true-有权限, false-无权限 + */ + private boolean hasApprovalPermission(Long currentUserId, Long teamOwnerId, List approverUserIds) { + if (currentUserId == null) { + return false; + } + + // 1. 是团队负责人 + if (currentUserId.equals(teamOwnerId)) { + return true; + } + + // 2. 在审批人列表中 + if (approverUserIds != null && !approverUserIds.isEmpty()) { + return approverUserIds.contains(currentUserId); + } + + return false; + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/MapUtils.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/MapUtils.java new file mode 100644 index 00000000..24f03593 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/MapUtils.java @@ -0,0 +1,236 @@ +package com.qqchen.deploy.backend.framework.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * Map 工具类 + *

提供从 Map 中安全获取和转换各种类型值的工具方法 + * + * @author qqchen + * @since 2025-11-06 + */ +@Slf4j +public class MapUtils { + + private MapUtils() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * 从 Map 中安全获取 Long 值 + *

支持类型转换:Long、Integer、String + * + * @param map 数据 Map + * @param key 键名 + * @return Long 值,转换失败或不存在返回 null + */ + public static Long getLongValue(Map map, String key) { + if (map == null || key == null) { + return null; + } + + 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("无法将 key={} 的值转换为 Long: {}", key, value); + return null; + } + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + + log.warn("不支持的 Long 类型转换,key={}, type={}", key, value.getClass().getName()); + return null; + } + + /** + * 从 Map 中安全获取 Integer 值 + *

支持类型转换:Integer、Long、String + * + * @param map 数据 Map + * @param key 键名 + * @return Integer 值,转换失败或不存在返回 null + */ + public static Integer getIntegerValue(Map map, String key) { + if (map == null || key == null) { + return null; + } + + Object value = map.get(key); + if (value == null) { + return null; + } + + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Long) { + return ((Long) value).intValue(); + } + if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException e) { + log.warn("无法将 key={} 的值转换为 Integer: {}", key, value); + return null; + } + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + + log.warn("不支持的 Integer 类型转换,key={}, type={}", key, value.getClass().getName()); + return null; + } + + /** + * 从 Map 中安全获取 String 值 + * + * @param map 数据 Map + * @param key 键名 + * @return String 值,不存在返回 null + */ + public static String getStringValue(Map map, String key) { + if (map == null || key == null) { + return null; + } + + Object value = map.get(key); + return value != null ? value.toString() : null; + } + + /** + * 从 Map 中安全获取 Boolean 值 + *

支持类型转换:Boolean、String("true"/"false") + * + * @param map 数据 Map + * @param key 键名 + * @return Boolean 值,转换失败或不存在返回 null + */ + public static Boolean getBooleanValue(Map map, String key) { + if (map == null || key == null) { + return null; + } + + Object value = map.get(key); + if (value == null) { + return null; + } + + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + + log.warn("不支持的 Boolean 类型转换,key={}, type={}", key, value.getClass().getName()); + return null; + } + + /** + * 从 Map 中安全获取 Double 值 + *

支持类型转换:Double、Float、Integer、Long、String、BigDecimal + * + * @param map 数据 Map + * @param key 键名 + * @return Double 值,转换失败或不存在返回 null + */ + public static Double getDoubleValue(Map map, String key) { + if (map == null || key == null) { + return null; + } + + Object value = map.get(key); + if (value == null) { + return null; + } + + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + if (value instanceof String) { + try { + return Double.parseDouble((String) value); + } catch (NumberFormatException e) { + log.warn("无法将 key={} 的值转换为 Double: {}", key, value); + return null; + } + } + + log.warn("不支持的 Double 类型转换,key={}, type={}", key, value.getClass().getName()); + return null; + } + + /** + * 从 Map 中安全获取 Long 值(带默认值) + * + * @param map 数据 Map + * @param key 键名 + * @param defaultValue 默认值 + * @return Long 值,转换失败或不存在返回默认值 + */ + public static Long getLongValue(Map map, String key, Long defaultValue) { + Long value = getLongValue(map, key); + return value != null ? value : defaultValue; + } + + /** + * 从 Map 中安全获取 Integer 值(带默认值) + * + * @param map 数据 Map + * @param key 键名 + * @param defaultValue 默认值 + * @return Integer 值,转换失败或不存在返回默认值 + */ + public static Integer getIntegerValue(Map map, String key, Integer defaultValue) { + Integer value = getIntegerValue(map, key); + return value != null ? value : defaultValue; + } + + /** + * 从 Map 中安全获取 String 值(带默认值) + * + * @param map 数据 Map + * @param key 键名 + * @param defaultValue 默认值 + * @return String 值,不存在返回默认值 + */ + public static String getStringValue(Map map, String key, String defaultValue) { + String value = getStringValue(map, key); + return value != null ? value : defaultValue; + } + + /** + * 从 Map 中安全获取 Boolean 值(带默认值) + * + * @param map 数据 Map + * @param key 键名 + * @param defaultValue 默认值 + * @return Boolean 值,转换失败或不存在返回默认值 + */ + public static Boolean getBooleanValue(Map map, String key, Boolean defaultValue) { + Boolean value = getBooleanValue(map, key); + return value != null ? value : defaultValue; + } +} +