打印了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 = "获取当前登录用户在各团队中可部署的环境和应用列表") @Operation(summary = "获取可部署的环境", description = "获取当前登录用户在各团队中可部署的环境和应用列表")
@GetMapping("/environments") @GetMapping("/environments")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public Response<UserDeployableDTO> getDeployableEnvironments() { public Response<List<UserTeamDeployableDTO>> getDeployableEnvironments() {
return Response.success(deployService.getDeployableEnvironments()); return Response.success(deployService.getDeployableEnvironments());
} }
@ -50,7 +50,7 @@ public class DeployApiController {
@Operation(summary = "执行部署", description = "根据团队应用配置启动部署工作流") @Operation(summary = "执行部署", description = "根据团队应用配置启动部署工作流")
@PostMapping("/execute") @PostMapping("/execute")
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployRequestDTO request) { public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployExecuteRequest request) {
return Response.success(deployService.executeDeploy(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 = "审批人列表") @Schema(description = "审批人列表")
private List<ApproverDTO> approvers; private List<ApproverDTO> approvers;
@Schema(description = "是否启用部署通知")
private Boolean notificationEnabled;
@Schema(description = "通知渠道ID")
private Long notificationChannelId;
@Schema(description = "是否要求代码审查") @Schema(description = "是否要求代码审查")
private Boolean requireCodeReview; 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 用户可部署环境DTO * 用户可部署团队成员DTO
* *
* @author qqchen * @author qqchen
* @since 2025-11-02 * @since 2025-11-06
*/ */
@Data @Data
@Schema(description = "用户可部署环境信息") @Schema(description = "团队成员信息")
public class UserDeployableDTO { public class UserDeployableTeamMemberDTO {
@Schema(description = "用户ID") @Schema(description = "用户ID")
private Long userId; private Long userId;
@ -24,7 +22,7 @@ public class UserDeployableDTO {
@Schema(description = "真实姓名") @Schema(description = "真实姓名")
private String realName; private String realName;
@Schema(description = "用户所属团队列表") @Schema(description = "团队角色")
private List<TeamDeployableDTO> teams; 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查询所有团队成员记录 * 根据用户ID查询所有团队成员记录
*/ */
List<TeamMember> findByUserIdAndDeletedFalse(Long userId); 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 部署备注 * @param deployRemark 部署备注
* @return 部署记录DTO * @return 部署记录DTO
*/ */
DeployRecordDTO createDeployRecord( DeployRecordDTO createDeployRecord(Long workflowInstanceId, String businessKey, Long teamApplicationId, Long teamId, Long applicationId, Long environmentId, String deployBy, String deployRemark);
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 部署请求 * @param request 部署请求
* @return 部署结果 * @return 部署结果
*/ */
DeployResultDTO executeDeploy(DeployRequestDTO request); DeployResultDTO executeDeploy(DeployExecuteRequest request);
/** /**
* 获取当前用户的部署审批任务列表 * 获取当前用户的部署审批任务列表

View File

@ -56,7 +56,7 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, DeployRecordDTO, DeployRecordQuery, Long> public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, DeployRecordDTO, DeployRecordQuery, Long>
implements IDeployRecordService { implements IDeployRecordService {
@Resource @Resource
private IDeployRecordRepository deployRecordRepository; private IDeployRecordRepository deployRecordRepository;
@ -84,16 +84,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
@Override @Override
@Transactional @Transactional
public DeployRecordDTO createDeployRecord( public DeployRecordDTO createDeployRecord(Long workflowInstanceId, String businessKey, Long teamApplicationId, Long teamId, Long applicationId, Long environmentId, String deployBy, String deployRemark) {
Long workflowInstanceId,
String businessKey,
Long teamApplicationId,
Long teamId,
Long applicationId,
Long environmentId,
String deployBy,
String deployRemark
) {
// 检查是否已存在 // 检查是否已存在
DeployRecord existing = deployRecordRepository.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId).orElse(null); DeployRecord existing = deployRecordRepository.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId).orElse(null);
@ -125,8 +116,8 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
@Transactional @Transactional
public void syncStatusFromWorkflowInstance(WorkflowInstance instance, WorkflowInstanceStatusEnums status) { public void syncStatusFromWorkflowInstance(WorkflowInstance instance, WorkflowInstanceStatusEnums status) {
DeployRecord record = deployRecordRepository DeployRecord record = deployRecordRepository
.findByWorkflowInstanceIdAndDeletedFalse(instance.getId()) .findByWorkflowInstanceIdAndDeletedFalse(instance.getId())
.orElse(null); .orElse(null);
if (record == null) { if (record == null) {
log.warn("部署记录不存在,无法同步状态: workflowInstanceId={}", instance.getId()); log.warn("部署记录不存在,无法同步状态: workflowInstanceId={}", instance.getId());
@ -136,7 +127,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
// 如果当前状态已经是终态特别是审批被拒绝的REJECTED不应该被覆盖 // 如果当前状态已经是终态特别是审批被拒绝的REJECTED不应该被覆盖
if (isFinalState(record.getStatus())) { if (isFinalState(record.getStatus())) {
log.debug("部署记录已处于终态,跳过同步: id={}, workflowInstanceId={}, currentStatus={}, workflowStatus={}", log.debug("部署记录已处于终态,跳过同步: id={}, workflowInstanceId={}, currentStatus={}, workflowStatus={}",
record.getId(), instance.getId(), record.getStatus(), status); record.getId(), instance.getId(), record.getStatus(), status);
// 只更新时间不更新状态 // 只更新时间不更新状态
if (instance.getEndTime() != null && record.getEndTime() == null) { if (instance.getEndTime() != null && record.getEndTime() == null) {
record.setEndTime(instance.getEndTime()); record.setEndTime(instance.getEndTime());
@ -159,23 +150,23 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
deployRecordRepository.save(record); deployRecordRepository.save(record);
log.debug("同步部署记录状态: id={}, workflowInstanceId={}, status={}", log.debug("同步部署记录状态: id={}, workflowInstanceId={}, status={}",
record.getId(), instance.getId(), deployStatus); record.getId(), instance.getId(), deployStatus);
} }
@Override @Override
public DeployRecordDTO findByWorkflowInstanceId(Long workflowInstanceId) { public DeployRecordDTO findByWorkflowInstanceId(Long workflowInstanceId) {
return deployRecordRepository return deployRecordRepository
.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId) .findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId)
.map(deployRecordConverter::toDto) .map(deployRecordConverter::toDto)
.orElse(null); .orElse(null);
} }
@Override @Override
@Transactional @Transactional
public void updateStatusToPendingApproval(Long workflowInstanceId) { public void updateStatusToPendingApproval(Long workflowInstanceId) {
DeployRecord record = deployRecordRepository DeployRecord record = deployRecordRepository
.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId) .findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId)
.orElse(null); .orElse(null);
if (record == null) { if (record == null) {
log.warn("部署记录不存在,无法更新状态: workflowInstanceId={}", workflowInstanceId); log.warn("部署记录不存在,无法更新状态: workflowInstanceId={}", workflowInstanceId);
@ -184,23 +175,23 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
// 只有非终态才能更新为待审批 // 只有非终态才能更新为待审批
if (isFinalState(record.getStatus())) { if (isFinalState(record.getStatus())) {
log.debug("部署记录已处于终态,跳过更新: workflowInstanceId={}, status={}", log.debug("部署记录已处于终态,跳过更新: workflowInstanceId={}, status={}",
workflowInstanceId, record.getStatus()); workflowInstanceId, record.getStatus());
return; return;
} }
record.setStatus(DeployRecordStatusEnums.PENDING_APPROVAL); record.setStatus(DeployRecordStatusEnums.PENDING_APPROVAL);
deployRecordRepository.save(record); deployRecordRepository.save(record);
log.debug("部署记录状态已更新为待审批: id={}, workflowInstanceId={}", log.debug("部署记录状态已更新为待审批: id={}, workflowInstanceId={}",
record.getId(), workflowInstanceId); record.getId(), workflowInstanceId);
} }
@Override @Override
@Transactional @Transactional
public void updateStatusFromApproval(Long workflowInstanceId, boolean approved) { public void updateStatusFromApproval(Long workflowInstanceId, boolean approved) {
DeployRecord record = deployRecordRepository DeployRecord record = deployRecordRepository
.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId) .findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId)
.orElse(null); .orElse(null);
if (record == null) { if (record == null) {
log.warn("部署记录不存在,无法更新状态: workflowInstanceId={}", workflowInstanceId); log.warn("部署记录不存在,无法更新状态: workflowInstanceId={}", workflowInstanceId);
@ -209,21 +200,21 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
// 只有非终态才能更新 // 只有非终态才能更新
if (isFinalState(record.getStatus())) { if (isFinalState(record.getStatus())) {
log.debug("部署记录已处于终态,跳过更新: workflowInstanceId={}, status={}", log.debug("部署记录已处于终态,跳过更新: workflowInstanceId={}, status={}",
workflowInstanceId, record.getStatus()); workflowInstanceId, record.getStatus());
return; return;
} }
if (approved) { if (approved) {
// 审批通过更新为运行中 // 审批通过更新为运行中
record.setStatus(DeployRecordStatusEnums.RUNNING); record.setStatus(DeployRecordStatusEnums.RUNNING);
log.info("部署记录状态已更新为运行中(审批通过): id={}, workflowInstanceId={}", log.info("部署记录状态已更新为运行中(审批通过): id={}, workflowInstanceId={}",
record.getId(), workflowInstanceId); record.getId(), workflowInstanceId);
} else { } else {
// 审批被拒绝更新为审批被拒绝 // 审批被拒绝更新为审批被拒绝
record.setStatus(DeployRecordStatusEnums.REJECTED); record.setStatus(DeployRecordStatusEnums.REJECTED);
log.info("部署记录状态已更新为审批被拒绝: id={}, workflowInstanceId={}", log.info("部署记录状态已更新为审批被拒绝: id={}, workflowInstanceId={}",
record.getId(), workflowInstanceId); record.getId(), workflowInstanceId);
} }
deployRecordRepository.save(record); deployRecordRepository.save(record);
@ -233,16 +224,16 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
@Transactional(readOnly = true) @Transactional(readOnly = true)
public DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId) { public DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId) {
log.info("查询部署流程图: deployRecordId={}", deployRecordId); log.info("查询部署流程图: deployRecordId={}", deployRecordId);
// ============ 1. 查询基础数据 ============ // ============ 1. 查询基础数据 ============
// 1.1 查询部署记录 // 1.1 查询部署记录
DeployRecord deployRecord = deployRecordRepository.findById(deployRecordId) DeployRecord deployRecord = deployRecordRepository.findById(deployRecordId)
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"部署记录"})); .orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[] {"部署记录"}));
// 1.2 查询工作流实例 // 1.2 查询工作流实例
WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId()) WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId())
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"工作流实例"})); .orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[] {"工作流实例"}));
// 1.3 获取流程图快照 // 1.3 获取流程图快照
WorkflowDefinitionGraph graphSnapshot = workflowInstance.getGraphSnapshot(); WorkflowDefinitionGraph graphSnapshot = workflowInstance.getGraphSnapshot();
@ -257,13 +248,13 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
Environment environment = environmentRepository.findById(deployRecord.getEnvironmentId()).orElse(null); Environment environment = environmentRepository.findById(deployRecord.getEnvironmentId()).orElse(null);
// ============ 2. 查询并组装节点执行状态 ============ // ============ 2. 查询并组装节点执行状态 ============
// 2.1 查询已执行的节点实例列表 // 2.1 查询已执行的节点实例列表
List<WorkflowNodeInstance> nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId()); List<WorkflowNodeInstance> nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId());
// 2.2 构建已执行节点的映射按nodeId索引 // 2.2 构建已执行节点的映射按nodeId索引
Map<String, WorkflowNodeInstance> nodeInstanceMap = nodeInstances.stream() Map<String, WorkflowNodeInstance> nodeInstanceMap = nodeInstances.stream()
.collect(Collectors.toMap(WorkflowNodeInstance::getNodeId, instance -> instance, (a, b) -> a)); .collect(Collectors.toMap(WorkflowNodeInstance::getNodeId, instance -> instance, (a, b) -> a));
// 2.3 从BPMN模型中获取流程元素顺序用于排序 // 2.3 从BPMN模型中获取流程元素顺序用于排序
BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId()); BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId());
@ -278,30 +269,30 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
// 2.5 遍历流程图中的所有节点组装节点DTO包括已执行和未执行的 // 2.5 遍历流程图中的所有节点组装节点DTO包括已执行和未执行的
List<WorkflowNodeInstanceDTO> nodeInstanceDTOs = graphSnapshot.getNodes().stream() List<WorkflowNodeInstanceDTO> nodeInstanceDTOs = graphSnapshot.getNodes().stream()
.sorted(Comparator.comparingInt(node -> nodeOrderMap.getOrDefault(node.getId(), Integer.MAX_VALUE))) .sorted(Comparator.comparingInt(node -> nodeOrderMap.getOrDefault(node.getId(), Integer.MAX_VALUE)))
.map(graphNode -> buildNodeInstanceDTO(graphNode, nodeInstanceMap.get(graphNode.getId()), workflowInstance)) .map(graphNode -> buildNodeInstanceDTO(graphNode, nodeInstanceMap.get(graphNode.getId()), workflowInstance))
.collect(Collectors.toList()); .collect(Collectors.toList());
// ============ 3. 计算执行统计 ============ // ============ 3. 计算执行统计 ============
int totalNodeCount = nodeInstanceDTOs.size(); int totalNodeCount = nodeInstanceDTOs.size();
int executedNodeCount = (int) nodeInstanceDTOs.stream() int executedNodeCount = (int) nodeInstanceDTOs.stream()
.filter(node -> node.getStatus() != WorkflowNodeInstanceStatusEnums.NOT_STARTED) .filter(node -> node.getStatus() != WorkflowNodeInstanceStatusEnums.NOT_STARTED)
.count(); .count();
int successNodeCount = (int) nodeInstanceDTOs.stream() int successNodeCount = (int) nodeInstanceDTOs.stream()
.filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.COMPLETED) .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.COMPLETED)
.count(); .count();
int failedNodeCount = (int) nodeInstanceDTOs.stream() int failedNodeCount = (int) nodeInstanceDTOs.stream()
.filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.FAILED) .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.FAILED)
.count(); .count();
int runningNodeCount = (int) nodeInstanceDTOs.stream() int runningNodeCount = (int) nodeInstanceDTOs.stream()
.filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.RUNNING) .filter(node -> node.getStatus() == WorkflowNodeInstanceStatusEnums.RUNNING)
.count(); .count();
// ============ 4. 组装完整的DTO ============ // ============ 4. 组装完整的DTO ============
DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO(); DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO();
// 4.1 部署记录基本信息 // 4.1 部署记录基本信息
dto.setDeployRecordId(deployRecord.getId()); dto.setDeployRecordId(deployRecord.getId());
dto.setBusinessKey(deployRecord.getBusinessKey()); dto.setBusinessKey(deployRecord.getBusinessKey());
@ -337,27 +328,27 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
dto.setGraph(graphSnapshot); dto.setGraph(graphSnapshot);
dto.setNodeInstances(nodeInstanceDTOs); dto.setNodeInstances(nodeInstanceDTOs);
log.info("获取部署流程图成功: deployRecordId={}, 总节点数={}, 已执行={}, 成功={}, 失败={}, 运行中={}", log.info("获取部署流程图成功: deployRecordId={}, 总节点数={}, 已执行={}, 成功={}, 失败={}, 运行中={}",
deployRecordId, totalNodeCount, executedNodeCount, successNodeCount, failedNodeCount, runningNodeCount); deployRecordId, totalNodeCount, executedNodeCount, successNodeCount, failedNodeCount, runningNodeCount);
return dto; return dto;
} }
/** /**
* 构建节点实例DTO * 构建节点实例DTO
* *
* @param graphNode 流程图节点 * @param graphNode 流程图节点
* @param nodeInstance 节点实例可能为null表示未执行 * @param nodeInstance 节点实例可能为null表示未执行
* @param workflowInstance 工作流实例 * @param workflowInstance 工作流实例
* @return 节点实例DTO * @return 节点实例DTO
*/ */
private WorkflowNodeInstanceDTO buildNodeInstanceDTO( private WorkflowNodeInstanceDTO buildNodeInstanceDTO(
WorkflowDefinitionGraphNode graphNode, WorkflowDefinitionGraphNode graphNode,
WorkflowNodeInstance nodeInstance, WorkflowNodeInstance nodeInstance,
WorkflowInstance workflowInstance) { WorkflowInstance workflowInstance) {
WorkflowNodeInstanceDTO dto = new WorkflowNodeInstanceDTO(); WorkflowNodeInstanceDTO dto = new WorkflowNodeInstanceDTO();
if (nodeInstance != null) { if (nodeInstance != null) {
// 已执行的节点使用节点实例的数据 // 已执行的节点使用节点实例的数据
dto.setId(nodeInstance.getId()); dto.setId(nodeInstance.getId());
@ -387,15 +378,15 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
dto.setCreateTime(null); dto.setCreateTime(null);
dto.setUpdateTime(null); dto.setUpdateTime(null);
} }
return dto; return dto;
} }
/** /**
* 计算时长毫秒 * 计算时长毫秒
* *
* @param startTime 开始时间 * @param startTime 开始时间
* @param endTime 结束时间 * @param endTime 结束时间
* @return 时长毫秒如果任一时间为 null 则返回 null * @return 时长毫秒如果任一时间为 null 则返回 null
*/ */
private Long calculateDuration(LocalDateTime startTime, LocalDateTime endTime) { private Long calculateDuration(LocalDateTime startTime, LocalDateTime endTime) {
@ -411,7 +402,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
*/ */
private DeployRecordFlowGraphDTO buildEmptyFlowGraphDTO(DeployRecord deployRecord, WorkflowInstance workflowInstance) { private DeployRecordFlowGraphDTO buildEmptyFlowGraphDTO(DeployRecord deployRecord, WorkflowInstance workflowInstance) {
DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO(); DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO();
// 部署记录基本信息 // 部署记录基本信息
dto.setDeployRecordId(deployRecord.getId()); dto.setDeployRecordId(deployRecord.getId());
dto.setBusinessKey(deployRecord.getBusinessKey()); dto.setBusinessKey(deployRecord.getBusinessKey());
@ -421,7 +412,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
dto.setDeployStartTime(deployRecord.getStartTime()); dto.setDeployStartTime(deployRecord.getStartTime());
dto.setDeployEndTime(deployRecord.getEndTime()); dto.setDeployEndTime(deployRecord.getEndTime());
dto.setDeployDuration(calculateDuration(deployRecord.getStartTime(), deployRecord.getEndTime())); dto.setDeployDuration(calculateDuration(deployRecord.getStartTime(), deployRecord.getEndTime()));
// 工作流实例信息 // 工作流实例信息
dto.setWorkflowInstanceId(workflowInstance.getId()); dto.setWorkflowInstanceId(workflowInstance.getId());
dto.setProcessInstanceId(workflowInstance.getProcessInstanceId()); dto.setProcessInstanceId(workflowInstance.getProcessInstanceId());
@ -429,18 +420,18 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
dto.setWorkflowStartTime(workflowInstance.getStartTime()); dto.setWorkflowStartTime(workflowInstance.getStartTime());
dto.setWorkflowEndTime(workflowInstance.getEndTime()); dto.setWorkflowEndTime(workflowInstance.getEndTime());
dto.setWorkflowDuration(calculateDuration(workflowInstance.getStartTime(), workflowInstance.getEndTime())); dto.setWorkflowDuration(calculateDuration(workflowInstance.getStartTime(), workflowInstance.getEndTime()));
// 执行统计信息都为0 // 执行统计信息都为0
dto.setTotalNodeCount(0); dto.setTotalNodeCount(0);
dto.setExecutedNodeCount(0); dto.setExecutedNodeCount(0);
dto.setSuccessNodeCount(0); dto.setSuccessNodeCount(0);
dto.setFailedNodeCount(0); dto.setFailedNodeCount(0);
dto.setRunningNodeCount(0); dto.setRunningNodeCount(0);
// 流程图数据 // 流程图数据
dto.setGraph(new WorkflowDefinitionGraph()); dto.setGraph(new WorkflowDefinitionGraph());
dto.setNodeInstances(new ArrayList<>()); dto.setNodeInstances(new ArrayList<>());
return dto; return dto;
} }
@ -448,12 +439,12 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
* 判断是否为终态 * 判断是否为终态
*/ */
private boolean isFinalState(DeployRecordStatusEnums status) { private boolean isFinalState(DeployRecordStatusEnums status) {
return status == DeployRecordStatusEnums.SUCCESS return status == DeployRecordStatusEnums.SUCCESS
|| status == DeployRecordStatusEnums.FAILED || status == DeployRecordStatusEnums.FAILED
|| status == DeployRecordStatusEnums.REJECTED || status == DeployRecordStatusEnums.REJECTED
|| status == DeployRecordStatusEnums.CANCELLED || status == DeployRecordStatusEnums.CANCELLED
|| status == DeployRecordStatusEnums.TERMINATED || status == DeployRecordStatusEnums.TERMINATED
|| status == DeployRecordStatusEnums.PARTIAL_SUCCESS; || status == DeployRecordStatusEnums.PARTIAL_SUCCESS;
} }
} }

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

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.notification.adapter.impl; package com.qqchen.deploy.backend.notification.adapter.impl;
import com.fasterxml.jackson.databind.ObjectMapper; 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.adapter.INotificationChannelAdapter;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest; import com.qqchen.deploy.backend.notification.dto.NotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkNotificationConfig; import com.qqchen.deploy.backend.notification.dto.WeworkNotificationConfig;
@ -35,7 +36,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
@Override @Override
public void send(Map<String, Object> config, NotificationRequest request) throws Exception { public void send(Map<String, Object> config, NotificationRequest request) throws Exception {
// 1. 解析配置 // 1. 解析配置
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class); WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) { if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
throw new IllegalArgumentException("企业微信Webhook URL未配置"); throw new IllegalArgumentException("企业微信Webhook URL未配置");
@ -104,7 +105,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
@Override @Override
public String validateConfig(Map<String, Object> config) { public String validateConfig(Map<String, Object> config) {
try { try {
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class); WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) { if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
return "Webhook URL不能为空"; return "Webhook URL不能为空";
@ -123,7 +124,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
@Override @Override
public void testConnection(Map<String, Object> config) throws Exception { public void testConnection(Map<String, Object> config) throws Exception {
// 1. 解析配置 // 1. 解析配置
WeworkNotificationConfig weworkConfig = objectMapper.convertValue(config, WeworkNotificationConfig.class); WeworkNotificationConfig weworkConfig = JsonUtils.fromMap(config, WeworkNotificationConfig.class);
if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) { if (weworkConfig.getWebhookUrl() == null || weworkConfig.getWebhookUrl().isEmpty()) {
throw new IllegalArgumentException("企业微信Webhook URL未配置"); 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.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; 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.framework.utils.SpelExpressionResolver;
import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs; import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum; 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); Map<String, Object> resolvedMap = resolveExpressions(inputMap, execution);
// 转换为强类型对象 // 转换为强类型对象
return objectMapper.convertValue(resolvedMap, inputClass); return JsonUtils.convert(resolvedMap, inputClass);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to parse input mapping", 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.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import lombok.Data; import lombok.Data;
import java.util.HashMap; import java.util.HashMap;
@ -68,14 +69,12 @@ public class NodeContext<I, O> {
} }
if (inputMapping != null) { if (inputMapping != null) {
Map<String, Object> inputMap = objectMapper.convertValue(inputMapping, Map<String, Object> inputMap = JsonUtils.toMapFast(inputMapping);
new TypeReference<Map<String, Object>>() {});
map.put("inputMapping", inputMap); map.put("inputMapping", inputMap);
} }
if (outputs != null) { if (outputs != null) {
Map<String, Object> outputsMap = objectMapper.convertValue(outputs, Map<String, Object> outputsMap = JsonUtils.toMapFast(outputs);
new TypeReference<Map<String, Object>>() {});
// 只保存嵌套的 outputs保持数据结构规范 // 只保存嵌套的 outputs保持数据结构规范
map.put("outputs", outputsMap); map.put("outputs", outputsMap);
@ -102,13 +101,13 @@ public class NodeContext<I, O> {
if (map.containsKey("inputMapping") && inputClass != null) { if (map.containsKey("inputMapping") && inputClass != null) {
Object inputObj = map.get("inputMapping"); Object inputObj = map.get("inputMapping");
I input = objectMapper.convertValue(inputObj, inputClass); I input = JsonUtils.convert(inputObj, inputClass);
context.setInputMapping(input); context.setInputMapping(input);
} }
if (map.containsKey("outputs") && outputClass != null) { if (map.containsKey("outputs") && outputClass != null) {
Object outputsObj = map.get("outputs"); Object outputsObj = map.get("outputs");
O output = objectMapper.convertValue(outputsObj, outputClass); O output = JsonUtils.convert(outputsObj, outputClass);
context.setOutputs(output); context.setOutputs(output);
} }