打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-06 16:10:25 +08:00
parent 69a41c8aaf
commit 6318ca6241
25 changed files with 1661 additions and 747 deletions

View File

@ -40,7 +40,7 @@ public class DeployApiController {
@Operation(summary = "获取可部署的环境", description = "获取当前登录用户在各团队中可部署的环境和应用列表")
@GetMapping("/environments")
@PreAuthorize("isAuthenticated()")
public Response<UserDeployableDTO> getDeployableEnvironments() {
public Response<List<UserTeamDeployableDTO>> getDeployableEnvironments() {
return Response.success(deployService.getDeployableEnvironments());
}
@ -50,7 +50,7 @@ public class DeployApiController {
@Operation(summary = "执行部署", description = "根据团队应用配置启动部署工作流")
@PostMapping("/execute")
@PreAuthorize("isAuthenticated()")
public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployRequestDTO request) {
public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployExecuteRequest request) {
return Response.success(deployService.executeDeploy(request));
}

View File

@ -0,0 +1,68 @@
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

@ -0,0 +1,169 @@
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

@ -0,0 +1,94 @@
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,46 +0,0 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.fasterxml.jackson.databind.JsonNode;
import com.qqchen.deploy.backend.deploy.enums.BuildTypeEnum;
import com.qqchen.deploy.backend.deploy.enums.DevelopmentLanguageTypeEnum;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用配置DTO
*/
@Data
@Schema(description = "应用配置")
@EqualsAndHashCode(callSuper = true)
public class DeployAppBuildDTO extends BaseDTO {
@Schema(description = "构建类型")
@NotNull(message = "构建类型不能为空")
private BuildTypeEnum buildType;
@NotNull(message = "应用语言不能为空")
private DevelopmentLanguageTypeEnum languageType;
@Schema(description = "表单配置")
private JsonNode formVariables;
@Schema(description = "构建配置")
@NotNull(message = "构建配置不能为空")
private JsonNode buildVariables;
@Schema(description = "环境ID")
@NotNull(message = "环境ID不能为空")
private Long environmentId;
@Schema(description = "应用ID")
@NotNull(message = "应用ID不能为空")
private Long applicationId;
@NotNull(message = "已发布的流程定义ID")
private Long workflowDefinitionId;
}

View File

@ -0,0 +1,115 @@
package com.qqchen.deploy.backend.deploy.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 部署执行请求
*
* @author qqchen
* @since 2025-11-02
*/
@Data
@Schema(description = "部署执行请求")
public class DeployExecuteRequest {
@Schema(description = "Jenkins配置", required = true)
@NotNull(message = "Jenkins配置不能为空")
@Valid
private JenkinsConfig jenkins;
@Schema(description = "团队ID", required = true)
@NotNull(message = "团队ID不能为空")
private Long teamId;
@Schema(description = "团队应用关联ID", required = true)
@NotNull(message = "团队应用关联ID不能为空")
private Long teamApplicationId;
@Schema(description = "应用ID", required = true)
@NotNull(message = "应用ID不能为空")
private Long applicationId;
@Schema(description = "应用编码", required = true)
@NotBlank(message = "应用编码不能为空")
private String applicationCode;
@Schema(description = "应用名称", required = true)
@NotBlank(message = "应用名称不能为空")
private String applicationName;
@Schema(description = "环境ID", required = true)
@NotNull(message = "环境ID不能为空")
private Long environmentId;
@Schema(description = "环境编码", required = true)
@NotBlank(message = "环境编码不能为空")
private String environmentCode;
@Schema(description = "环境名称", required = true)
@NotBlank(message = "环境名称不能为空")
private String environmentName;
@Schema(description = "审批配置", required = true)
@NotNull(message = "审批配置不能为空")
@Valid
private ApprovalConfig approval;
@Schema(description = "通知配置", required = true)
@NotNull(message = "通知配置不能为空")
@Valid
private NotificationConfig notification;
@Schema(description = "部署备注")
private String remark;
/**
* Jenkins配置
*/
@Data
@Schema(description = "Jenkins配置")
public static class JenkinsConfig {
@Schema(description = "Jenkins服务器ID", required = true)
@NotNull(message = "Jenkins服务器ID不能为空")
private Long serverId;
@Schema(description = "Jenkins任务名称", required = true)
@NotBlank(message = "Jenkins任务名称不能为空")
private String jobName;
@Schema(description = "Git分支")
private String branch;
}
/**
* 审批配置
*/
@Data
@Schema(description = "审批配置")
public static class ApprovalConfig {
@Schema(description = "是否需要审批", required = true)
@NotNull(message = "是否需要审批不能为空")
private Boolean required;
@Schema(description = "审批人ID列表逗号分隔")
private String userIds;
}
/**
* 通知配置
*/
@Data
@Schema(description = "通知配置")
public static class NotificationConfig {
@Schema(description = "是否需要通知", required = true)
@NotNull(message = "是否需要通知不能为空")
private Boolean required;
@Schema(description = "通知渠道ID")
private Long channelId;
}
}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.deploy.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 部署请求DTO
*
* @author qqchen
* @since 2025-11-02
*/
@Data
@Schema(description = "部署请求")
public class DeployRequestDTO {
@Schema(description = "团队应用关联ID", required = true)
@NotNull(message = "团队应用关联ID不能为空")
private Long teamApplicationId;
@Schema(description = "部署备注")
private String remark;
}

View File

@ -39,6 +39,12 @@ public class DeployableEnvironmentDTO {
@Schema(description = "审批人列表")
private List<ApproverDTO> approvers;
@Schema(description = "是否启用部署通知")
private Boolean notificationEnabled;
@Schema(description = "通知渠道ID")
private Long notificationChannelId;
@Schema(description = "是否要求代码审查")
private Boolean requireCodeReview;

View File

@ -0,0 +1,68 @@
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-06
*/
@Data
@Schema(description = "可部署应用信息")
public class UserDeployableTeamEnvironmentApplicationDTO {
@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 = "部署任务分支")
private String deployBranch;
@Schema(description = "工作流定义ID")
private Long workflowDefinitionId;
@Schema(description = "工作流绑定的FORM表单ID")
private Long workflowDefinitionFormId;
@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

@ -0,0 +1,25 @@
package com.qqchen.deploy.backend.deploy.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 用户可部署团队环境审批人DTO
*
* @author qqchen
* @since 2025-11-06
*/
@Data
@Schema(description = "审批人信息")
public class UserDeployableTeamEnvironmentApproverDTO {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "真实姓名")
private String realName;
}

View File

@ -0,0 +1,54 @@
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-06
*/
@Data
@Schema(description = "用户可部署团队环境信息")
public class UserDeployableTeamEnvironmentDTO {
@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<UserDeployableTeamEnvironmentApproverDTO> approvers;
@Schema(description = "是否启用部署通知")
private Boolean notificationEnabled;
@Schema(description = "通知渠道ID")
private Long notificationChannelId;
@Schema(description = "是否要求代码审查")
private Boolean requireCodeReview;
@Schema(description = "可部署应用列表")
private List<UserDeployableTeamEnvironmentApplicationDTO> applications;
}

View File

@ -3,17 +3,15 @@ package com.qqchen.deploy.backend.deploy.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 用户可部署环境DTO
* 用户可部署团队成员DTO
*
* @author qqchen
* @since 2025-11-02
* @since 2025-11-06
*/
@Data
@Schema(description = "用户可部署环境信息")
public class UserDeployableDTO {
@Schema(description = "团队成员信息")
public class UserDeployableTeamMemberDTO {
@Schema(description = "用户ID")
private Long userId;
@ -24,7 +22,7 @@ public class UserDeployableDTO {
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "用户所属团队列表")
private List<TeamDeployableDTO> teams;
@Schema(description = "团队角色")
private String roleInTeam;
}

View File

@ -0,0 +1,42 @@
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-06
*/
@Data
@Schema(description = "用户团队可部署数据")
public class UserTeamDeployableDTO {
@Schema(description = "团队ID")
private Long teamId;
@Schema(description = "团队编码")
private String teamCode;
@Schema(description = "团队名称")
private String teamName;
@Schema(description = "团队描述")
private String description;
@Schema(description = "团队负责人ID")
private Long ownerId;
@Schema(description = "团队负责人名称")
private String ownerName;
@Schema(description = "团队成员列表")
private List<UserDeployableTeamMemberDTO> members;
@Schema(description = "可部署环境列表")
private List<UserDeployableTeamEnvironmentDTO> environments;
}

View File

@ -29,5 +29,14 @@ public interface ITeamMemberRepository extends IBaseRepository<TeamMember, Long>
* 根据用户ID查询所有团队成员记录
*/
List<TeamMember> findByUserIdAndDeletedFalse(Long userId);
}
/**
* 批量查询用户在指定团队列表中的成员记录
*/
List<TeamMember> findByUserIdAndTeamIdInAndDeletedFalse(Long userId, List<Long> teamIds);
/**
* 批量查询指定团队列表的所有成员
*/
List<TeamMember> findByTeamIdInAndDeletedFalse(List<Long> teamIds);
}

View File

@ -27,16 +27,7 @@ public interface IDeployRecordService {
* @param deployRemark 部署备注
* @return 部署记录DTO
*/
DeployRecordDTO createDeployRecord(
Long workflowInstanceId,
String businessKey,
Long teamApplicationId,
Long teamId,
Long applicationId,
Long environmentId,
String deployBy,
String deployRemark
);
DeployRecordDTO createDeployRecord(Long workflowInstanceId, String businessKey, Long teamApplicationId, Long teamId, Long applicationId, Long environmentId, String deployBy, String deployRemark);
/**
* 根据工作流实例同步部署记录状态

View File

@ -15,9 +15,9 @@ public interface IDeployService {
/**
* 获取当前用户可部署的环境和应用
*
* @return 用户可部署环境信息
* @return 用户所属团队的可部署环境列表
*/
UserDeployableDTO getDeployableEnvironments();
List<UserTeamDeployableDTO> getDeployableEnvironments();
/**
* 执行部署
@ -25,7 +25,7 @@ public interface IDeployService {
* @param request 部署请求
* @return 部署结果
*/
DeployResultDTO executeDeploy(DeployRequestDTO request);
DeployResultDTO executeDeploy(DeployExecuteRequest request);
/**
* 获取当前用户的部署审批任务列表

View File

@ -84,16 +84,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
@Override
@Transactional
public DeployRecordDTO createDeployRecord(
Long workflowInstanceId,
String businessKey,
Long teamApplicationId,
Long teamId,
Long applicationId,
Long environmentId,
String deployBy,
String deployRemark
) {
public DeployRecordDTO createDeployRecord(Long workflowInstanceId, String businessKey, Long teamApplicationId, Long teamId, Long applicationId, Long environmentId, String deployBy, String deployRemark) {
// 检查是否已存在
DeployRecord existing = deployRecordRepository.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId).orElse(null);

View File

@ -0,0 +1,439 @@
package com.qqchen.deploy.backend.framework.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.Collections;
import java.util.Map;
/**
* JSON 工具类
* <p>提供对象与 JSONMap 之间的转换功能
* <p>封装 ObjectMapper提供统一的异常处理和类型安全的 API
* <p>基于 Jackson性能优于 FastJSON2大型数据集场景下快 1.6
*
* @author qqchen
* @since 2025-11-06
*/
@Slf4j
@Component
public class JsonUtils {
@Resource
private ObjectMapper objectMapper;
private static ObjectMapper MAPPER;
/**
* 性能监控阈值纳秒超过 10ms 记录警告日志
*/
private static final long SLOW_CONVERSION_THRESHOLD_NS = 10_000_000L;
@PostConstruct
public void init() {
MAPPER = this.objectMapper;
optimizePerformance();
}
/**
* 优化 Jackson 性能配置
*/
private void optimizePerformance() {
// 1. 反序列化优化遇到未知属性不抛异常提升容错性和性能
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 2. 序列化优化允许空对象
MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 3. 序列化优化不序列化 null 值字段减少 JSON 大小提升性能
// 注意如果业务需要保留 null 字段可以注释掉这行
// MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 4. 日期格式优化使用 ISO-8601 格式而非时间戳可读性更好
MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 5. 数字格式优化BigDecimal 作为字符串输出避免精度丢失
MAPPER.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
log.info("JsonUtils 性能优化配置已加载");
}
/**
* 将对象转换为 Map
* <p>常用于将 DTO 转换为工作流变量
*
* @param obj 源对象
* @return Map转换失败返回空 Map
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> toMap(Object obj) {
if (obj == null) {
return Collections.emptyMap();
}
try {
long start = System.nanoTime();
Map<String, Object> result = MAPPER.convertValue(obj, Map.class);
logSlowConversion(start, "toMap", obj.getClass().getName());
return result;
} catch (Exception e) {
log.error("对象转换为 Map 失败: {}", obj.getClass().getName(), e);
return Collections.emptyMap();
}
}
/**
* 将对象转换为 Map不带性能监控适用于高频调用场景
* <p>性能略优于 toMap()但不记录慢转换日志
*
* @param obj 源对象
* @return Map转换失败返回空 Map
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> toMapFast(Object obj) {
if (obj == null) {
return Collections.emptyMap();
}
try {
return MAPPER.convertValue(obj, Map.class);
} catch (Exception e) {
log.error("对象转换为 Map 失败: {}", obj.getClass().getName(), e);
return Collections.emptyMap();
}
}
/**
* Map 转换为指定类型的对象
* <p>常用于将配置 Map 转换为强类型配置对象
*
* @param map Map
* @param clazz 目标类型
* @param <T> 目标类型
* @return 转换后的对象转换失败返回 null
*/
public static <T> T fromMap(Map<String, Object> map, Class<T> clazz) {
if (map == null || map.isEmpty()) {
return null;
}
try {
return MAPPER.convertValue(map, clazz);
} catch (Exception e) {
log.error("Map 转换为对象失败: {}", clazz.getName(), e);
return null;
}
}
/**
* 对象类型转换
* <p>将一个对象转换为另一个类型通过 JSON 序列化/反序列化
*
* @param obj 源对象
* @param clazz 目标类型
* @param <T> 目标类型
* @return 转换后的对象转换失败返回 null
*/
public static <T> T convert(Object obj, Class<T> clazz) {
if (obj == null) {
return null;
}
try {
return MAPPER.convertValue(obj, clazz);
} catch (Exception e) {
log.error("对象转换失败: {} -> {}", obj.getClass().getName(), clazz.getName(), e);
return null;
}
}
/**
* 对象类型转换支持泛型
* <p>用于复杂类型转换 ListMap
*
* @param obj 源对象
* @param typeReference 目标类型引用
* @param <T> 目标类型
* @return 转换后的对象转换失败返回 null
*/
public static <T> T convert(Object obj, TypeReference<T> typeReference) {
if (obj == null) {
return null;
}
try {
return MAPPER.convertValue(obj, typeReference);
} catch (Exception e) {
log.error("对象转换失败: {}", obj.getClass().getName(), e);
return null;
}
}
/**
* 将对象转换为 JSON 字符串
*
* @param obj 源对象
* @return JSON 字符串转换失败返回 null
*/
public static String toJson(Object obj) {
if (obj == null) {
return null;
}
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.error("对象转换为 JSON 失败: {}", obj.getClass().getName(), e);
return null;
}
}
/**
* 将对象转换为格式化的 JSON 字符串带缩进
*
* @param obj 源对象
* @return 格式化的 JSON 字符串转换失败返回 null
*/
public static String toPrettyJson(Object obj) {
if (obj == null) {
return null;
}
try {
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.error("对象转换为格式化 JSON 失败: {}", obj.getClass().getName(), e);
return null;
}
}
/**
* JSON 字符串转换为对象
*
* @param json JSON 字符串
* @param clazz 目标类型
* @param <T> 目标类型
* @return 转换后的对象转换失败返回 null
*/
public static <T> T fromJson(String json, Class<T> clazz) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
log.error("JSON 转换为对象失败: {}", clazz.getName(), e);
return null;
}
}
/**
* JSON 字符串转换为对象支持泛型
*
* @param json JSON 字符串
* @param typeReference 目标类型引用
* @param <T> 目标类型
* @return 转换后的对象转换失败返回 null
*/
public static <T> T fromJson(String json, TypeReference<T> typeReference) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return MAPPER.readValue(json, typeReference);
} catch (JsonProcessingException e) {
log.error("JSON 转换为对象失败", e);
return null;
}
}
/**
* JSON 字符串解析为 JsonNode
*
* @param json JSON 字符串
* @return JsonNode转换失败返回 null
*/
public static JsonNode parseJson(String json) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return MAPPER.readTree(json);
} catch (JsonProcessingException e) {
log.error("JSON 解析失败", e);
return null;
}
}
/**
* 深拷贝对象
* <p>通过 JSON 序列化/反序列化实现深拷贝
*
* @param obj 源对象
* @param clazz 目标类型
* @param <T> 目标类型
* @return 深拷贝后的对象转换失败返回 null
*/
public static <T> T deepCopy(T obj, Class<T> clazz) {
if (obj == null) {
return null;
}
try {
String json = MAPPER.writeValueAsString(obj);
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
log.error("对象深拷贝失败: {}", clazz.getName(), e);
return null;
}
}
/**
* 判断字符串是否为有效的 JSON
*
* @param json JSON 字符串
* @return true-有效的 JSONfalse-无效
*/
public static boolean isValidJson(String json) {
if (json == null || json.isEmpty()) {
return false;
}
try {
MAPPER.readTree(json);
return true;
} catch (JsonProcessingException e) {
return false;
}
}
/**
* 合并两个对象使用后者覆盖前者的非空字段
*
* @param original 原始对象
* @param updates 更新对象
* @param clazz 目标类型
* @param <T> 目标类型
* @return 合并后的对象转换失败返回原始对象
*/
public static <T> T merge(T original, T updates, Class<T> clazz) {
if (original == null) {
return updates;
}
if (updates == null) {
return original;
}
try {
return MAPPER.updateValue(original, updates);
} catch (JsonProcessingException e) {
log.error("对象合并失败: {}", clazz.getName(), e);
return original;
}
}
// ==================== 性能监控辅助方法 ====================
/**
* 记录慢转换日志
* <p>当转换耗时超过阈值时记录警告日志
*
* @param startTimeNs 开始时间纳秒
* @param operation 操作名称
* @param target 目标类型
*/
private static void logSlowConversion(long startTimeNs, String operation, String target) {
long duration = System.nanoTime() - startTimeNs;
if (duration > SLOW_CONVERSION_THRESHOLD_NS) {
double durationMs = duration / 1_000_000.0;
log.warn("JSON 转换耗时过长: operation={}, target={}, duration={}ms",
operation, target, String.format("%.2f", durationMs));
}
}
// ==================== 便捷工具方法 ====================
/**
* 获取 ObjectMapper 实例用于高级定制场景
* <p>注意直接使用 ObjectMapper 会绕过工具类的统一异常处理和性能监控
*
* @return ObjectMapper 实例
*/
public static ObjectMapper getMapper() {
return MAPPER;
}
/**
* 判断对象是否可以被 JSON 序列化
*
* @param obj 源对象
* @return true-可序列化false-不可序列化
*/
public static boolean isSerializable(Object obj) {
if (obj == null) {
return true;
}
try {
MAPPER.writeValueAsString(obj);
return true;
} catch (Exception e) {
log.debug("对象不可序列化: {}", obj.getClass().getName());
return false;
}
}
/**
* 格式化 JSON 字符串美化输出
* <p>适用于日志输出调试等场景
*
* @param json JSON 字符串
* @return 格式化后的 JSON 字符串格式化失败返回原字符串
*/
public static String prettyFormat(String json) {
if (json == null || json.isEmpty()) {
return json;
}
try {
Object obj = MAPPER.readValue(json, Object.class);
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.debug("JSON 格式化失败,返回原字符串", e);
return json;
}
}
/**
* 压缩 JSON 字符串移除所有空白字符
* <p>适用于网络传输存储等场景
*
* @param json JSON 字符串
* @return 压缩后的 JSON 字符串压缩失败返回原字符串
*/
public static String compactFormat(String json) {
if (json == null || json.isEmpty()) {
return json;
}
try {
Object obj = MAPPER.readValue(json, Object.class);
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.debug("JSON 压缩失败,返回原字符串", e);
return json;
}
}
}

View File

@ -533,5 +533,20 @@ public class RedisUtil {
return 0;
}
}
/**
* 根据pattern查找所有匹配的key
*
* @param pattern 匹配模式支持通配符*
* @return 匹配的key集合
*/
public Set<String> keys(String pattern) {
try {
return redisTemplate.keys(pattern);
} catch (Exception e) {
log.error("Redis keys error: pattern={}", pattern, e);
return Collections.emptySet();
}
}
}

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.notification.adapter.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter;
import com.qqchen.deploy.backend.notification.dto.EmailNotificationConfig;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest;
@ -33,7 +34,7 @@ public class EmailChannelAdapter implements INotificationChannelAdapter {
@Override
public void send(Map<String, Object> config, NotificationRequest request) throws Exception {
// 1. 解析配置
EmailNotificationConfig emailConfig = objectMapper.convertValue(config, EmailNotificationConfig.class);
EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class);
validateEmailConfig(emailConfig);
@ -89,7 +90,7 @@ public class EmailChannelAdapter implements INotificationChannelAdapter {
@Override
public String validateConfig(Map<String, Object> config) {
try {
EmailNotificationConfig emailConfig = objectMapper.convertValue(config, EmailNotificationConfig.class);
EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class);
validateEmailConfig(emailConfig);
return "配置有效";
} catch (Exception e) {
@ -100,7 +101,7 @@ public class EmailChannelAdapter implements INotificationChannelAdapter {
@Override
public void testConnection(Map<String, Object> config) throws Exception {
// 1. 解析配置
EmailNotificationConfig emailConfig = objectMapper.convertValue(config, EmailNotificationConfig.class);
EmailNotificationConfig emailConfig = JsonUtils.fromMap(config, EmailNotificationConfig.class);
// 2. 校验配置
validateEmailConfig(emailConfig);

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.notification.adapter.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.adapter.INotificationChannelAdapter;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkNotificationConfig;
@ -35,7 +36,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
@Override
public void send(Map<String, Object> config, NotificationRequest request) throws Exception {
// 1. 解析配置
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class);
WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
throw new IllegalArgumentException("企业微信Webhook URL未配置");
@ -104,7 +105,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
@Override
public String validateConfig(Map<String, Object> config) {
try {
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class);
WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
return "Webhook URL不能为空";
@ -123,7 +124,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
@Override
public void testConnection(Map<String, Object> config) throws Exception {
// 1. 解析配置
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class);
WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
throw new IllegalArgumentException("企业微信Webhook URL未配置");

View File

@ -2,6 +2,7 @@ package com.qqchen.deploy.backend.workflow.delegate;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
@ -165,7 +166,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
Map<String, Object> resolvedMap = resolveExpressions(inputMap, execution);
// 转换为强类型对象
return objectMapper.convertValue(resolvedMap, inputClass);
return JsonUtils.convert(resolvedMap, inputClass);
} catch (Exception e) {
log.error("Failed to parse input mapping", e);

View File

@ -2,6 +2,7 @@ package com.qqchen.deploy.backend.workflow.model;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import lombok.Data;
import java.util.HashMap;
@ -68,14 +69,12 @@ public class NodeContext<I, O> {
}
if (inputMapping != null) {
Map<String, Object> inputMap = objectMapper.convertValue(inputMapping,
new TypeReference<Map<String, Object>>() {});
Map<String, Object> inputMap = JsonUtils.toMapFast(inputMapping);
map.put("inputMapping", inputMap);
}
if (outputs != null) {
Map<String, Object> outputsMap = objectMapper.convertValue(outputs,
new TypeReference<Map<String, Object>>() {});
Map<String, Object> outputsMap = JsonUtils.toMapFast(outputs);
// 只保存嵌套的 outputs保持数据结构规范
map.put("outputs", outputsMap);
@ -102,13 +101,13 @@ public class NodeContext<I, O> {
if (map.containsKey("inputMapping") && inputClass != null) {
Object inputObj = map.get("inputMapping");
I input = objectMapper.convertValue(inputObj, inputClass);
I input = JsonUtils.convert(inputObj, inputClass);
context.setInputMapping(input);
}
if (map.containsKey("outputs") && outputClass != null) {
Object outputsObj = map.get("outputs");
O output = objectMapper.convertValue(outputsObj, outputClass);
O output = JsonUtils.convert(outputsObj, outputClass);
context.setOutputs(output);
}