增加构建通知
This commit is contained in:
parent
730bb94926
commit
e7211f8699
@ -16,6 +16,7 @@ import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
|
||||
import com.qqchen.deploy.backend.deploy.repository.*;
|
||||
import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService;
|
||||
import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService;
|
||||
import com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest;
|
||||
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
|
||||
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
|
||||
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig;
|
||||
@ -33,14 +34,26 @@ import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -48,8 +61,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, JenkinsBuildDTO, JenkinsBuildQuery, Long>
|
||||
implements IJenkinsBuildService {
|
||||
public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, JenkinsBuildDTO, JenkinsBuildQuery, Long> implements IJenkinsBuildService {
|
||||
|
||||
@Resource
|
||||
private IExternalSystemRepository externalSystemRepository;
|
||||
@ -96,8 +108,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
@Resource
|
||||
private INotificationService notificationService;
|
||||
|
||||
@Resource
|
||||
private INotificationSendService notificationSendService;
|
||||
|
||||
@Resource(name = "jenkinsTaskExecutor")
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
@ -107,7 +117,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
@Transactional
|
||||
public void syncBuilds(Long externalSystemId) {
|
||||
doSyncBuilds(externalSystemId, null, null);
|
||||
|
||||
|
||||
// 同步完成后检查并发送构建通知
|
||||
checkBuildNotifications(externalSystemId);
|
||||
}
|
||||
@ -143,7 +153,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
() -> syncView(context.getExternalSystem(), view),
|
||||
threadPoolTaskExecutor
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
|
||||
// 3. 等待所有任务完成并汇总结果
|
||||
int totalSyncedBuilds = futures.stream()
|
||||
@ -160,7 +170,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
|
||||
// 4. 更新同步历史
|
||||
updateSyncHistorySuccess(context.getSyncHistory());
|
||||
|
||||
|
||||
log.info("Successfully synchronized total {} builds for external system: {}",
|
||||
totalSyncedBuilds, context.getExternalSystem().getId());
|
||||
return totalSyncedBuilds;
|
||||
@ -172,7 +182,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
// 1. 获取视图下的所有任务
|
||||
List<JenkinsJob> jobs = jenkinsJobRepository.findByExternalSystemIdAndViewId(
|
||||
externalSystem.getId(), view.getId());
|
||||
|
||||
|
||||
if (jobs.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
@ -223,7 +233,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
|
||||
// 3. 保存新的构建信息
|
||||
saveNewBuilds(externalSystem, job, newBuilds);
|
||||
|
||||
|
||||
// 4. 更新任务的最新构建信息
|
||||
updateJobLastBuild(job, jobResponse);
|
||||
|
||||
@ -236,11 +246,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
}
|
||||
|
||||
private List<JenkinsBuildResponse> getNewBuilds(
|
||||
ExternalSystem externalSystem,
|
||||
JenkinsJob job,
|
||||
JenkinsJobResponse jobResponse,
|
||||
Optional<JenkinsBuild> lastBuild) {
|
||||
|
||||
ExternalSystem externalSystem,
|
||||
JenkinsJob job,
|
||||
JenkinsJobResponse jobResponse,
|
||||
Optional<JenkinsBuild> lastBuild) {
|
||||
|
||||
// 1. 获取最新构建号(从jobResponse中获取,避免额外的API调用)
|
||||
if (jobResponse.getLastBuild() == null) {
|
||||
log.info("No builds found for job: {}", job.getJobName());
|
||||
@ -268,7 +278,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
|
||||
// 4. 获取构建信息
|
||||
List<JenkinsBuildResponse> builds = jenkinsServiceIntegration.listBuilds(externalSystem, job.getJobName());
|
||||
|
||||
|
||||
// 5. 过滤出需要的构建
|
||||
List<JenkinsBuildResponse> newBuilds = builds.stream()
|
||||
.filter(build -> build.getNumber() >= fromBuildNumber && build.getNumber() <= latestBuildNumber)
|
||||
@ -280,9 +290,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
}
|
||||
|
||||
private void saveNewBuilds(
|
||||
ExternalSystem externalSystem,
|
||||
JenkinsJob job,
|
||||
List<JenkinsBuildResponse> builds) {
|
||||
ExternalSystem externalSystem,
|
||||
JenkinsJob job,
|
||||
List<JenkinsBuildResponse> builds) {
|
||||
List<JenkinsBuild> jenkinsBuilds = builds.stream()
|
||||
.map(buildResponse -> {
|
||||
JenkinsBuild build = new JenkinsBuild();
|
||||
@ -310,9 +320,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
}
|
||||
|
||||
private void handleSyncException(JenkinsSyncContext context, Exception e) {
|
||||
log.error("Failed to sync Jenkins builds for external system: {}",
|
||||
log.error("Failed to sync Jenkins builds for external system: {}",
|
||||
context.getExternalSystem().getId(), e);
|
||||
|
||||
|
||||
context.getSyncHistory().setStatus(ExternalSystemSyncStatus.FAILED);
|
||||
context.getSyncHistory().setErrorMessage(e.getMessage());
|
||||
jenkinsSyncHistoryService.saveOrUpdateHistory(context.getSyncHistory());
|
||||
@ -355,7 +365,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
* 同步指定视图下的构建信息(异步执行)
|
||||
*
|
||||
* @param externalSystemId 外部系统ID
|
||||
* @param viewId 视图ID
|
||||
* @param viewId 视图ID
|
||||
*/
|
||||
@Override
|
||||
@Async
|
||||
@ -368,8 +378,8 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
* 同步指定任务的构建信息(异步执行)
|
||||
*
|
||||
* @param externalSystemId 外部系统ID
|
||||
* @param viewId 视图ID (未使用,但为了保持API一致性)
|
||||
* @param jobId 任务ID
|
||||
* @param viewId 视图ID (未使用,但为了保持API一致性)
|
||||
* @param jobId 任务ID
|
||||
*/
|
||||
@Override
|
||||
@Async
|
||||
@ -382,8 +392,8 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
* 执行构建同步的核心方法
|
||||
*
|
||||
* @param externalSystemId 外部系统ID
|
||||
* @param viewId 视图ID(可选)
|
||||
* @param jobId 任务ID(可选)
|
||||
* @param viewId 视图ID(可选)
|
||||
* @param jobId 任务ID(可选)
|
||||
*/
|
||||
private void doSyncBuilds(Long externalSystemId, Long viewId, Long jobId) {
|
||||
// 1. 创建同步上下文
|
||||
@ -430,7 +440,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
*/
|
||||
private void checkBuildNotifications(Long externalSystemId) {
|
||||
log.info("开始检查构建通知: externalSystemId={}", externalSystemId);
|
||||
|
||||
|
||||
try {
|
||||
// 0. 获取外部系统信息(用于后续获取日志)
|
||||
ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId)
|
||||
@ -439,99 +449,99 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
log.warn("外部系统不存在: externalSystemId={}", externalSystemId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 1. 查询最近6分钟内开始的新构建(用于发送开始通知)
|
||||
LocalDateTime recentSince = LocalDateTime.now().minusMinutes(6);
|
||||
List<JenkinsBuild> recentBuilds = jenkinsBuildRepository
|
||||
.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()
|
||||
List<JenkinsBuild> pendingBuilds = pendingBuildIds.isEmpty()
|
||||
? Collections.emptyList()
|
||||
: jenkinsBuildRepository.findAllById(pendingBuildIds).stream()
|
||||
.filter(b -> b.getExternalSystemId().equals(externalSystemId))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
.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()) {
|
||||
log.info("没有团队绑定该Jenkins系统: externalSystemId={}", externalSystemId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 3. 按 deploy_job 分组 TeamApplication
|
||||
Map<String, List<TeamApplication>> teamAppsByJob = teamApps.stream()
|
||||
.collect(Collectors.groupingBy(TeamApplication::getDeployJob));
|
||||
|
||||
|
||||
// 4. 查询启用了构建通知的团队环境配置
|
||||
Set<Long> teamIds = teamApps.stream()
|
||||
.map(TeamApplication::getTeamId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
|
||||
Set<Long> envIds = teamApps.stream()
|
||||
.map(TeamApplication::getEnvironmentId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
List<TeamEnvironmentNotificationConfig> configs =
|
||||
|
||||
List<TeamEnvironmentNotificationConfig> configs =
|
||||
teamEnvironmentNotificationConfigRepository
|
||||
.findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(teamIds, envIds);
|
||||
|
||||
|
||||
if (configs.isEmpty()) {
|
||||
log.info("没有团队启用构建通知");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 5. 按 team_id + environment_id 分组配置
|
||||
Map<String, TeamEnvironmentNotificationConfig> configMap = configs.stream()
|
||||
.collect(Collectors.toMap(
|
||||
cfg -> cfg.getTeamId() + "_" + cfg.getEnvironmentId(),
|
||||
cfg -> cfg
|
||||
));
|
||||
|
||||
|
||||
// 6. 批量查询通知渠道
|
||||
Set<Long> channelIds = configs.stream()
|
||||
.map(TeamEnvironmentNotificationConfig::getNotificationChannelId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, NotificationChannel> channelMap =
|
||||
|
||||
Map<Long, NotificationChannel> channelMap =
|
||||
notificationChannelRepository.findAllById(channelIds).stream()
|
||||
.collect(Collectors.toMap(NotificationChannel::getId, c -> c));
|
||||
|
||||
|
||||
// 7. 批量查询 Job
|
||||
Set<Long> jobIds = buildsToProcess.stream()
|
||||
.map(JenkinsBuild::getJobId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
|
||||
Map<Long, JenkinsJob> jobMap = jenkinsJobRepository
|
||||
.findAllById(jobIds).stream()
|
||||
.collect(Collectors.toMap(JenkinsJob::getId, j -> j));
|
||||
|
||||
|
||||
// 8. 按 job_name 分组构建记录
|
||||
Map<String, List<JenkinsBuild>> buildsByJobName = buildsToProcess.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
build -> jobMap.get(build.getJobId()).getJobName()
|
||||
));
|
||||
|
||||
|
||||
// 9. 批量查询应用信息
|
||||
Set<Long> applicationIds = teamApps.stream()
|
||||
.map(TeamApplication::getApplicationId)
|
||||
@ -549,66 +559,54 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
if (relatedTeamApps == null || relatedTeamApps.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (TeamApplication teamApp : relatedTeamApps) {
|
||||
String key = teamApp.getTeamId() + "_" + teamApp.getEnvironmentId();
|
||||
TeamEnvironmentNotificationConfig config = configMap.get(key);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("检查构建通知失败: externalSystemId={}", externalSystemId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理单个构建的通知
|
||||
*/
|
||||
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, ExternalSystem externalSystem, Application application, Environment environment) {
|
||||
|
||||
try {
|
||||
// 1. 查询通知记录
|
||||
JenkinsBuildNotification record = jenkinsBuildNotificationRepository
|
||||
.findByBuildIdAndTeamIdAndEnvironmentId(
|
||||
build.getId(),
|
||||
config.getTeamId(),
|
||||
config.getEnvironmentId()
|
||||
)
|
||||
JenkinsBuildNotification record = jenkinsBuildNotificationRepository.findByBuildIdAndTeamIdAndEnvironmentId(build.getId(), config.getTeamId(), config.getEnvironmentId())
|
||||
.orElse(null);
|
||||
|
||||
|
||||
// 2. 新构建(只处理6分钟内的新构建)
|
||||
if (record == null) {
|
||||
long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between(
|
||||
build.getStarttime(),
|
||||
build.getStarttime(),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
|
||||
|
||||
// 超过6分钟的旧构建,不创建通知记录,直接跳过
|
||||
if (minutesAgo > 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 6分钟内的新构建,发送"构建中"通知
|
||||
record = new JenkinsBuildNotification();
|
||||
record.setBuildId(build.getId());
|
||||
@ -619,120 +617,106 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
jenkinsBuildNotificationRepository.save(record);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 3. 已有记录,检查结束通知
|
||||
if (!record.getBuildEndNotice()) {
|
||||
if (isBuildFinished(build)) {
|
||||
// 构建已完成,发送结束通知
|
||||
String status = build.getBuildStatus();
|
||||
|
||||
|
||||
// 通知成功、失败、取消
|
||||
if ("SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status)) {
|
||||
sendNotification(config, channel, job, build, status, externalSystem, application, environment);
|
||||
}
|
||||
|
||||
|
||||
record.setBuildEndNotice(true);
|
||||
jenkinsBuildNotificationRepository.save(record);
|
||||
} else {
|
||||
// 兜底逻辑:构建开始超过2小时仍未完成,强制标记为已完成(不发通知)
|
||||
long hoursAgo = java.time.temporal.ChronoUnit.HOURS.between(
|
||||
build.getStarttime(),
|
||||
build.getStarttime(),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
if (hoursAgo >= 2) {
|
||||
log.warn("构建超时未完成,强制标记结束通知: buildId={}, teamId={}, envId={}, startTime={}",
|
||||
log.warn("构建超时未完成,强制标记结束通知: buildId={}, teamId={}, envId={}, startTime={}",
|
||||
build.getId(), config.getTeamId(), config.getEnvironmentId(), build.getStarttime());
|
||||
record.setBuildEndNotice(true);
|
||||
jenkinsBuildNotificationRepository.save(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断构建是否已结束
|
||||
*/
|
||||
private boolean isBuildFinished(JenkinsBuild build) {
|
||||
String status = build.getBuildStatus();
|
||||
return "SUCCESS".equals(status) ||
|
||||
"FAILURE".equals(status) ||
|
||||
"ABORTED".equals(status);
|
||||
return "SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送通知(使用模板)
|
||||
*/
|
||||
private void sendNotification(
|
||||
TeamEnvironmentNotificationConfig config,
|
||||
NotificationChannel channel,
|
||||
JenkinsJob job,
|
||||
JenkinsBuild build,
|
||||
String status,
|
||||
ExternalSystem externalSystem,
|
||||
Application application,
|
||||
Environment environment) {
|
||||
|
||||
private void sendNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, String status, ExternalSystem externalSystem, Application application, Environment environment) {
|
||||
|
||||
try {
|
||||
// 1. 检查是否配置了构建通知模板
|
||||
if (config.getBuildNotificationTemplateId() == null) {
|
||||
log.warn("未配置构建通知模板,跳过通知: teamId={}, envId={}",
|
||||
log.warn("未配置构建通知模板,跳过通知: teamId={}, envId={}",
|
||||
config.getTeamId(), config.getEnvironmentId());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 2. 查询模板
|
||||
NotificationTemplate template = notificationTemplateRepository
|
||||
.findById(config.getBuildNotificationTemplateId())
|
||||
.orElse(null);
|
||||
NotificationTemplate template = notificationTemplateRepository.findById(config.getBuildNotificationTemplateId()).orElse(null);
|
||||
if (template == null) {
|
||||
log.warn("构建通知模板不存在: templateId={}", config.getBuildNotificationTemplateId());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 3. 构建模板参数
|
||||
Map<String, Object> templateParams = new HashMap<>();
|
||||
|
||||
|
||||
// 应用信息
|
||||
if (application != null) {
|
||||
templateParams.put("applicationId", application.getId());
|
||||
templateParams.put("applicationCode", application.getAppCode());
|
||||
templateParams.put("applicationName", application.getAppName());
|
||||
}
|
||||
|
||||
|
||||
// 环境信息
|
||||
if (environment != null) {
|
||||
templateParams.put("environmentId", environment.getId());
|
||||
templateParams.put("environmentCode", environment.getEnvCode());
|
||||
templateParams.put("environmentName", environment.getEnvName());
|
||||
}
|
||||
|
||||
|
||||
// Jenkins 构建信息
|
||||
templateParams.put("jobName", job.getJobName());
|
||||
templateParams.put("buildNumber", build.getBuildNumber());
|
||||
templateParams.put("buildUrl", build.getBuildUrl());
|
||||
templateParams.put("buildStatus", status);
|
||||
|
||||
|
||||
// 时间格式化
|
||||
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
if (build.getStarttime() != null) {
|
||||
templateParams.put("buildStartTime", build.getStarttime().format(formatter));
|
||||
}
|
||||
|
||||
|
||||
// 构建结束时间(如果有)
|
||||
if (build.getStarttime() != null && build.getDuration() != null) {
|
||||
LocalDateTime endTime = build.getStarttime().plusNanos(build.getDuration() * 1_000_000);
|
||||
templateParams.put("buildEndTime", endTime.format(formatter));
|
||||
}
|
||||
|
||||
|
||||
// 耗时格式化
|
||||
if (build.getDuration() != null) {
|
||||
long totalSeconds = build.getDuration() / 1000;
|
||||
@ -741,49 +725,44 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
templateParams.put("buildDurationFormatted", String.format("%d分%d秒", minutes, seconds));
|
||||
templateParams.put("buildDurationMs", build.getDuration());
|
||||
}
|
||||
|
||||
|
||||
// 从 actions JSON 中解析 commit 信息
|
||||
parseCommitInfoFromActions(build.getActions(), templateParams);
|
||||
|
||||
|
||||
// 4. 校验模板和渠道类型是否匹配
|
||||
if (!template.getChannelType().equals(channel.getChannelType())) {
|
||||
log.warn("模板渠道类型({})与通知渠道类型({})不匹配,跳过通知: templateId={}, channelId={}",
|
||||
template.getChannelType(), channel.getChannelType(),
|
||||
config.getBuildNotificationTemplateId(), channel.getId());
|
||||
log.warn("模板渠道类型({})与通知渠道类型({})不匹配,跳过通知: templateId={}, channelId={}", template.getChannelType(), channel.getChannelType(), config.getBuildNotificationTemplateId(), channel.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 5. 构建 SendNotificationRequest
|
||||
SendNotificationRequest request = new SendNotificationRequest();
|
||||
request.setNotificationTemplateId(config.getBuildNotificationTemplateId());
|
||||
request.setTemplateParams(templateParams);
|
||||
request.setSendRequest(createSendRequestByChannel(channel, template));
|
||||
|
||||
log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}, channelType={}",
|
||||
job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(),
|
||||
channel.getId(), channel.getChannelType());
|
||||
|
||||
|
||||
log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}, channelType={}", job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(), channel.getId(), channel.getChannelType());
|
||||
|
||||
// 6. 发送通知
|
||||
notificationService.send(request);
|
||||
|
||||
log.info("已发送构建通知: job={}, build={}, status={}, templateId={}",
|
||||
job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
|
||||
|
||||
|
||||
log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
|
||||
|
||||
// 6. 构建失败时,发送日志文件(如果开启)
|
||||
if ("FAILURE".equals(status) && Boolean.TRUE.equals(config.getBuildFailureFileEnabled())) {
|
||||
sendBuildFailureLogFile(externalSystem, channel, job.getJobName(), build.getBuildNumber());
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送通知失败: job={}, build={}", job.getJobName(), build.getBuildNumber(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据渠道类型创建对应的发送请求
|
||||
*/
|
||||
private com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest createSendRequestByChannel(
|
||||
NotificationChannel channel, NotificationTemplate template) {
|
||||
private BaseSendNotificationRequest createSendRequestByChannel(
|
||||
NotificationChannel channel, NotificationTemplate template) {
|
||||
switch (channel.getChannelType()) {
|
||||
case WEWORK:
|
||||
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
|
||||
@ -799,15 +778,14 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
throw new RuntimeException("不支持的渠道类型: " + channel.getChannelType());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从模板配置中获取企业微信消息类型
|
||||
*/
|
||||
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
|
||||
try {
|
||||
if (template.getTemplateConfig() != null) {
|
||||
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(
|
||||
template.getTemplateConfig(), WeworkTemplateConfig.class);
|
||||
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
|
||||
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
|
||||
return weworkConfig.getMessageType();
|
||||
}
|
||||
@ -827,12 +805,12 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(actionsJson);
|
||||
|
||||
|
||||
// 解析 gitCommitId
|
||||
if (root.has("gitCommitId") && !root.get("gitCommitId").isNull()) {
|
||||
templateParams.put("gitCommitId", root.get("gitCommitId").asText());
|
||||
}
|
||||
|
||||
|
||||
// 解析 changeSets 中的 commit 信息
|
||||
JsonNode changeSets = root.get("changeSets");
|
||||
if (changeSets != null && changeSets.isArray() && changeSets.size() > 0) {
|
||||
@ -844,7 +822,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
String message = item.has("message") ? item.get("message").asText() : "";
|
||||
String author = item.has("author") ? item.get("author").asText() : "";
|
||||
String commitId = item.has("commitId") ? item.get("commitId").asText() : "";
|
||||
|
||||
|
||||
if (!message.isEmpty()) {
|
||||
if (commitMessages.length() > 0) {
|
||||
commitMessages.append("\n");
|
||||
@ -864,78 +842,72 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
log.warn("解析 commit 信息失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送构建失败日志文件到企业微信
|
||||
*/
|
||||
private void sendBuildFailureLogFile(ExternalSystem externalSystem, NotificationChannel channel,
|
||||
private void sendBuildFailureLogFile(ExternalSystem externalSystem, NotificationChannel channel,
|
||||
String jobName, Integer buildNumber) {
|
||||
try {
|
||||
log.info("开始下载构建失败日志: job={}, buildNumber={}", jobName, buildNumber);
|
||||
|
||||
|
||||
// 1. 下载日志文件到临时目录
|
||||
String logFilePath = downloadBuildLogFile(externalSystem, jobName, buildNumber);
|
||||
if (logFilePath == null) {
|
||||
log.warn("日志文件下载失败,跳过文件发送");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 2. 上传文件到企业微信
|
||||
String mediaId = uploadFileToWechat(channel, logFilePath);
|
||||
if (mediaId == null) {
|
||||
log.warn("文件上传到企业微信失败");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 3. 发送文件消息
|
||||
sendWechatFileMessage(channel, mediaId);
|
||||
|
||||
|
||||
// 4. 清理临时文件
|
||||
cleanupTempFile(logFilePath);
|
||||
|
||||
|
||||
log.info("构建失败日志文件发送成功: job={}, buildNumber={}", jobName, buildNumber);
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送构建失败日志文件失败: job={}, buildNumber={}",
|
||||
jobName, buildNumber, e);
|
||||
log.error("发送构建失败日志文件失败: job={}, buildNumber={}", jobName, buildNumber, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下载构建日志文件
|
||||
*/
|
||||
private String downloadBuildLogFile(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
|
||||
try {
|
||||
// 获取完整的控制台输出
|
||||
var consoleOutput = jenkinsServiceIntegration.getConsoleOutput(
|
||||
externalSystem,
|
||||
jobName,
|
||||
buildNumber,
|
||||
0L
|
||||
);
|
||||
|
||||
var consoleOutput = jenkinsServiceIntegration.getConsoleOutput(externalSystem, jobName, buildNumber, 0L);
|
||||
|
||||
if (consoleOutput == null || consoleOutput.getLines() == null || consoleOutput.getLines().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 创建临时文件
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
String fileName = String.format("jenkins-build-%s-%d.log", jobName, buildNumber);
|
||||
java.nio.file.Path filePath = java.nio.file.Paths.get(tempDir, fileName);
|
||||
|
||||
Path filePath = Paths.get(tempDir, fileName);
|
||||
|
||||
// 写入日志内容
|
||||
java.nio.file.Files.write(filePath, consoleOutput.getLines());
|
||||
|
||||
log.info("日志文件已下载: {}", filePath.toString());
|
||||
Files.write(filePath, consoleOutput.getLines());
|
||||
|
||||
log.info("日志文件已下载: {}", filePath);
|
||||
return filePath.toString();
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("下载构建日志文件失败: job={}, buildNumber={}",
|
||||
log.error("下载构建日志文件失败: job={}, buildNumber={}",
|
||||
jobName, buildNumber, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件到企业微信
|
||||
*/
|
||||
@ -951,28 +923,24 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
log.error("企业微信渠道 key 为空: channelId={}", channel.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 企业微信文件上传接口
|
||||
String uploadUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=" + key + "&type=file";
|
||||
|
||||
|
||||
// 构建 multipart/form-data 请求
|
||||
org.springframework.core.io.FileSystemResource fileResource =
|
||||
new org.springframework.core.io.FileSystemResource(filePath);
|
||||
|
||||
org.springframework.util.LinkedMultiValueMap<String, Object> map =
|
||||
new org.springframework.util.LinkedMultiValueMap<>();
|
||||
FileSystemResource fileResource = new FileSystemResource(filePath);
|
||||
|
||||
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
|
||||
map.add("media", fileResource);
|
||||
|
||||
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
||||
|
||||
HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
||||
headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
org.springframework.http.HttpEntity<org.springframework.util.MultiValueMap<String, Object>> requestEntity =
|
||||
new org.springframework.http.HttpEntity<>(map, headers);
|
||||
|
||||
org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
|
||||
org.springframework.http.ResponseEntity<String> response =
|
||||
restTemplate.postForEntity(uploadUrl, requestEntity, String.class);
|
||||
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers);
|
||||
|
||||
RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(uploadUrl, requestEntity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
// 解析响应,获取 media_id
|
||||
JsonNode jsonNode = JsonUtils.parseJson(response.getBody());
|
||||
@ -980,27 +948,27 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
log.error("文件上传响应解析失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
int errcode = jsonNode.get("errcode").asInt();
|
||||
if (errcode == 0) {
|
||||
String mediaId = jsonNode.get("media_id").asText();
|
||||
log.info("文件上传成功,media_id: {}", mediaId);
|
||||
return mediaId;
|
||||
} else {
|
||||
log.error("文件上传失败,errcode: {}, errmsg: {}",
|
||||
log.error("文件上传失败,errcode: {}, errmsg: {}",
|
||||
errcode, jsonNode.get("errmsg").asText());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("上传文件到企业微信失败: filePath={}", filePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送企业微信文件消息
|
||||
*/
|
||||
@ -1008,37 +976,35 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
|
||||
try {
|
||||
String key = (String) channel.getConfig().get("key");
|
||||
String webhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + key;
|
||||
|
||||
|
||||
// 构建文件消息
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("msgtype", "file");
|
||||
|
||||
|
||||
Map<String, String> file = new HashMap<>();
|
||||
file.put("media_id", mediaId);
|
||||
message.put("file", file);
|
||||
|
||||
|
||||
// 发送请求
|
||||
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
||||
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
|
||||
|
||||
org.springframework.http.HttpEntity<Map<String, Object>> requestEntity =
|
||||
new org.springframework.http.HttpEntity<>(message, headers);
|
||||
|
||||
org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
|
||||
org.springframework.http.ResponseEntity<String> response =
|
||||
restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(message, headers);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info("企业微信文件消息发送成功");
|
||||
} else {
|
||||
log.error("企业微信文件消息发送失败: {}", response.getBody());
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送企业微信文件消息失败: mediaId={}", mediaId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*/
|
||||
|
||||
@ -52,15 +52,15 @@ VALUES
|
||||
(99, '工作台', '/dashboard', 'Dashboard', 'DashboardOutlined', 'dashboard', 2, NULL, 0, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
|
||||
-- 工作流管理
|
||||
(100, '工作流管理', '/workflow', NULL, 'DeploymentUnitOutlined', NULL, 1, NULL, 1, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 工作流设计
|
||||
(101, '工作流设计', '/workflow/definitions', 'Workflow/Definition/List', 'EditOutlined', 'workflow:definition', 2, 100, 10, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
(100, '流程管理', '/workflow', NULL, 'DeploymentUnitOutlined', NULL, 1, NULL, 1, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 流程设计
|
||||
(101, '流程设计', '/workflow/definitions', 'Workflow/Definition/List', 'EditOutlined', 'workflow:definition', 2, 100, 10, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 工作流设计器(隐藏路由,用于编辑工作流)
|
||||
(1011, '工作流设计器', '/workflow/design/:id', 'Workflow/Design', 'EditOutlined', NULL, 2, 100, 11, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 工作流实例
|
||||
(102, '工作流实例', '/workflow/instances', 'Workflow/Instance/List', 'BranchesOutlined', 'workflow:instance', 2, 100, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 动态表单菜单
|
||||
(104, '动态表单菜单', '/workflow/form', 'Form/Definition/List', 'FormOutlined', 'workflow:form', 2, 100, 30, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 流程实例
|
||||
(102, '流程实例', '/workflow/instances', 'Workflow/Instance/List', 'BranchesOutlined', 'workflow:instance', 2, 100, 20, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 动态表单
|
||||
(104, '动态表单', '/workflow/form', 'Form/Definition/List', 'FormOutlined', 'workflow:form', 2, 100, 30, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 表单设计器(隐藏路由,用于设计表单)
|
||||
(1041, '表单设计器', '/workflow/form/:id/design', 'Form/Definition/Designer', 'FormOutlined', NULL, 2, 100, 31, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
|
||||
-- 表单数据详情(隐藏路由,用于查看表单数据)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user