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),只负责可部署应用相关的转换逻辑
- *
- *
数据源:
- *
- * - TeamApplication - 团队应用关联信息
- * - Application - 应用基础信息
- * - ExternalSystem - 部署系统信息(Jenkins)
- * - WorkflowDefinition - 工作流定义
- * - DeployStatistics - 部署统计
- * - DeployRecord - 部署记录
- *
- *
- * @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 + 自定义方法混合模式:
- *
- * - 简单字段映射:交给 MapStruct 自动生成
- * - 复杂业务逻辑:使用 @AfterMapping 或自定义方法
- *
- *
- * @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;
+ }
+}
+