增加构建通知

This commit is contained in:
dengqichen 2025-11-28 13:53:02 +08:00
parent 44ea1353b2
commit 33f648d79f
2 changed files with 165 additions and 140 deletions

View File

@ -4,6 +4,7 @@ import com.qqchen.deploy.backend.deploy.entity.JenkinsBuildNotification;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -34,4 +35,12 @@ public interface IJenkinsBuildNotificationRepository
* @return 未完成结束通知的记录列表 * @return 未完成结束通知的记录列表
*/ */
List<JenkinsBuildNotification> findByBuildEndNoticeFalseAndDeletedFalse(); List<JenkinsBuildNotification> findByBuildEndNoticeFalseAndDeletedFalse();
/**
* 根据构建ID批量查询通知记录
*
* @param buildIds 构建ID集合
* @return 通知记录列表
*/
List<JenkinsBuildNotification> findByBuildIdInAndDeletedFalse(Collection<Long> buildIds);
} }

View File

@ -436,81 +436,56 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} }
/** /**
* 检查并发送构建通知照搬longi-deployment逻辑 * 检查并发送构建通知参考longi-deployment逻辑重构
*
* <p>核心流程
* <ol>
* <li>获取所有绑定了该Jenkins系统的TeamApplication</li>
* <li> deployJob 分组遍历每个 Job</li>
* <li>获取该 Job 的最新构建记录</li>
* <li>查通知记录表判断是否需要发送通知</li>
* </ol>
*/ */
private void checkBuildNotifications(Long externalSystemId) { private void checkBuildNotifications(Long externalSystemId) {
log.info("开始检查构建通知: externalSystemId={}", externalSystemId); log.info("开始检查构建通知: externalSystemId={}", externalSystemId);
try { try {
// 0. 获取外部系统信息用于后续获取日志 // 1. 获取外部系统信息
ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId) ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId).orElse(null);
.orElse(null);
if (externalSystem == null) { if (externalSystem == null) {
log.warn("外部系统不存在: externalSystemId={}", externalSystemId); log.warn("外部系统不存在: externalSystemId={}", externalSystemId);
return; return;
} }
// 1. 查询最近6分钟内开始的新构建用于发送开始通知 // 2. 查询绑定该Jenkins系统的TeamApplication构建类型为JENKINS
LocalDateTime recentSince = LocalDateTime.now().minusMinutes(6); List<TeamApplication> teamApps = teamApplicationRepository
List<JenkinsBuild> recentBuilds = jenkinsBuildRepository .findByDeploySystemIdAndBuildType(externalSystemId, BuildTypeEnum.JENKINS);
.findByExternalSystemIdAndStarttimeAfter(externalSystemId, recentSince);
// 2. 查询未完成通知的构建ID用于发送结束通知和兜底处理
List<JenkinsBuildNotification> pendingNotifications = jenkinsBuildNotificationRepository
.findByBuildEndNoticeFalseAndDeletedFalse();
Set<Long> pendingBuildIds = pendingNotifications.stream()
.map(JenkinsBuildNotification::getBuildId)
.collect(Collectors.toSet());
// 3. 查询未完成通知对应的构建记录
List<JenkinsBuild> pendingBuilds = pendingBuildIds.isEmpty()
? Collections.emptyList()
: jenkinsBuildRepository.findAllById(pendingBuildIds).stream()
.filter(b -> b.getExternalSystemId().equals(externalSystemId))
.toList();
// 4. 合并需要处理的构建去重
Map<Long, JenkinsBuild> buildMap = new HashMap<>();
recentBuilds.forEach(b -> buildMap.put(b.getId(), b));
pendingBuilds.forEach(b -> buildMap.put(b.getId(), b));
List<JenkinsBuild> buildsToProcess = new ArrayList<>(buildMap.values());
if (buildsToProcess.isEmpty()) {
log.info("没有需要处理的构建记录");
return;
}
// 2. 反查团队绑定关系只查询构建类型为JENKINS的应用
List<TeamApplication> teamApps = teamApplicationRepository.findByDeploySystemIdAndBuildType(externalSystemId, BuildTypeEnum.JENKINS);
if (teamApps.isEmpty()) { if (teamApps.isEmpty()) {
log.info("没有团队绑定该Jenkins系统: externalSystemId={}", externalSystemId); log.info("没有团队绑定该Jenkins系统: externalSystemId={}", externalSystemId);
return; return;
} }
// 3. deploy_job 分组 TeamApplication // 3. deployJob 分组过滤掉空的deployJob
Map<String, List<TeamApplication>> teamAppsByJob = teamApps.stream() Map<String, List<TeamApplication>> teamAppsByJob = teamApps.stream()
.filter(ta -> ta.getDeployJob() != null && !ta.getDeployJob().isEmpty())
.collect(Collectors.groupingBy(TeamApplication::getDeployJob)); .collect(Collectors.groupingBy(TeamApplication::getDeployJob));
if (teamAppsByJob.isEmpty()) {
log.info("没有配置deployJob的团队应用");
return;
}
// 4. 查询启用了构建通知的团队环境配置 // 4. 查询启用了构建通知的团队环境配置
Set<Long> teamIds = teamApps.stream() Set<Long> teamIds = teamApps.stream().map(TeamApplication::getTeamId).collect(Collectors.toSet());
.map(TeamApplication::getTeamId) Set<Long> envIds = teamApps.stream().map(TeamApplication::getEnvironmentId).collect(Collectors.toSet());
.collect(Collectors.toSet());
Set<Long> envIds = teamApps.stream() List<TeamEnvironmentNotificationConfig> configs = teamEnvironmentNotificationConfigRepository
.map(TeamApplication::getEnvironmentId)
.collect(Collectors.toSet());
List<TeamEnvironmentNotificationConfig> configs =
teamEnvironmentNotificationConfigRepository
.findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(teamIds, envIds); .findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(teamIds, envIds);
if (configs.isEmpty()) { if (configs.isEmpty()) {
log.info("没有团队启用构建通知"); log.info("没有团队启用构建通知");
return; return;
} }
// 5. team_id + environment_id 分组配置 // 5. 构建配置Mapkey: teamId_envId
Map<String, TeamEnvironmentNotificationConfig> configMap = configs.stream() Map<String, TeamEnvironmentNotificationConfig> configMap = configs.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
cfg -> cfg.getTeamId() + "_" + cfg.getEnvironmentId(), cfg -> cfg.getTeamId() + "_" + cfg.getEnvironmentId(),
@ -522,63 +497,101 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
.map(TeamEnvironmentNotificationConfig::getNotificationChannelId) .map(TeamEnvironmentNotificationConfig::getNotificationChannelId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Map<Long, NotificationChannel> channelMap = notificationChannelRepository.findAllById(channelIds)
.stream().collect(Collectors.toMap(NotificationChannel::getId, c -> c));
Map<Long, NotificationChannel> channelMap = // 7. 批量查询应用和环境信息
notificationChannelRepository.findAllById(channelIds).stream() Set<Long> applicationIds = teamApps.stream().map(TeamApplication::getApplicationId).collect(Collectors.toSet());
.collect(Collectors.toMap(NotificationChannel::getId, c -> c)); Map<Long, Application> applicationMap = applicationRepository.findAllById(applicationIds)
.stream().collect(Collectors.toMap(Application::getId, a -> a));
Map<Long, Environment> environmentMap = environmentRepository.findAllById(envIds)
.stream().collect(Collectors.toMap(Environment::getId, e -> e));
// 7. 批量查询 Job // 8. 批量查询所有视图下的Job构建 jobName -> JenkinsJob 的Map
Set<Long> jobIds = buildsToProcess.stream() List<JenkinsView> views = jenkinsViewRepository.findByExternalSystemId(externalSystemId);
.map(JenkinsBuild::getJobId) Map<String, JenkinsJob> jobByNameMap = new HashMap<>();
.collect(Collectors.toSet()); for (JenkinsView view : views) {
List<JenkinsJob> jobs = jenkinsJobRepository.findByExternalSystemIdAndViewId(externalSystemId, view.getId());
for (JenkinsJob job : jobs) {
jobByNameMap.put(job.getJobName(), job);
}
}
Map<Long, JenkinsJob> jobMap = jenkinsJobRepository // 9. 收集需要处理的Job及其最新构建
.findAllById(jobIds).stream() Map<Long, JenkinsBuild> latestBuildByJobId = new HashMap<>();
.collect(Collectors.toMap(JenkinsJob::getId, j -> j)); Set<String> 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));
}
// 8. job_name 分组构建记录 if (latestBuildByJobId.isEmpty()) {
Map<String, List<JenkinsBuild>> buildsByJobName = buildsToProcess.stream() log.info("没有需要处理的构建记录");
.collect(Collectors.groupingBy(
build -> jobMap.get(build.getJobId()).getJobName()
));
// 9. 批量查询应用信息
Set<Long> applicationIds = teamApps.stream()
.map(TeamApplication::getApplicationId)
.collect(Collectors.toSet());
Map<Long, Application> applicationMap = applicationRepository.findAllById(applicationIds).stream()
.collect(Collectors.toMap(Application::getId, a -> a));
// 10. 批量查询环境信息
Map<Long, Environment> environmentMap = environmentRepository.findAllById(envIds).stream()
.collect(Collectors.toMap(Environment::getId, e -> e));
// 11. 处理每个 Job 的构建通知
buildsByJobName.forEach((jobName, builds) -> {
List<TeamApplication> relatedTeamApps = teamAppsByJob.get(jobName);
if (relatedTeamApps == null || relatedTeamApps.isEmpty()) {
return; return;
} }
for (TeamApplication teamApp : relatedTeamApps) { // 10. 批量查询通知记录避免N+1
String key = teamApp.getTeamId() + "_" + teamApp.getEnvironmentId(); Set<Long> buildIds = latestBuildByJobId.values().stream()
TeamEnvironmentNotificationConfig config = configMap.get(key); .map(JenkinsBuild::getId)
.collect(Collectors.toSet());
List<JenkinsBuildNotification> existingNotifications = jenkinsBuildNotificationRepository
.findByBuildIdInAndDeletedFalse(buildIds);
// 构建 buildId_teamId_envId -> notification 的Map
Map<String, JenkinsBuildNotification> notificationMap = existingNotifications.stream()
.collect(Collectors.toMap(
n -> n.getBuildId() + "_" + n.getTeamId() + "_" + n.getEnvironmentId(),
n -> n
));
// 11. 遍历每个 Job处理通知
for (Map.Entry<String, List<TeamApplication>> entry : teamAppsByJob.entrySet()) {
String jobName = entry.getKey();
List<TeamApplication> relatedTeamApps = entry.getValue();
JenkinsJob job = jobByNameMap.get(jobName);
if (job == null) continue;
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 configKey = teamApp.getTeamId() + "_" + teamApp.getEnvironmentId();
TeamEnvironmentNotificationConfig config = configMap.get(configKey);
if (config == null) continue; if (config == null) continue;
NotificationChannel channel = channelMap.get(config.getNotificationChannelId()); NotificationChannel channel = channelMap.get(config.getNotificationChannelId());
if (channel == null) continue; if (channel == null) continue;
JenkinsJob job = jobMap.get(builds.get(0).getJobId());
Application application = applicationMap.get(teamApp.getApplicationId()); Application application = applicationMap.get(teamApp.getApplicationId());
Environment environment = environmentMap.get(teamApp.getEnvironmentId()); Environment environment = environmentMap.get(teamApp.getEnvironmentId());
// 处理该团队环境的所有构建通知 // 从Map中获取通知记录避免循环内查询
for (JenkinsBuild build : builds) { String notificationKey = latestBuild.getId() + "_" + config.getTeamId() + "_" + config.getEnvironmentId();
processBuildNotification(config, channel, job, build, externalSystem, application, environment); JenkinsBuildNotification existingRecord = notificationMap.get(notificationKey);
// 处理该构建的通知
processBuildNotification(config, channel, job, latestBuild, latestStatus,
externalSystem, application, environment, existingRecord);
} }
} }
});
} catch (Exception e) { } catch (Exception e) {
log.error("检查构建通知失败: externalSystemId={}", externalSystemId, e); log.error("检查构建通知失败: externalSystemId={}", externalSystemId, e);
@ -586,82 +599,85 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} }
/** /**
* 处理单个构建的通知 * 处理单个构建的通知参考longi-deployment逻辑
* <p>参考 longi-deployment 的实现每次都从 Jenkins API 实时获取最新状态 *
* <p>核心逻辑
* <ul>
* <li>无记录 = 新构建超时(>20分钟)直接标记完成未超时发送"构建中"通知</li>
* <li>有记录但buildEndNotice=false检查是否完成发送结束通知</li>
* </ul>
*/ */
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 { try {
// 1. 查询通知记录 // 计算构建开始到现在的分钟数
JenkinsBuildNotification record = jenkinsBuildNotificationRepository.findByBuildIdAndTeamIdAndEnvironmentId(build.getId(), config.getTeamId(), config.getEnvironmentId()) long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between(build.getStarttime(), LocalDateTime.now());
.orElse(null);
// 2. Jenkins API 实时获取该构建的最新状态 // 1. 新构建通知记录不存在
JenkinsBuildResponse latestBuildInfo = fetchLatestBuildInfo(externalSystem, job, build.getBuildNumber()); if (existingRecord == null) {
String latestStatus = (latestBuildInfo != null && latestBuildInfo.getResult() != null) // 超过20分钟的旧构建直接标记完成不发通知
? latestBuildInfo.getResult() if (minutesAgo > 20) {
: "BUILDING"; JenkinsBuildNotification record = new JenkinsBuildNotification();
record.setBuildId(build.getId());
// 3. 新构建只处理6分钟内的新构建 record.setTeamId(config.getTeamId());
if (record == null) { record.setEnvironmentId(config.getEnvironmentId());
long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between( record.setBuildStartNotice(true);
build.getStarttime(), record.setBuildEndNotice(true); // 直接标记结束
LocalDateTime.now() jenkinsBuildNotificationRepository.save(record);
); log.info("构建超时,直接标记完成: job={}, build={}, minutesAgo={}",
job.getJobName(), build.getBuildNumber(), minutesAgo);
// 超过6分钟的旧构建不创建通知记录直接跳过
if (minutesAgo > 6) {
return; return;
} }
// 6分钟内的新构建发送"构建中"通知 // 未超时发送"构建中"通知
record = new JenkinsBuildNotification(); JenkinsBuildNotification record = new JenkinsBuildNotification();
record.setBuildId(build.getId()); record.setBuildId(build.getId());
record.setTeamId(config.getTeamId()); record.setTeamId(config.getTeamId());
record.setEnvironmentId(config.getEnvironmentId()); record.setEnvironmentId(config.getEnvironmentId());
sendNotification(config, channel, job, build, "BUILDING", externalSystem, application, environment); sendNotification(config, channel, job, build, "BUILDING", externalSystem, application, environment);
record.setBuildStartNotice(true); record.setBuildStartNotice(true);
record.setBuildEndNotice(false);
jenkinsBuildNotificationRepository.save(record); jenkinsBuildNotificationRepository.save(record);
log.info("发送构建开始通知: job={}, build={}", job.getJobName(), build.getBuildNumber());
return; return;
} }
// 4. 已有记录检查结束通知 // 2. 已有记录检查是否需要发送结束通知
if (!record.getBuildEndNotice()) { if (!existingRecord.getBuildEndNotice()) {
// 使用实时获取的状态判断是否已完成
if (isStatusFinished(latestStatus)) { 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); sendNotification(config, channel, job, build, latestStatus, externalSystem, application, environment);
} existingRecord.setBuildEndNotice(true);
jenkinsBuildNotificationRepository.save(existingRecord);
record.setBuildEndNotice(true); log.info("发送构建结束通知: job={}, build={}, status={}",
jenkinsBuildNotificationRepository.save(record); job.getJobName(), build.getBuildNumber(), latestStatus);
} else { } else {
// 兜底逻辑构建开始超过2小时仍未完成强制标记为已完成不发通知 // 兜底超过2小时仍未完成强制标记结束不发通知
long hoursAgo = java.time.temporal.ChronoUnit.HOURS.between( long hoursAgo = java.time.temporal.ChronoUnit.HOURS.between(build.getStarttime(), LocalDateTime.now());
build.getStarttime(),
LocalDateTime.now()
);
if (hoursAgo >= 2) { if (hoursAgo >= 2) {
log.warn("构建超时未完成,强制标记结束通知: buildId={}, teamId={}, envId={}, startTime={}", log.warn("构建超时未完成,强制标记结束: job={}, build={}, startTime={}",
build.getId(), config.getTeamId(), config.getEnvironmentId(), build.getStarttime()); job.getJobName(), build.getBuildNumber(), build.getStarttime());
record.setBuildEndNotice(true); existingRecord.setBuildEndNotice(true);
jenkinsBuildNotificationRepository.save(record); jenkinsBuildNotificationRepository.save(existingRecord);
} }
} }
} }
} catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) { } catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) {
// 乐观锁冲突说明记录已被其他线程更新跳过即可 log.warn("构建通知记录乐观锁冲突,跳过: teamId={}, envId={}, buildId={}",
log.warn("构建通知记录乐观锁冲突,跳过处理: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId()); config.getTeamId(), config.getEnvironmentId(), build.getId());
} catch (Exception e) { } 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);
} }
} }