diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java index 17c2c1c9..32566b17 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java @@ -554,39 +554,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration } } - // 提取 gitCommitId:优先 changeSet/changeSets,兜底 actions.BuildData.lastBuiltRevision.SHA1 - String commitId = null; - try { - // 1. 先尝试从 changeSet(单数,FreeStyle 任务)获取 - if (buildResponse.getChangeSet() != null && - buildResponse.getChangeSet().getItems() != null && - !buildResponse.getChangeSet().getItems().isEmpty()) { - // 取最后一个 commit(最新的) - var items = buildResponse.getChangeSet().getItems(); - commitId = items.get(items.size() - 1).getCommitId(); - } - // 2. 兜底:解析原始 JSON 的 actions 查找 BuildData - if (commitId == null) { - com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(response.getBody()); - com.fasterxml.jackson.databind.JsonNode actions = root.get("actions"); - if (actions != null && actions.isArray()) { - for (com.fasterxml.jackson.databind.JsonNode act : actions) { - com.fasterxml.jackson.databind.JsonNode clazz = act.get("_class"); - if (clazz != null && "hudson.plugins.git.util.BuildData".equals(clazz.asText())) { - com.fasterxml.jackson.databind.JsonNode lbr = act.get("lastBuiltRevision"); - if (lbr != null && lbr.get("SHA1") != null) { - commitId = lbr.get("SHA1").asText(); - break; - } - } - } - } - } - } catch (Exception ex) { - log.warn("Failed to extract git commit id from build details: {}", ex.getMessage()); - } - buildResponse.setGitCommitId(commitId); - // 拼接制品完整URL串(逗号分隔) try { if (buildResponse.getArtifacts() != null && !buildResponse.getArtifacts().isEmpty()) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java index f7a928d7..4e924b4e 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java @@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.deploy.integration.response; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -22,12 +23,6 @@ public class JenkinsBuildResponse { */ private String url; - /** - * 构建类型 - */ - @JsonProperty("_class") - private String buildClass; - /** * 构建结果 */ @@ -88,11 +83,6 @@ public class JenkinsBuildResponse { */ private String id; - /** - * 保持永久 - */ - private Boolean keepLog; - /** * 构建队列ID */ @@ -108,6 +98,11 @@ public class JenkinsBuildResponse { */ private ChangeSet changeSet; + /** + * 构建变更集(Jenkins Pipeline 任务返回 changeSets 复数形式) + */ + private List changeSets; + /** * 构建制品 */ @@ -116,16 +111,35 @@ public class JenkinsBuildResponse { * 构建制品URL(逗号分隔的完整URL串),由服务层拼接 */ private String artifactUrl; - - /** - * 构建控制台输出 - */ - private String consoleLog; /** - * 提取的Git提交ID(兜底自 actions.BuildData.lastBuiltRevision.SHA1 或 changeSets) + * 获取所有变更集(统一处理 FreeStyle 的 changeSet 和 Pipeline 的 changeSets) + * 调用方无需关心底层差异,直接使用此方法获取所有变更集 */ - private String gitCommitId; + public List getAllChangeSets() { + List result = new ArrayList<>(); + if (changeSet != null) { + result.add(changeSet); + } + if (changeSets != null) { + result.addAll(changeSets); + } + return result; + } + + /** + * 获取所有变更集中的提交记录(统一处理 FreeStyle 和 Pipeline) + * 调用方无需关心底层差异,直接使用此方法获取所有 commit 信息 + */ + public List getAllChangeSetItems() { + List items = new ArrayList<>(); + for (ChangeSet cs : getAllChangeSets()) { + if (cs.getItems() != null) { + items.addAll(cs.getItems()); + } + } + return items; + } @Data @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildRepository.java index 73446884..cde78a3f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildRepository.java @@ -75,4 +75,27 @@ public interface IJenkinsBuildRepository extends IBaseRepository findByExternalSystemIdAndStarttimeAfter( Long externalSystemId, LocalDateTime starttime); -} \ No newline at end of file + + /** + * 批量查询多个任务的最新构建记录(用于解决N+1查询问题) + * + * @param externalSystemId 外部系统ID + * @param jobIds 任务ID集合 + * @return 最新构建记录列表 + */ + @Query(""" + SELECT b FROM JenkinsBuild b + WHERE b.externalSystemId = :externalSystemId + AND b.jobId IN :jobIds + AND (b.jobId, b.buildNumber) IN ( + SELECT b2.jobId, MAX(b2.buildNumber) + FROM JenkinsBuild b2 + WHERE b2.externalSystemId = :externalSystemId + AND b2.jobId IN :jobIds + GROUP BY b2.jobId + ) + """) + List findLatestBuildsByJobIds( + @Param("externalSystemId") Long externalSystemId, + @Param("jobIds") Collection jobIds); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java index 5b821c1b..202987f8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java @@ -40,6 +40,7 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; @@ -55,6 +56,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -183,7 +185,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl jobIds = jobs.stream().map(JenkinsJob::getId).toList(); + List latestBuilds = jenkinsBuildRepository.findLatestBuildsByJobIds( + externalSystem.getId(), jobIds); + Map latestBuildMap = latestBuilds.stream() + .collect(Collectors.toMap(JenkinsBuild::getJobId, build -> build)); + + // 4. 创建任务名称到响应的映射 Map jobResponseMap = jobResponses.stream() .collect(Collectors.toMap(JenkinsJobResponse::getName, job -> job)); - // 4. 同步每个任务的构建信息 + // 5. 同步每个任务的构建信息 return jobs.stream() .map(job -> { JenkinsJobResponse jobResponse = jobResponseMap.get(job.getJobName()); - return syncJob(externalSystem, job, jobResponse); + JenkinsBuild latestBuild = latestBuildMap.get(job.getId()); + return syncJob(externalSystem, job, jobResponse, latestBuild); }) .mapToInt(Integer::intValue) .sum(); @@ -227,6 +236,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl build = jenkinsBuildRepository + .findTopByExternalSystemIdAndJobIdOrderByBuildNumberDesc(externalSystem.getId(), job.getId()); + lastSyncedBuildNumber = build.map(JenkinsBuild::getBuildNumber).orElse(null); + } - // 1. 判断是否有新构建(对比本地缓存的 lastBuildNumber 和 Jenkins 最新的) + // 2. 判断是否有新构建(基于Build表实际数据判断) if (lastSyncedBuildNumber != null && latestBuildNumber <= lastSyncedBuildNumber) { log.info("No new builds to sync for job: {} (last synced: {}, latest: {})", job.getJobName(), lastSyncedBuildNumber, latestBuildNumber); return 0; } - // 2. 确定同步范围 + // 3. 确定同步范围 Integer fromBuildNumber = (lastSyncedBuildNumber != null) ? lastSyncedBuildNumber + 1 : 1; - // 3. 获取构建信息 + // 4. 获取构建信息 List builds = jenkinsServiceIntegration.listBuilds(externalSystem, job.getJobName()); - // 4. 过滤出需要同步的构建 + // 5. 过滤出需要同步的构建 List newBuilds = builds.stream() .filter(build -> build.getNumber() >= fromBuildNumber && build.getNumber() <= latestBuildNumber) .collect(Collectors.toList()); @@ -258,10 +281,10 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl builds) { - List jenkinsBuilds = builds.stream() - .map(buildResponse -> { - JenkinsBuild build = new JenkinsBuild(); - build.setExternalSystemId(externalSystem.getId()); - build.setJobId(job.getId()); - build.setBuildNumber(buildResponse.getNumber()); + List jenkinsBuilds = new ArrayList<>(); + + for (JenkinsBuildResponse buildResponse : builds) { + JenkinsBuild build = new JenkinsBuild(); + build.setExternalSystemId(externalSystem.getId()); + build.setJobId(job.getId()); + build.setBuildNumber(buildResponse.getNumber()); + + try { + // 调用 getBuildDetails 获取完整信息(包含 changeSets 等) + JenkinsBuildResponse detailedBuild = jenkinsServiceIntegration.getBuildDetails(externalSystem, job.getJobName(), buildResponse.getNumber()); + if (detailedBuild != null) { + updateBuildFromResponse(build, detailedBuild); + } else { + updateBuildFromResponse(build, buildResponse); + } + } catch (Exception e) { + log.warn("获取构建详情失败,使用基础信息: job={}, build={}", job.getJobName(), buildResponse.getNumber()); updateBuildFromResponse(build, buildResponse); - return build; - }) - .collect(Collectors.toList()); + } + + jenkinsBuilds.add(build); + } log.info("Saving {} builds for job: {}", jenkinsBuilds.size(), job.getJobName()); jenkinsBuildRepository.saveAll(jenkinsBuilds); @@ -304,8 +340,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl buildData = new HashMap<>(); - buildData.put("actions", response.getActions()); - buildData.put("changeSet", response.getChangeSet()); - buildData.put("gitCommitId", response.getGitCommitId()); - String actionsJson = objectMapper.writeValueAsString(buildData); - jenkinsBuild.setActions(actionsJson); + String changeSetsJson = objectMapper.writeValueAsString(response.getAllChangeSets()); + jenkinsBuild.setActions(changeSetsJson); } catch (JsonProcessingException e) { - log.error("Failed to serialize build actions", e); - jenkinsBuild.setActions("{}"); + log.error("Failed to serialize changeSets", e); + jenkinsBuild.setActions("[]"); } } @@ -543,25 +572,26 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl有记录但buildEndNotice=false:检查是否完成,发送结束通知 * */ - private void processBuildNotification( - TeamEnvironmentNotificationConfig config, - NotificationChannel channel, - JenkinsJob job, - JenkinsBuild build, - String latestStatus, - ExternalSystem externalSystem, - Application application, - Environment environment, - JenkinsBuildNotification existingRecord) { + private void processBuildNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, String latestStatus, ExternalSystem externalSystem, Application application, Environment environment, JenkinsBuildNotification existingRecord) { try { // 计算构建开始到现在的分钟数 - long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between(build.getStarttime(), LocalDateTime.now()); + long minutesAgo = ChronoUnit.MINUTES.between(build.getStarttime(), LocalDateTime.now()); // 1. 新构建(通知记录不存在) if (existingRecord == null) { @@ -625,8 +646,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl= 2) { - log.warn("构建超时未完成,强制标记结束: job={}, build={}, startTime={}", - job.getJobName(), build.getBuildNumber(), build.getStarttime()); + log.warn("构建超时未完成,强制标记结束: job={}, build={}, startTime={}", job.getJobName(), build.getBuildNumber(), build.getStarttime()); existingRecord.setBuildEndNotice(true); jenkinsBuildNotificationRepository.save(existingRecord); } } } - } catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) { - log.warn("构建通知记录乐观锁冲突,跳过: teamId={}, envId={}, buildId={}", - config.getTeamId(), config.getEnvironmentId(), build.getId()); + } catch (ObjectOptimisticLockingFailureException e) { + log.warn("构建通知记录乐观锁冲突,跳过: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId()); } catch (Exception e) { - log.error("处理构建通知失败: teamId={}, envId={}, buildId={}", - config.getTeamId(), config.getEnvironmentId(), build.getId(), e); + log.error("处理构建通知失败: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId(), e); } } @@ -680,14 +696,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl templateParams) { - if (actionsJson == null || actionsJson.isEmpty()) { - log.debug("actionsJson 为空,跳过解析"); + private void parseCommitInfoFromActions(String changeSetsJson, Map templateParams) { + if (changeSetsJson == null || changeSetsJson.isEmpty()) { return; } try { - log.debug("开始解析 actionsJson: {}", actionsJson); - JsonNode root = objectMapper.readTree(actionsJson); + // 直接反序列化为 List + List changeSets = objectMapper.readValue( + changeSetsJson, + new com.fasterxml.jackson.core.type.TypeReference>() { + } + ); - // 解析 gitCommitId - if (root.has("gitCommitId") && !root.get("gitCommitId").isNull()) { - templateParams.put("gitCommitId", root.get("gitCommitId").asText()); + if (changeSets == null || changeSets.isEmpty()) { + return; } - // 解析 changeSets 中的 commit 信息 - JsonNode changeSets = root.get("changeSets"); - log.debug("changeSets 节点: {}", changeSets); + StringBuilder commitMessages = new StringBuilder(); + for (JenkinsBuildResponse.ChangeSet changeSet : changeSets) { + if (changeSet.getItems() == null) continue; + for (JenkinsBuildResponse.ChangeSetItem item : changeSet.getItems()) { + if (item.getMsg() == null || item.getMsg().isEmpty()) continue; - if (changeSets != null && changeSets.isArray() && changeSets.size() > 0) { - log.debug("changeSets 数组大小: {}", changeSets.size()); - StringBuilder commitMessages = new StringBuilder(); - for (JsonNode changeSet : changeSets) { - JsonNode items = changeSet.get("items"); - log.debug("items 节点: {}", items); - - if (items != null && items.isArray()) { - log.debug("items 数组大小: {}", items.size()); - for (JsonNode item : items) { - log.debug("处理 item: {}", item); - - // Jenkins API 返回的字段名是 msg - String msg = item.has("msg") ? item.get("msg").asText() : ""; - - // author 是一个对象,需要提取 fullName 或其他字段 - String author = ""; - if (item.has("author")) { - JsonNode authorNode = item.get("author"); - log.debug("author 节点: {}", authorNode); - - if (authorNode.isTextual()) { - // 如果是字符串,直接使用 - author = authorNode.asText(); - } else if (authorNode.isObject()) { - // 如果是对象,尝试提取 fullName - if (authorNode.has("fullName")) { - author = authorNode.get("fullName").asText(); - } else if (authorNode.has("name")) { - author = authorNode.get("name").asText(); - } - } - } - - String commitId = item.has("commitId") ? item.get("commitId").asText() : ""; - - log.debug("解析结果 - msg: {}, author: {}, commitId: {}", msg, author, commitId); - - if (!msg.isEmpty()) { - if (commitMessages.length() > 0) { - commitMessages.append("\n"); - } - // 截取 commitId 前8位 - String shortCommitId = commitId.length() > 8 ? commitId.substring(0, 8) : commitId; - commitMessages.append(String.format("[%s] %s - %s", shortCommitId, msg.trim(), author)); - } - } + if (commitMessages.length() > 0) { + commitMessages.append("\n"); } + String commitId = item.getCommitId() != null ? item.getCommitId() : ""; + String shortCommitId = commitId.length() > 8 ? commitId.substring(0, 8) : commitId; + String author = item.getAuthor() != null && item.getAuthor().getFullName() != null + ? item.getAuthor().getFullName() : ""; + commitMessages.append(String.format("[%s] %s - %s", shortCommitId, item.getMsg().trim(), author)); } - if (commitMessages.length() > 0) { - log.info("成功解析 commitMessage: {}", commitMessages.toString()); - templateParams.put("commitMessage", commitMessages.toString()); - } else { - log.warn("commitMessages 为空"); - } - } else { - log.warn("changeSets 为空或不是数组"); + } + + if (commitMessages.length() > 0) { + templateParams.put("commitMessage", commitMessages.toString()); } } catch (Exception e) { log.error("解析 commit 信息失败", e); @@ -1073,7 +1040,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl