增加构建通知

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.repository.*;
import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService; import com.qqchen.deploy.backend.deploy.service.IJenkinsBuildService;
import com.qqchen.deploy.backend.deploy.service.IJenkinsSyncHistoryService; 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.NotificationChannel;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate; import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig; 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 com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; 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.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -48,8 +61,7 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, JenkinsBuildDTO, JenkinsBuildQuery, Long> public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, JenkinsBuildDTO, JenkinsBuildQuery, Long> implements IJenkinsBuildService {
implements IJenkinsBuildService {
@Resource @Resource
private IExternalSystemRepository externalSystemRepository; private IExternalSystemRepository externalSystemRepository;
@ -96,8 +108,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
@Resource @Resource
private INotificationService notificationService; private INotificationService notificationService;
@Resource
private INotificationSendService notificationSendService;
@Resource(name = "jenkinsTaskExecutor") @Resource(name = "jenkinsTaskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor; private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@ -143,7 +153,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
() -> syncView(context.getExternalSystem(), view), () -> syncView(context.getExternalSystem(), view),
threadPoolTaskExecutor threadPoolTaskExecutor
)) ))
.collect(Collectors.toList()); .toList();
// 3. 等待所有任务完成并汇总结果 // 3. 等待所有任务完成并汇总结果
int totalSyncedBuilds = futures.stream() int totalSyncedBuilds = futures.stream()
@ -457,7 +467,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
? Collections.emptyList() ? Collections.emptyList()
: jenkinsBuildRepository.findAllById(pendingBuildIds).stream() : jenkinsBuildRepository.findAllById(pendingBuildIds).stream()
.filter(b -> b.getExternalSystemId().equals(externalSystemId)) .filter(b -> b.getExternalSystemId().equals(externalSystemId))
.collect(Collectors.toList()); .toList();
// 4. 合并需要处理的构建去重 // 4. 合并需要处理的构建去重
Map<Long, JenkinsBuild> buildMap = new HashMap<>(); Map<Long, JenkinsBuild> buildMap = new HashMap<>();
@ -578,23 +588,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
/** /**
* 处理单个构建的通知 * 处理单个构建的通知
*/ */
private void processBuildNotification( private void processBuildNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, ExternalSystem externalSystem, Application application, Environment environment) {
TeamEnvironmentNotificationConfig config,
NotificationChannel channel,
JenkinsJob job,
JenkinsBuild build,
ExternalSystem externalSystem,
Application application,
Environment environment) {
try { try {
// 1. 查询通知记录 // 1. 查询通知记录
JenkinsBuildNotification record = jenkinsBuildNotificationRepository JenkinsBuildNotification record = jenkinsBuildNotificationRepository.findByBuildIdAndTeamIdAndEnvironmentId(build.getId(), config.getTeamId(), config.getEnvironmentId())
.findByBuildIdAndTeamIdAndEnvironmentId(
build.getId(),
config.getTeamId(),
config.getEnvironmentId()
)
.orElse(null); .orElse(null);
// 2. 新构建只处理6分钟内的新构建 // 2. 新构建只处理6分钟内的新构建
@ -650,11 +648,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} 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={}", log.error("处理构建通知失败: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId(), e);
config.getTeamId(), config.getEnvironmentId(), build.getId(), e);
} }
} }
@ -663,23 +659,13 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
*/ */
private boolean isBuildFinished(JenkinsBuild build) { private boolean isBuildFinished(JenkinsBuild build) {
String status = build.getBuildStatus(); String status = build.getBuildStatus();
return "SUCCESS".equals(status) || return "SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status);
"FAILURE".equals(status) ||
"ABORTED".equals(status);
} }
/** /**
* 发送通知使用模板 * 发送通知使用模板
*/ */
private void sendNotification( private void sendNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, String status, ExternalSystem externalSystem, Application application, Environment environment) {
TeamEnvironmentNotificationConfig config,
NotificationChannel channel,
JenkinsJob job,
JenkinsBuild build,
String status,
ExternalSystem externalSystem,
Application application,
Environment environment) {
try { try {
// 1. 检查是否配置了构建通知模板 // 1. 检查是否配置了构建通知模板
@ -690,9 +676,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} }
// 2. 查询模板 // 2. 查询模板
NotificationTemplate template = notificationTemplateRepository NotificationTemplate template = notificationTemplateRepository.findById(config.getBuildNotificationTemplateId()).orElse(null);
.findById(config.getBuildNotificationTemplateId())
.orElse(null);
if (template == null) { if (template == null) {
log.warn("构建通知模板不存在: templateId={}", config.getBuildNotificationTemplateId()); log.warn("构建通知模板不存在: templateId={}", config.getBuildNotificationTemplateId());
return; return;
@ -722,7 +706,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
templateParams.put("buildStatus", status); 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) { if (build.getStarttime() != null) {
templateParams.put("buildStartTime", build.getStarttime().format(formatter)); templateParams.put("buildStartTime", build.getStarttime().format(formatter));
} }
@ -747,9 +731,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
// 4. 校验模板和渠道类型是否匹配 // 4. 校验模板和渠道类型是否匹配
if (!template.getChannelType().equals(channel.getChannelType())) { if (!template.getChannelType().equals(channel.getChannelType())) {
log.warn("模板渠道类型({})与通知渠道类型({})不匹配,跳过通知: templateId={}, channelId={}", log.warn("模板渠道类型({})与通知渠道类型({})不匹配,跳过通知: templateId={}, channelId={}", template.getChannelType(), channel.getChannelType(), config.getBuildNotificationTemplateId(), channel.getId());
template.getChannelType(), channel.getChannelType(),
config.getBuildNotificationTemplateId(), channel.getId());
return; return;
} }
@ -759,15 +741,12 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
request.setTemplateParams(templateParams); request.setTemplateParams(templateParams);
request.setSendRequest(createSendRequestByChannel(channel, template)); request.setSendRequest(createSendRequestByChannel(channel, template));
log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}, channelType={}", log.debug("准备发送构建通知: job={}, build={}, templateId={}, channelId={}, channelType={}", job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(), channel.getId(), channel.getChannelType());
job.getJobName(), build.getBuildNumber(), config.getBuildNotificationTemplateId(),
channel.getId(), channel.getChannelType());
// 6. 发送通知 // 6. 发送通知
notificationService.send(request); notificationService.send(request);
log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", log.info("已发送构建通知: job={}, build={}, status={}, templateId={}", job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
job.getJobName(), build.getBuildNumber(), status, config.getBuildNotificationTemplateId());
// 6. 构建失败时发送日志文件如果开启 // 6. 构建失败时发送日志文件如果开启
if ("FAILURE".equals(status) && Boolean.TRUE.equals(config.getBuildFailureFileEnabled())) { if ("FAILURE".equals(status) && Boolean.TRUE.equals(config.getBuildFailureFileEnabled())) {
@ -782,7 +761,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
/** /**
* 根据渠道类型创建对应的发送请求 * 根据渠道类型创建对应的发送请求
*/ */
private com.qqchen.deploy.backend.notification.dto.BaseSendNotificationRequest createSendRequestByChannel( private BaseSendNotificationRequest createSendRequestByChannel(
NotificationChannel channel, NotificationTemplate template) { NotificationChannel channel, NotificationTemplate template) {
switch (channel.getChannelType()) { switch (channel.getChannelType()) {
case WEWORK: case WEWORK:
@ -806,8 +785,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) { private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
try { try {
if (template.getTemplateConfig() != null) { if (template.getTemplateConfig() != null) {
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap( WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
template.getTemplateConfig(), WeworkTemplateConfig.class);
if (weworkConfig != null && weworkConfig.getMessageType() != null) { if (weworkConfig != null && weworkConfig.getMessageType() != null) {
return weworkConfig.getMessageType(); return weworkConfig.getMessageType();
} }
@ -896,8 +874,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
log.info("构建失败日志文件发送成功: job={}, buildNumber={}", jobName, buildNumber); log.info("构建失败日志文件发送成功: job={}, buildNumber={}", jobName, buildNumber);
} catch (Exception e) { } catch (Exception e) {
log.error("发送构建失败日志文件失败: job={}, buildNumber={}", log.error("发送构建失败日志文件失败: job={}, buildNumber={}", jobName, buildNumber, e);
jobName, buildNumber, e);
} }
} }
@ -907,12 +884,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
private String downloadBuildLogFile(ExternalSystem externalSystem, String jobName, Integer buildNumber) { private String downloadBuildLogFile(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
try { try {
// 获取完整的控制台输出 // 获取完整的控制台输出
var consoleOutput = jenkinsServiceIntegration.getConsoleOutput( var consoleOutput = jenkinsServiceIntegration.getConsoleOutput(externalSystem, jobName, buildNumber, 0L);
externalSystem,
jobName,
buildNumber,
0L
);
if (consoleOutput == null || consoleOutput.getLines() == null || consoleOutput.getLines().isEmpty()) { if (consoleOutput == null || consoleOutput.getLines() == null || consoleOutput.getLines().isEmpty()) {
return null; return null;
@ -921,12 +893,12 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
// 创建临时文件 // 创建临时文件
String tempDir = System.getProperty("java.io.tmpdir"); String tempDir = System.getProperty("java.io.tmpdir");
String fileName = String.format("jenkins-build-%s-%d.log", jobName, buildNumber); 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()); Files.write(filePath, consoleOutput.getLines());
log.info("日志文件已下载: {}", filePath.toString()); log.info("日志文件已下载: {}", filePath);
return filePath.toString(); return filePath.toString();
} catch (Exception e) { } catch (Exception e) {
@ -956,22 +928,18 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
String uploadUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=" + key + "&type=file"; String uploadUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=" + key + "&type=file";
// 构建 multipart/form-data 请求 // 构建 multipart/form-data 请求
org.springframework.core.io.FileSystemResource fileResource = FileSystemResource fileResource = new FileSystemResource(filePath);
new org.springframework.core.io.FileSystemResource(filePath);
org.springframework.util.LinkedMultiValueMap<String, Object> map = LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
new org.springframework.util.LinkedMultiValueMap<>();
map.add("media", fileResource); 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); headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA);
org.springframework.http.HttpEntity<org.springframework.util.MultiValueMap<String, Object>> requestEntity = HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers);
new org.springframework.http.HttpEntity<>(map, headers);
org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate(); RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
org.springframework.http.ResponseEntity<String> response = ResponseEntity<String> response = restTemplate.postForEntity(uploadUrl, requestEntity, String.class);
restTemplate.postForEntity(uploadUrl, requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 解析响应获取 media_id // 解析响应获取 media_id
@ -1018,15 +986,13 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
message.put("file", file); message.put("file", file);
// 发送请求 // 发送请求
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); headers.setContentType(MediaType.APPLICATION_JSON);
org.springframework.http.HttpEntity<Map<String, Object>> requestEntity = HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(message, headers);
new org.springframework.http.HttpEntity<>(message, headers);
org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate(); RestTemplate restTemplate = new RestTemplate();
org.springframework.http.ResponseEntity<String> response = ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful()) { if (response.getStatusCode().is2xxSuccessful()) {
log.info("企业微信文件消息发送成功"); log.info("企业微信文件消息发送成功");

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), (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), (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), (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), (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), (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), (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), (1041, '表单设计器', '/workflow/form/:id/design', 'Form/Definition/Designer', 'FormOutlined', NULL, 2, 100, 31, TRUE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 表单数据详情(隐藏路由,用于查看表单数据) -- 表单数据详情(隐藏路由,用于查看表单数据)