增加构建通知

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;
@ -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()
@ -236,10 +246,10 @@ 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) {
@ -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();
@ -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. 创建同步上下文
@ -456,8 +466,8 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
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<>();
@ -578,23 +588,11 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
/**
* 处理单个构建的通知
*/
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分钟内的新构建
@ -650,11 +648,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} 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);
}
}
@ -663,23 +659,13 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
*/
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. 检查是否配置了构建通知模板
@ -690,9 +676,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
}
// 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;
@ -722,7 +706,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
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));
}
@ -747,9 +731,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
// 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;
}
@ -759,15 +741,12 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
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())) {
@ -782,8 +761,8 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
/**
* 根据渠道类型创建对应的发送请求
*/
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();
@ -806,8 +785,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
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();
}
@ -896,8 +874,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
log.info("构建失败日志文件发送成功: job={}, buildNumber={}", jobName, buildNumber);
} catch (Exception e) {
log.error("发送构建失败日志文件失败: job={}, buildNumber={}",
jobName, buildNumber, e);
log.error("发送构建失败日志文件失败: job={}, buildNumber={}", jobName, buildNumber, e);
}
}
@ -907,12 +884,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
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;
@ -921,12 +893,12 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
// 创建临时文件
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());
Files.write(filePath, consoleOutput.getLines());
log.info("日志文件已下载: {}", filePath.toString());
log.info("日志文件已下载: {}", filePath);
return filePath.toString();
} 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";
// 构建 multipart/form-data 请求
org.springframework.core.io.FileSystemResource fileResource =
new org.springframework.core.io.FileSystemResource(filePath);
FileSystemResource fileResource = new FileSystemResource(filePath);
org.springframework.util.LinkedMultiValueMap<String, Object> map =
new org.springframework.util.LinkedMultiValueMap<>();
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);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new 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);
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
@ -1018,15 +986,13 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
message.put("file", file);
// 发送请求
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
org.springframework.http.HttpEntity<Map<String, Object>> requestEntity =
new org.springframework.http.HttpEntity<>(message, headers);
HttpEntity<Map<String, Object>> requestEntity = new 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);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
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),
-- 工作流管理
(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),
-- 表单数据详情(隐藏路由,用于查看表单数据)