增加构建通知

This commit is contained in:
dengqichen 2025-11-28 09:36:54 +08:00
parent 730bb94926
commit e7211f8699
2 changed files with 176 additions and 210 deletions

View File

@ -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);
}
}
/**
* 清理临时文件
*/

View File

@ -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),
-- 表单数据详情(隐藏路由,用于查看表单数据)