打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-06 18:07:23 +08:00
parent bb1faa1495
commit e34248f3ea
12 changed files with 416 additions and 625 deletions

View File

@ -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;
/**
* 审批人转换器
*
* <p>职责User ApproverDTO 的转换
* <p>遵循单一职责原则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<ApproverDTO> toDTOList(List<User> users);
/**
* 从用户ID列表和用户Map构建审批人列表
* <p>常用场景 TeamEnvironmentConfig.approverUserIds 构建审批人列表
*
* @param userIds 用户ID列表
* @param userMap 用户Mapkey=userId, value=User
* @return 审批人DTO列表自动过滤 Map 中不存在的用户
*/
default List<ApproverDTO> fromUserIds(List<Long> userIds, Map<Long, User> 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) // 过滤 nullMap 中不存在的用户
.map(this::toDTO) // 转换为 ApproverDTO
.collect(Collectors.toList());
}
}

View File

@ -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;
/**
* 可部署应用转换器
*
* <p>职责多源数据 DeployableApplicationDTO 的转换
* <p>遵循单一职责原则SRP只负责可部署应用相关的转换逻辑
*
* <p>数据源
* <ul>
* <li>TeamApplication - 团队应用关联信息</li>
* <li>Application - 应用基础信息</li>
* <li>ExternalSystem - 部署系统信息Jenkins</li>
* <li>WorkflowDefinition - 工作流定义</li>
* <li>DeployStatistics - 部署统计</li>
* <li>DeployRecord - 部署记录</li>
* </ul>
*
* @author qqchen
* @since 2025-11-06
*/
@Mapper(componentModel = "spring")
@Component
public interface DeployableApplicationConverter {
/**
* 基础转换TeamApplication + Application DeployableApplicationDTO
* <p>只处理简单字段映射
*/
@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<DeployRecordSummaryDTO> 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
* <p>封装所有转换逻辑简化 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<DeployRecordSummaryDTO> recentRecordSummaries
) {
// 1. 基础转换
DeployableApplicationDTO dto = toBaseDTO(teamApplication, application);
// 2. 应用扩展信息
applyDeploySystem(dto, deploySystem);
applyWorkflowDefinition(dto, workflow);
applyDeployStatistics(dto, statistics, latestRecord);
setRecentDeployRecords(dto, recentRecordSummaries);
return dto;
}
}

View File

@ -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;
/**
* 可部署环境转换器
*
* <p>采用 MapStruct + 自定义方法混合模式
* <ul>
* <li>简单字段映射交给 MapStruct 自动生成</li>
* <li>复杂业务逻辑使用 @AfterMapping 或自定义方法</li>
* </ul>
*
* @author qqchen
* @since 2025-11-06
*/
@Mapper(componentModel = "spring")
@Component
public interface DeployableEnvironmentConverter {
/**
* 基础转换Environment DeployableEnvironmentDTO
* <p>仅处理简单字段映射
*/
@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
* <p> 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<ApproverDTO> approvers) {
dto.setApprovers(approvers != null ? approvers : Collections.emptyList());
}
/**
* 设置应用列表
*
* @param dto 目标 DTO
* @param applications 应用列表
*/
default void setApplications(DeployableEnvironmentDTO dto, List<DeployableApplicationDTO> applications) {
dto.setApplications(applications != null ? applications : Collections.emptyList());
}
}

View File

@ -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;
}

View File

@ -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<String> userNames;
}
/**

View File

@ -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 = "部署系统IDJenkins系统")
private Long deploySystemId;
@Schema(description = "部署系统名称")
private String deploySystemName;
@Schema(description = "部署任务IDJenkins 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<DeployRecordSummaryDTO> recentDeployRecords;
}

View File

@ -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<ApproverDTO> 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<DeployableApplicationDTO> applications;
}

View File

@ -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<DeployableEnvironmentDTO> environments;
}

View File

@ -50,5 +50,13 @@ public class UserDeployableTeamEnvironmentDTO {
@Schema(description = "可部署应用列表")
private List<UserDeployableTeamEnvironmentApplicationDTO> applications;
// ==================== 当前用户权限 ====================
@Schema(description = "当前用户是否可以发起部署")
private Boolean canDeploy;
@Schema(description = "当前用户是否可以审批部署")
private Boolean canApprove;
}

View File

@ -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<UserTeamDeployableDTO> getDeployableEnvironments() {
Long currentUserId = SecurityUtils.getCurrentUserId();
@ -249,7 +237,7 @@ public class DeployServiceImpl implements IDeployService {
List<UserTeamDeployableDTO> 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<Long, Team> teamMap,
Map<Long, User> 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<TeamApplication> teamApps,
@ -430,6 +422,18 @@ public class DeployServiceImpl implements IDeployService {
.collect(Collectors.toList());
dto.setApplications(applications);
// ==================== 设置当前用户权限 ====================
// 1. 团队成员都可以发起部署
dto.setCanDeploy(true);
// 2. 判断是否可以审批团队负责人 审批人列表中的任意一人
List<Long> 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<Task> 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<String, Object> deployContext = (Map<String, Object>) variables.get("deploy");
if (deployContext == null) {
log.debug("任务 {} 不包含部署上下文,跳过", task.getId());
return null;
}
// 3. 获取 BusinessKey需要通过 RuntimeService 查询
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.singleResult();
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<DeployRecord> 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<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return null;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Integer) {
return ((Integer) value).longValue();
}
if (value instanceof String) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException e) {
log.warn("无法将 {} 转换为 Long: {}", key, value);
return null;
}
}
return null;
}
@Override
@Transactional(rollbackFor = Exception.class)
@ -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<ApprovalInputMapping, ApprovalOutputs> 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<DeployApprovalTaskDTO> dtoList) {
if (dtoList == null || dtoList.isEmpty()) {
return;
}
// 1. 收集所有 teamIds
Set<Long> teamIds = dtoList.stream()
.map(DeployApprovalTaskDTO::getTeamId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (teamIds.isEmpty()) {
return;
}
// 2. 批量查询团队信息
Map<Long, Team> 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
* <p> NodeContext inputs 字段中解析审批配置
*
* @param variables 流程变量 Map
* @param nodeId 审批节点ID
* @return ApprovalInputMapping 对象解析失败返回 null
*/
private ApprovalInputMapping extractApprovalInputMapping(Map<String, Object> 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<String, Object> nodeDataMap = (Map<String, Object>) nodeData;
NodeContext<ApprovalInputMapping, ApprovalOutputs> 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;
}
}
/**
* 判断用户是否有审批权限
* <p>权限规则团队负责人 审批人列表中的任意一人
*
* @param currentUserId 当前用户ID
* @param teamOwnerId 团队负责人ID
* @param approverUserIds 审批人ID列表
* @return true-有权限, false-无权限
*/
private boolean hasApprovalPermission(Long currentUserId, Long teamOwnerId, List<Long> 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;
}
}

View File

@ -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 工具类
* <p>提供从 Map 中安全获取和转换各种类型值的工具方法
*
* @author qqchen
* @since 2025-11-06
*/
@Slf4j
public class MapUtils {
private MapUtils() {
throw new UnsupportedOperationException("Utility class cannot be instantiated");
}
/**
* Map 中安全获取 Long
* <p>支持类型转换LongIntegerString
*
* @param map 数据 Map
* @param key 键名
* @return Long 转换失败或不存在返回 null
*/
public static Long getLongValue(Map<String, Object> 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
* <p>支持类型转换IntegerLongString
*
* @param map 数据 Map
* @param key 键名
* @return Integer 转换失败或不存在返回 null
*/
public static Integer getIntegerValue(Map<String, Object> 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<String, Object> map, String key) {
if (map == null || key == null) {
return null;
}
Object value = map.get(key);
return value != null ? value.toString() : null;
}
/**
* Map 中安全获取 Boolean
* <p>支持类型转换BooleanString("true"/"false")
*
* @param map 数据 Map
* @param key 键名
* @return Boolean 转换失败或不存在返回 null
*/
public static Boolean getBooleanValue(Map<String, Object> 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
* <p>支持类型转换DoubleFloatIntegerLongStringBigDecimal
*
* @param map 数据 Map
* @param key 键名
* @return Double 转换失败或不存在返回 null
*/
public static Double getDoubleValue(Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> map, String key, Boolean defaultValue) {
Boolean value = getBooleanValue(map, key);
return value != null ? value : defaultValue;
}
}