From 33f648d79ffc4a7714dc4ac35fb5d094fcb2a509 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 28 Nov 2025 13:53:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9E=84=E5=BB=BA=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IJenkinsBuildNotificationRepository.java | 9 + .../service/impl/JenkinsBuildServiceImpl.java | 296 +++++++++--------- 2 files changed, 165 insertions(+), 140 deletions(-) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildNotificationRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildNotificationRepository.java index 73ef86c5..79ef70d3 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildNotificationRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsBuildNotificationRepository.java @@ -4,6 +4,7 @@ import com.qqchen.deploy.backend.deploy.entity.JenkinsBuildNotification; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import org.springframework.stereotype.Repository; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -34,4 +35,12 @@ public interface IJenkinsBuildNotificationRepository * @return 未完成结束通知的记录列表 */ List findByBuildEndNoticeFalseAndDeletedFalse(); + + /** + * 根据构建ID批量查询通知记录 + * + * @param buildIds 构建ID集合 + * @return 通知记录列表 + */ + List findByBuildIdInAndDeletedFalse(Collection buildIds); } 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 74ea9fc5..419e3ede 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 @@ -436,81 +436,56 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl核心流程: + *
    + *
  1. 获取所有绑定了该Jenkins系统的TeamApplication
  2. + *
  3. 按 deployJob 分组,遍历每个 Job
  4. + *
  5. 获取该 Job 的最新构建记录
  6. + *
  7. 查通知记录表,判断是否需要发送通知
  8. + *
*/ private void checkBuildNotifications(Long externalSystemId) { log.info("开始检查构建通知: externalSystemId={}", externalSystemId); try { - // 0. 获取外部系统信息(用于后续获取日志) - ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId) - .orElse(null); + // 1. 获取外部系统信息 + ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId).orElse(null); if (externalSystem == null) { log.warn("外部系统不存在: externalSystemId={}", externalSystemId); return; } - // 1. 查询最近6分钟内开始的新构建(用于发送开始通知) - LocalDateTime recentSince = LocalDateTime.now().minusMinutes(6); - List recentBuilds = jenkinsBuildRepository - .findByExternalSystemIdAndStarttimeAfter(externalSystemId, recentSince); - - // 2. 查询未完成通知的构建ID(用于发送结束通知和兜底处理) - List pendingNotifications = jenkinsBuildNotificationRepository - .findByBuildEndNoticeFalseAndDeletedFalse(); - Set pendingBuildIds = pendingNotifications.stream() - .map(JenkinsBuildNotification::getBuildId) - .collect(Collectors.toSet()); - - // 3. 查询未完成通知对应的构建记录 - List pendingBuilds = pendingBuildIds.isEmpty() - ? Collections.emptyList() - : jenkinsBuildRepository.findAllById(pendingBuildIds).stream() - .filter(b -> b.getExternalSystemId().equals(externalSystemId)) - .toList(); - - // 4. 合并需要处理的构建(去重) - Map buildMap = new HashMap<>(); - recentBuilds.forEach(b -> buildMap.put(b.getId(), b)); - pendingBuilds.forEach(b -> buildMap.put(b.getId(), b)); - List buildsToProcess = new ArrayList<>(buildMap.values()); - - if (buildsToProcess.isEmpty()) { - log.info("没有需要处理的构建记录"); - return; - } - - // 2. 反查团队绑定关系(只查询构建类型为JENKINS的应用) - List teamApps = teamApplicationRepository.findByDeploySystemIdAndBuildType(externalSystemId, BuildTypeEnum.JENKINS); - + // 2. 查询绑定该Jenkins系统的TeamApplication(构建类型为JENKINS) + List teamApps = teamApplicationRepository + .findByDeploySystemIdAndBuildType(externalSystemId, BuildTypeEnum.JENKINS); if (teamApps.isEmpty()) { log.info("没有团队绑定该Jenkins系统: externalSystemId={}", externalSystemId); return; } - // 3. 按 deploy_job 分组 TeamApplication + // 3. 按 deployJob 分组(过滤掉空的deployJob) Map> teamAppsByJob = teamApps.stream() + .filter(ta -> ta.getDeployJob() != null && !ta.getDeployJob().isEmpty()) .collect(Collectors.groupingBy(TeamApplication::getDeployJob)); + if (teamAppsByJob.isEmpty()) { + log.info("没有配置deployJob的团队应用"); + return; + } // 4. 查询启用了构建通知的团队环境配置 - Set teamIds = teamApps.stream() - .map(TeamApplication::getTeamId) - .collect(Collectors.toSet()); - - Set envIds = teamApps.stream() - .map(TeamApplication::getEnvironmentId) - .collect(Collectors.toSet()); - - List configs = - teamEnvironmentNotificationConfigRepository - .findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(teamIds, envIds); - + Set teamIds = teamApps.stream().map(TeamApplication::getTeamId).collect(Collectors.toSet()); + Set envIds = teamApps.stream().map(TeamApplication::getEnvironmentId).collect(Collectors.toSet()); + + List configs = teamEnvironmentNotificationConfigRepository + .findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(teamIds, envIds); if (configs.isEmpty()) { log.info("没有团队启用构建通知"); return; } - // 5. 按 team_id + environment_id 分组配置 + // 5. 构建配置Map(key: teamId_envId) Map configMap = configs.stream() .collect(Collectors.toMap( cfg -> cfg.getTeamId() + "_" + cfg.getEnvironmentId(), @@ -522,63 +497,101 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl channelMap = notificationChannelRepository.findAllById(channelIds) + .stream().collect(Collectors.toMap(NotificationChannel::getId, c -> c)); - Map channelMap = - notificationChannelRepository.findAllById(channelIds).stream() - .collect(Collectors.toMap(NotificationChannel::getId, c -> c)); + // 7. 批量查询应用和环境信息 + Set applicationIds = teamApps.stream().map(TeamApplication::getApplicationId).collect(Collectors.toSet()); + Map applicationMap = applicationRepository.findAllById(applicationIds) + .stream().collect(Collectors.toMap(Application::getId, a -> a)); + Map environmentMap = environmentRepository.findAllById(envIds) + .stream().collect(Collectors.toMap(Environment::getId, e -> e)); - // 7. 批量查询 Job - Set jobIds = buildsToProcess.stream() - .map(JenkinsBuild::getJobId) + // 8. 批量查询所有视图下的Job,构建 jobName -> JenkinsJob 的Map + List views = jenkinsViewRepository.findByExternalSystemId(externalSystemId); + Map jobByNameMap = new HashMap<>(); + for (JenkinsView view : views) { + List jobs = jenkinsJobRepository.findByExternalSystemIdAndViewId(externalSystemId, view.getId()); + for (JenkinsJob job : jobs) { + jobByNameMap.put(job.getJobName(), job); + } + } + + // 9. 收集需要处理的Job及其最新构建 + Map latestBuildByJobId = new HashMap<>(); + Set jobNamesToProcess = teamAppsByJob.keySet(); + for (String jobName : jobNamesToProcess) { + JenkinsJob job = jobByNameMap.get(jobName); + if (job == null) { + log.debug("Job不存在: jobName={}", jobName); + continue; + } + jenkinsBuildRepository.findTopByExternalSystemIdAndJobIdOrderByBuildNumberDesc(externalSystemId, job.getId()) + .ifPresent(build -> latestBuildByJobId.put(job.getId(), build)); + } + + if (latestBuildByJobId.isEmpty()) { + log.info("没有需要处理的构建记录"); + return; + } + + // 10. 批量查询通知记录(避免N+1) + Set buildIds = latestBuildByJobId.values().stream() + .map(JenkinsBuild::getId) .collect(Collectors.toSet()); - - Map jobMap = jenkinsJobRepository - .findAllById(jobIds).stream() - .collect(Collectors.toMap(JenkinsJob::getId, j -> j)); - - // 8. 按 job_name 分组构建记录 - Map> buildsByJobName = buildsToProcess.stream() - .collect(Collectors.groupingBy( - build -> jobMap.get(build.getJobId()).getJobName() + List existingNotifications = jenkinsBuildNotificationRepository + .findByBuildIdInAndDeletedFalse(buildIds); + // 构建 buildId_teamId_envId -> notification 的Map + Map notificationMap = existingNotifications.stream() + .collect(Collectors.toMap( + n -> n.getBuildId() + "_" + n.getTeamId() + "_" + n.getEnvironmentId(), + n -> n )); - // 9. 批量查询应用信息 - Set applicationIds = teamApps.stream() - .map(TeamApplication::getApplicationId) - .collect(Collectors.toSet()); - Map applicationMap = applicationRepository.findAllById(applicationIds).stream() - .collect(Collectors.toMap(Application::getId, a -> a)); + // 11. 遍历每个 Job,处理通知 + for (Map.Entry> entry : teamAppsByJob.entrySet()) { + String jobName = entry.getKey(); + List relatedTeamApps = entry.getValue(); - // 10. 批量查询环境信息 - Map environmentMap = environmentRepository.findAllById(envIds).stream() - .collect(Collectors.toMap(Environment::getId, e -> e)); + JenkinsJob job = jobByNameMap.get(jobName); + if (job == null) continue; - // 11. 处理每个 Job 的构建通知 - buildsByJobName.forEach((jobName, builds) -> { - List relatedTeamApps = teamAppsByJob.get(jobName); - if (relatedTeamApps == null || relatedTeamApps.isEmpty()) { - return; + JenkinsBuild latestBuild = latestBuildByJobId.get(job.getId()); + if (latestBuild == null) continue; + + // 从 Jenkins API 实时获取最新状态(每个Job只调用一次) + JenkinsBuildResponse latestBuildInfo = fetchLatestBuildInfo(externalSystem, job, latestBuild.getBuildNumber()); + String latestStatus = (latestBuildInfo != null && latestBuildInfo.getResult() != null) + ? latestBuildInfo.getResult() + : "BUILDING"; + + // 更新数据库中的构建状态(如果有变化) + if (latestBuildInfo != null && !latestStatus.equals(latestBuild.getBuildStatus())) { + updateBuildFromResponse(latestBuild, latestBuildInfo); + jenkinsBuildRepository.save(latestBuild); } + // 处理每个关联的 TeamApplication for (TeamApplication teamApp : relatedTeamApps) { - String key = teamApp.getTeamId() + "_" + teamApp.getEnvironmentId(); - TeamEnvironmentNotificationConfig config = configMap.get(key); - + String configKey = teamApp.getTeamId() + "_" + teamApp.getEnvironmentId(); + TeamEnvironmentNotificationConfig config = configMap.get(configKey); if (config == null) continue; NotificationChannel channel = channelMap.get(config.getNotificationChannelId()); if (channel == null) continue; - JenkinsJob job = jobMap.get(builds.get(0).getJobId()); Application application = applicationMap.get(teamApp.getApplicationId()); Environment environment = environmentMap.get(teamApp.getEnvironmentId()); - // 处理该团队环境的所有构建通知 - for (JenkinsBuild build : builds) { - processBuildNotification(config, channel, job, build, externalSystem, application, environment); - } + // 从Map中获取通知记录(避免循环内查询) + String notificationKey = latestBuild.getId() + "_" + config.getTeamId() + "_" + config.getEnvironmentId(); + JenkinsBuildNotification existingRecord = notificationMap.get(notificationKey); + + // 处理该构建的通知 + processBuildNotification(config, channel, job, latestBuild, latestStatus, + externalSystem, application, environment, existingRecord); } - }); + } } catch (Exception e) { log.error("检查构建通知失败: externalSystemId={}", externalSystemId, e); @@ -586,82 +599,85 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl参考 longi-deployment 的实现:每次都从 Jenkins API 实时获取最新状态 + * 处理单个构建的通知(参考longi-deployment逻辑) + * + *

核心逻辑: + *

    + *
  • 无记录 = 新构建:超时(>20分钟)直接标记完成,未超时发送"构建中"通知
  • + *
  • 有记录但buildEndNotice=false:检查是否完成,发送结束通知
  • + *
*/ - private void processBuildNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, ExternalSystem externalSystem, Application application, Environment environment) { + private void processBuildNotification( + TeamEnvironmentNotificationConfig config, + NotificationChannel channel, + JenkinsJob job, + JenkinsBuild build, + String latestStatus, + ExternalSystem externalSystem, + Application application, + Environment environment, + JenkinsBuildNotification existingRecord) { try { - // 1. 查询通知记录 - JenkinsBuildNotification record = jenkinsBuildNotificationRepository.findByBuildIdAndTeamIdAndEnvironmentId(build.getId(), config.getTeamId(), config.getEnvironmentId()) - .orElse(null); + // 计算构建开始到现在的分钟数 + long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between(build.getStarttime(), LocalDateTime.now()); - // 2. 从 Jenkins API 实时获取该构建的最新状态 - JenkinsBuildResponse latestBuildInfo = fetchLatestBuildInfo(externalSystem, job, build.getBuildNumber()); - String latestStatus = (latestBuildInfo != null && latestBuildInfo.getResult() != null) - ? latestBuildInfo.getResult() - : "BUILDING"; - - // 3. 新构建(只处理6分钟内的新构建) - if (record == null) { - long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between( - build.getStarttime(), - LocalDateTime.now() - ); - - // 超过6分钟的旧构建,不创建通知记录,直接跳过 - if (minutesAgo > 6) { + // 1. 新构建(通知记录不存在) + if (existingRecord == null) { + // 超过20分钟的旧构建,直接标记完成,不发通知 + if (minutesAgo > 20) { + JenkinsBuildNotification record = new JenkinsBuildNotification(); + record.setBuildId(build.getId()); + record.setTeamId(config.getTeamId()); + record.setEnvironmentId(config.getEnvironmentId()); + record.setBuildStartNotice(true); + record.setBuildEndNotice(true); // 直接标记结束 + jenkinsBuildNotificationRepository.save(record); + log.info("构建超时,直接标记完成: job={}, build={}, minutesAgo={}", + job.getJobName(), build.getBuildNumber(), minutesAgo); return; } - // 6分钟内的新构建,发送"构建中"通知 - record = new JenkinsBuildNotification(); + // 未超时,发送"构建中"通知 + JenkinsBuildNotification record = new JenkinsBuildNotification(); record.setBuildId(build.getId()); record.setTeamId(config.getTeamId()); record.setEnvironmentId(config.getEnvironmentId()); sendNotification(config, channel, job, build, "BUILDING", externalSystem, application, environment); record.setBuildStartNotice(true); + record.setBuildEndNotice(false); jenkinsBuildNotificationRepository.save(record); + log.info("发送构建开始通知: job={}, build={}", job.getJobName(), build.getBuildNumber()); return; } - // 4. 已有记录,检查结束通知 - if (!record.getBuildEndNotice()) { - // 使用实时获取的状态判断是否已完成 + // 2. 已有记录,检查是否需要发送结束通知 + if (!existingRecord.getBuildEndNotice()) { if (isStatusFinished(latestStatus)) { - // 更新数据库中的构建状态 - if (latestBuildInfo != null) { - updateBuildFromResponse(build, latestBuildInfo); - jenkinsBuildRepository.save(build); - } - - // 发送结束通知 - if ("SUCCESS".equals(latestStatus) || "FAILURE".equals(latestStatus) || "ABORTED".equals(latestStatus)) { - sendNotification(config, channel, job, build, latestStatus, externalSystem, application, environment); - } - - record.setBuildEndNotice(true); - jenkinsBuildNotificationRepository.save(record); + // 构建已完成,发送结束通知 + sendNotification(config, channel, job, build, latestStatus, externalSystem, application, environment); + existingRecord.setBuildEndNotice(true); + jenkinsBuildNotificationRepository.save(existingRecord); + log.info("发送构建结束通知: job={}, build={}, status={}", + job.getJobName(), build.getBuildNumber(), latestStatus); } else { - // 兜底逻辑:构建开始超过2小时仍未完成,强制标记为已完成(不发通知) - long hoursAgo = java.time.temporal.ChronoUnit.HOURS.between( - build.getStarttime(), - LocalDateTime.now() - ); + // 兜底:超过2小时仍未完成,强制标记结束(不发通知) + long hoursAgo = java.time.temporal.ChronoUnit.HOURS.between(build.getStarttime(), LocalDateTime.now()); if (hoursAgo >= 2) { - log.warn("构建超时未完成,强制标记结束通知: buildId={}, teamId={}, envId={}, startTime={}", - build.getId(), config.getTeamId(), config.getEnvironmentId(), build.getStarttime()); - record.setBuildEndNotice(true); - jenkinsBuildNotificationRepository.save(record); + 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()); + 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); } }