增加构建通知

This commit is contained in:
dengqichen 2025-11-11 18:07:29 +08:00
parent 8b15f8ae71
commit a974b6fea4
12 changed files with 951 additions and 74 deletions

View File

@ -0,0 +1,17 @@
package com.qqchen.deploy.backend.deploy.converter;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentNotificationConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentNotificationConfig;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import org.mapstruct.Mapper;
/**
* 团队环境通知配置Converter
*
* @author qqchen
* @since 2025-11-11
*/
@Mapper(componentModel = "spring")
public interface TeamEnvironmentNotificationConfigConverter
extends BaseConverter<TeamEnvironmentNotificationConfig, TeamEnvironmentNotificationConfigDTO> {
}

View File

@ -36,16 +36,6 @@ public class TeamEnvironmentConfigDTO extends BaseDTO {
*/ */
private List<Long> approverUserIds; private List<Long> approverUserIds;
/**
* 通知渠道ID
*/
private Long notificationChannelId;
/**
* 是否启用部署通知
*/
private Boolean notificationEnabled;
/** /**
* 是否要求代码审查通过 * 是否要求代码审查通过
*/ */
@ -63,14 +53,14 @@ public class TeamEnvironmentConfigDTO extends BaseDTO {
*/ */
private String environmentName; private String environmentName;
/**
* 通知渠道名称扩展字段
*/
private String notificationChannelName;
/** /**
* 该团队在该环境下关联的应用数量扩展字段 * 该团队在该环境下关联的应用数量扩展字段
*/ */
private Long applicationCount; private Long applicationCount;
/**
* 通知配置来自 deploy_team_environment_notification_config
*/
private TeamEnvironmentNotificationConfigDTO notificationConfig;
} }

View File

@ -0,0 +1,53 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 团队环境通知配置DTO
*
* @author qqchen
* @since 2025-11-11
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamEnvironmentNotificationConfigDTO extends BaseDTO {
/**
* 团队ID
*/
private Long teamId;
/**
* 环境ID
*/
private Long environmentId;
/**
* 通知渠道ID
*/
private Long notificationChannelId;
/**
* 是否启用部署通知
*/
private Boolean deployNotificationEnabled;
/**
* 是否启用构建通知
*/
private Boolean buildNotificationEnabled;
/**
* 构建失败时是否发送日志文件到企业微信
*/
private Boolean buildFailureFileEnabled;
// ===== 扩展字段非数据库字段 =====
/**
* 通知渠道名称扩展字段非数据库字段
*/
private String notificationChannelName;
}

View File

@ -0,0 +1,53 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Jenkins构建通知记录实体
*
* <p>记录构建通知的发送状态防止重复通知
* <p>参考 longi-deployment 的设计使用双标识防重机制
*
* @author qqchen
* @since 2025-11-11
*/
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "deploy_jenkins_build_notification")
public class JenkinsBuildNotification extends Entity<Long> {
/**
* 构建记录ID关联deploy_jenkins_build
*/
@Column(name = "build_id", nullable = false)
private Long buildId;
/**
* 团队ID
*/
@Column(name = "team_id", nullable = false)
private Long teamId;
/**
* 环境ID
*/
@Column(name = "environment_id", nullable = false)
private Long environmentId;
/**
* 构建开始是否已通知0未通知1已通知
*/
@Column(name = "build_start_notice", nullable = false)
private Boolean buildStartNotice = false;
/**
* 构建结束是否已通知0未通知1已通知
*/
@Column(name = "build_end_notice", nullable = false)
private Boolean buildEndNotice = false;
}

View File

@ -0,0 +1,65 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 团队环境通知配置实体
*
* <p>管理团队在特定环境下的通知配置
* <ul>
* <li>通知渠道配置</li>
* <li>部署通知开关</li>
* <li>构建通知开关</li>
* </ul>
*
* @author qqchen
* @since 2025-11-11
*/
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "deploy_team_environment_notification_config")
@LogicDelete(false)
public class TeamEnvironmentNotificationConfig extends Entity<Long> {
/**
* 团队ID
*/
@Column(name = "team_id", nullable = false)
private Long teamId;
/**
* 环境ID
*/
@Column(name = "environment_id", nullable = false)
private Long environmentId;
/**
* 通知渠道ID关联sys_notification_channel
*/
@Column(name = "notification_channel_id")
private Long notificationChannelId;
/**
* 是否启用部署通知
*/
@Column(name = "deploy_notification_enabled", nullable = false, columnDefinition = "BIT DEFAULT 1")
private Boolean deployNotificationEnabled = true;
/**
* 是否启用构建通知
*/
@Column(name = "build_notification_enabled", nullable = false)
private Boolean buildNotificationEnabled = false;
/**
* 构建失败时是否发送日志文件到企业微信
*/
@Column(name = "build_failure_file_enabled", nullable = false)
private Boolean buildFailureFileEnabled = false;
}

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.JenkinsBuildNotification;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* Jenkins构建通知记录Repository
*
* @author qqchen
* @since 2025-11-11
*/
@Repository
public interface IJenkinsBuildNotificationRepository
extends IBaseRepository<JenkinsBuildNotification, Long> {
/**
* 根据构建ID团队ID和环境ID查询通知记录
*
* @param buildId 构建记录ID
* @param teamId 团队ID
* @param environmentId 环境ID
* @return 通知记录如果存在
*/
Optional<JenkinsBuildNotification> findByBuildIdAndTeamIdAndEnvironmentId(
Long buildId, Long teamId, Long environmentId);
}

View File

@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import java.util.List; import java.util.List;
@ -64,4 +65,14 @@ public interface IJenkinsBuildRepository extends IBaseRepository<JenkinsBuild, L
"WHERE b.jobId IN :jobIds AND b.deleted = false " + "WHERE b.jobId IN :jobIds AND b.deleted = false " +
"GROUP BY b.jobId") "GROUP BY b.jobId")
List<Object[]> countByJobIds(@Param("jobIds") Collection<Long> jobIds); List<Object[]> countByJobIds(@Param("jobIds") Collection<Long> jobIds);
/**
* 查询指定时间之后创建的构建记录用于构建通知
*
* @param externalSystemId 外部系统ID
* @param createTime 创建时间
* @return 构建记录列表
*/
List<JenkinsBuild> findByExternalSystemIdAndCreateTimeAfter(
Long externalSystemId, LocalDateTime createTime);
} }

View File

@ -108,5 +108,13 @@ public interface ITeamApplicationRepository extends IBaseRepository<TeamApplicat
*/ */
@Query("SELECT COUNT(DISTINCT ta.applicationId) FROM TeamApplication ta WHERE ta.teamId IN :teamIds AND ta.environmentId = :environmentId") @Query("SELECT COUNT(DISTINCT ta.applicationId) FROM TeamApplication ta WHERE ta.teamId IN :teamIds AND ta.environmentId = :environmentId")
Long countDistinctApplicationIdByTeamIdsAndEnvironmentId(@Param("teamIds") Collection<Long> teamIds, @Param("environmentId") Long environmentId); Long countDistinctApplicationIdByTeamIdsAndEnvironmentId(@Param("teamIds") Collection<Long> teamIds, @Param("environmentId") Long environmentId);
/**
* 根据部署系统ID查询所有团队应用用于构建通知
*
* @param deploySystemId 部署系统IDJenkins系统
* @return 团队应用列表
*/
List<TeamApplication> findByDeploySystemId(Long deploySystemId);
} }

View File

@ -0,0 +1,48 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentNotificationConfig;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Set;
/**
* 团队环境通知配置Repository
*
* @author qqchen
* @since 2025-11-11
*/
@Repository
public interface ITeamEnvironmentNotificationConfigRepository
extends IBaseRepository<TeamEnvironmentNotificationConfig, Long> {
/**
* 根据团队ID和环境ID批量查询启用了构建通知的配置
*
* @param teamIds 团队ID集合
* @param environmentIds 环境ID集合
* @return 通知配置列表
*/
List<TeamEnvironmentNotificationConfig> findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(
Set<Long> teamIds, Set<Long> environmentIds);
/**
* 根据团队ID和环境ID批量查询通知配置
*
* @param teamIds 团队ID集合
* @param environmentIds 环境ID集合
* @return 通知配置列表
*/
List<TeamEnvironmentNotificationConfig> findByTeamIdInAndEnvironmentIdIn(
Set<Long> teamIds, Set<Long> environmentIds);
/**
* 根据团队ID和环境ID查询通知配置
*
* @param teamId 团队ID
* @param environmentId 环境ID
* @return 通知配置如果存在
*/
java.util.Optional<TeamEnvironmentNotificationConfig> findByTeamIdAndEnvironmentId(Long teamId, Long environmentId);
}

View File

@ -1,8 +1,10 @@
package com.qqchen.deploy.backend.deploy.service.impl; package com.qqchen.deploy.backend.deploy.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.deploy.dto.JenkinsSyncHistoryDTO; import com.qqchen.deploy.backend.deploy.dto.JenkinsSyncHistoryDTO;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.deploy.entity.*; import com.qqchen.deploy.backend.deploy.entity.*;
import com.qqchen.deploy.backend.deploy.dto.JenkinsBuildDTO; import com.qqchen.deploy.backend.deploy.dto.JenkinsBuildDTO;
import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus;
@ -13,6 +15,10 @@ 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.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.service.INotificationSendService;
import com.qqchen.deploy.backend.notification.dto.NotificationRequest;
import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext; import com.qqchen.deploy.backend.deploy.dto.sync.JenkinsSyncContext;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
@ -58,6 +64,21 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
@Resource @Resource
private IJenkinsSyncHistoryService jenkinsSyncHistoryService; private IJenkinsSyncHistoryService jenkinsSyncHistoryService;
@Resource
private ITeamApplicationRepository teamApplicationRepository;
@Resource
private ITeamEnvironmentNotificationConfigRepository teamEnvironmentNotificationConfigRepository;
@Resource
private IJenkinsBuildNotificationRepository jenkinsBuildNotificationRepository;
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Resource
private INotificationSendService notificationSendService;
@Resource(name = "jenkinsTaskExecutor") @Resource(name = "jenkinsTaskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor; private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@ -66,6 +87,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
@Transactional @Transactional
public void syncBuilds(Long externalSystemId) { public void syncBuilds(Long externalSystemId) {
doSyncBuilds(externalSystemId, null, null); doSyncBuilds(externalSystemId, null, null);
// 同步完成后检查并发送构建通知
checkBuildNotifications(externalSystemId);
} }
private JenkinsSyncContext createSyncContext(Long externalSystemId) { private JenkinsSyncContext createSyncContext(Long externalSystemId) {
@ -375,6 +399,460 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl<JenkinsBuild, Jenki
} }
} }
/**
* 检查并发送构建通知照搬longi-deployment逻辑
*/
private void checkBuildNotifications(Long externalSystemId) {
log.info("开始检查构建通知: externalSystemId={}", externalSystemId);
try {
// 0. 获取外部系统信息用于后续获取日志
ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId)
.orElse(null);
if (externalSystem == null) {
log.warn("外部系统不存在: externalSystemId={}", externalSystemId);
return;
}
// 1. 查询最近同步的构建6分钟内比同步间隔5分钟多1分钟
LocalDateTime since = LocalDateTime.now().minusMinutes(6);
List<JenkinsBuild> recentBuilds = jenkinsBuildRepository
.findByExternalSystemIdAndCreateTimeAfter(externalSystemId, since);
if (recentBuilds.isEmpty()) {
log.info("没有新的构建记录");
return;
}
// 2. 反查团队绑定关系
List<TeamApplication> teamApps = teamApplicationRepository
.findByDeploySystemId(externalSystemId);
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 =
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 =
notificationChannelRepository.findAllById(channelIds).stream()
.collect(Collectors.toMap(NotificationChannel::getId, c -> c));
// 7. 批量查询 Job
Set<Long> jobIds = recentBuilds.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 = recentBuilds.stream()
.collect(Collectors.groupingBy(
build -> jobMap.get(build.getJobId()).getJobName()
));
// 9. 处理每个 Job 的构建通知
buildsByJobName.forEach((jobName, builds) -> {
List<TeamApplication> relatedTeamApps = teamAppsByJob.get(jobName);
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());
// 处理该团队环境的所有构建通知
for (JenkinsBuild build : builds) {
processBuildNotification(config, channel, job, build, externalSystem);
}
}
});
} catch (Exception e) {
log.error("检查构建通知失败: externalSystemId={}", externalSystemId, e);
}
}
/**
* 处理单个构建的通知照搬longi逻辑
*/
private void processBuildNotification(
TeamEnvironmentNotificationConfig config,
NotificationChannel channel,
JenkinsJob job,
JenkinsBuild build,
ExternalSystem externalSystem) {
try {
// 1. 查询通知记录
JenkinsBuildNotification record = jenkinsBuildNotificationRepository
.findByBuildIdAndTeamIdAndEnvironmentId(
build.getId(),
config.getTeamId(),
config.getEnvironmentId()
)
.orElse(null);
// 2. 新构建
if (record == null) {
long minutesAgo = java.time.temporal.ChronoUnit.MINUTES.between(
build.getStarttime(),
LocalDateTime.now()
);
record = new JenkinsBuildNotification();
record.setBuildId(build.getId());
record.setTeamId(config.getTeamId());
record.setEnvironmentId(config.getEnvironmentId());
if (minutesAgo > 20) {
// 超时直接标记完成不发通知
record.setBuildStartNotice(true);
record.setBuildEndNotice(true);
jenkinsBuildNotificationRepository.save(record);
return;
} else {
// 未超时标记开始可选发送"构建中"通知
// sendNotification(channel, job, build, "BUILDING");
record.setBuildStartNotice(true);
jenkinsBuildNotificationRepository.save(record);
return;
}
}
// 3. 已有记录检查结束通知
if (!record.getBuildEndNotice() && isBuildFinished(build)) {
String status = build.getBuildStatus();
// 只通知成功和失败
if ("SUCCESS".equals(status) || "FAILURE".equals(status)) {
sendNotification(config, channel, job, build, status, externalSystem);
}
record.setBuildEndNotice(true);
jenkinsBuildNotificationRepository.save(record);
}
} catch (Exception 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);
}
/**
* 发送通知
*/
private void sendNotification(
TeamEnvironmentNotificationConfig config,
NotificationChannel channel,
JenkinsJob job,
JenkinsBuild build,
String status,
ExternalSystem externalSystem) {
try {
// 构建通知内容
StringBuilder content = new StringBuilder();
content.append("### 构建通知\n");
content.append(String.format("> 任务: %s\n", job.getJobName()));
content.append(String.format("> 构建号: #%d\n", build.getBuildNumber()));
// 状态显示
String statusDisplay;
switch (status) {
case "SUCCESS":
statusDisplay = "✅ 成功";
break;
case "FAILURE":
statusDisplay = "❌ 失败";
break;
case "BUILDING":
statusDisplay = "🔄 构建中";
break;
default:
statusDisplay = status;
}
content.append(String.format("> 状态: %s\n", statusDisplay));
content.append(String.format("> 时间: %s\n", build.getStarttime()));
// 耗时仅结束状态
if (!"BUILDING".equals(status) && build.getDuration() != null) {
long seconds = build.getDuration() / 1000;
long minutes = seconds / 60;
long secs = seconds % 60;
content.append(String.format("> 耗时: %d分%d秒\n", minutes, secs));
}
// 构建失败时发送日志文件如果开启
if ("FAILURE".equals(status) && config.getBuildFailureFileEnabled() != null && config.getBuildFailureFileEnabled()) {
// 先发送文本通知
NotificationRequest textRequest = NotificationRequest.builder()
.channelId(channel.getId())
.title("Jenkins构建通知")
.content(content.toString())
.build();
notificationSendService.send(textRequest);
// 然后发送日志文件
sendBuildFailureLogFile(externalSystem, channel, job.getJobName(), build.getBuildNumber());
return; // 已发送直接返回
}
// 构建通知请求
NotificationRequest request = NotificationRequest.builder()
.channelId(channel.getId())
.title("Jenkins构建通知")
.content(content.toString())
.build();
// 发送通知
notificationSendService.send(request);
log.info("已发送构建通知: job={}, build={}, status={}",
job.getJobName(), build.getBuildNumber(), status);
} catch (Exception e) {
log.error("发送通知失败: job={}, build={}", job.getJobName(), build.getBuildNumber(), e);
}
}
/**
* 发送构建失败日志文件到企业微信
*/
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);
}
}
/**
* 下载构建日志文件
*/
private String downloadBuildLogFile(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
try {
// 获取完整的控制台输出
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);
// 写入日志内容
java.nio.file.Files.write(filePath, consoleOutput.getLines());
log.info("日志文件已下载: {}", filePath.toString());
return filePath.toString();
} catch (Exception e) {
log.error("下载构建日志文件失败: job={}, buildNumber={}",
jobName, buildNumber, e);
return null;
}
}
/**
* 上传文件到企业微信
*/
private String uploadFileToWechat(NotificationChannel channel, String filePath) {
try {
// config 中获取 webhook URL
String webhookUrl = (String) channel.getConfig().get("webhookUrl");
if (webhookUrl == null || !webhookUrl.contains("key=")) {
log.error("无效的企业微信 webhook URL: {}", webhookUrl);
return null;
}
String key = webhookUrl.substring(webhookUrl.indexOf("key=") + 4);
// 企业微信文件上传接口
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<>();
map.add("media", fileResource);
org.springframework.http.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);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 解析响应获取 media_id
JsonNode jsonNode = JsonUtils.parseJson(response.getBody());
if (jsonNode == null) {
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: {}",
errcode, jsonNode.get("errmsg").asText());
return null;
}
}
return null;
} catch (Exception e) {
log.error("上传文件到企业微信失败: filePath={}", filePath, e);
return null;
}
}
/**
* 发送企业微信文件消息
*/
private void sendWechatFileMessage(NotificationChannel channel, String mediaId) {
try {
String webhookUrl = (String) channel.getConfig().get("webhookUrl");
// 构建文件消息
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);
if (response.getStatusCode().is2xxSuccessful()) {
log.info("企业微信文件消息发送成功");
} else {
log.error("企业微信文件消息发送失败: {}", response.getBody());
}
} catch (Exception e) {
log.error("发送企业微信文件消息失败: mediaId={}", mediaId, e);
}
}
/**
* 清理临时文件
*/
private void cleanupTempFile(String filePath) {
try {
java.nio.file.Files.deleteIfExists(java.nio.file.Paths.get(filePath));
log.debug("临时文件已清理: {}", filePath);
} catch (Exception e) {
log.warn("清理临时文件失败: {}", filePath, e);
}
}
@Override @Override
public Long countByExternalSystemId(Long externalSystemId) { public Long countByExternalSystemId(Long externalSystemId) {
QJenkinsBuild qJenkinsBuild = QJenkinsBuild.jenkinsBuild; QJenkinsBuild qJenkinsBuild = QJenkinsBuild.jenkinsBuild;

View File

@ -1,12 +1,16 @@
package com.qqchen.deploy.backend.deploy.service.impl; package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentConfigDTO; import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentConfigDTO;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentNotificationConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.Environment; import com.qqchen.deploy.backend.deploy.entity.Environment;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig; import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentNotificationConfig;
import com.qqchen.deploy.backend.deploy.query.TeamEnvironmentConfigQuery; import com.qqchen.deploy.backend.deploy.query.TeamEnvironmentConfigQuery;
import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository; import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository; import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamEnvironmentConfigRepository; import com.qqchen.deploy.backend.deploy.repository.ITeamEnvironmentConfigRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamEnvironmentNotificationConfigRepository;
import com.qqchen.deploy.backend.deploy.converter.TeamEnvironmentNotificationConfigConverter;
import com.qqchen.deploy.backend.deploy.service.ITeamEnvironmentConfigService; import com.qqchen.deploy.backend.deploy.service.ITeamEnvironmentConfigService;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
@ -47,6 +51,12 @@ public class TeamEnvironmentConfigServiceImpl
@Resource @Resource
private ITeamApplicationRepository teamApplicationRepository; private ITeamApplicationRepository teamApplicationRepository;
@Resource
private ITeamEnvironmentNotificationConfigRepository teamEnvironmentNotificationConfigRepository;
@Resource
private TeamEnvironmentNotificationConfigConverter notificationConfigConverter;
@Override @Override
public List<TeamEnvironmentConfigDTO> findByTeamId(Long teamId) { public List<TeamEnvironmentConfigDTO> findByTeamId(Long teamId) {
List<TeamEnvironmentConfigDTO> list = teamEnvironmentConfigRepository.findByTeamId(teamId).stream() List<TeamEnvironmentConfigDTO> list = teamEnvironmentConfigRepository.findByTeamId(teamId).stream()
@ -94,7 +104,14 @@ public class TeamEnvironmentConfigServiceImpl
public TeamEnvironmentConfigDTO create(TeamEnvironmentConfigDTO dto) { public TeamEnvironmentConfigDTO create(TeamEnvironmentConfigDTO dto) {
// 执行兜底逻辑确保数据一致性 // 执行兜底逻辑确保数据一致性
applyDataConsistencyRules(dto); applyDataConsistencyRules(dto);
return super.create(dto);
// 创建主配置
TeamEnvironmentConfigDTO result = super.create(dto);
// 保存或更新通知配置
saveOrUpdateNotificationConfig(dto);
return result;
} }
/** /**
@ -105,7 +122,14 @@ public class TeamEnvironmentConfigServiceImpl
public TeamEnvironmentConfigDTO update(Long id, TeamEnvironmentConfigDTO dto) { public TeamEnvironmentConfigDTO update(Long id, TeamEnvironmentConfigDTO dto) {
// 执行兜底逻辑确保数据一致性 // 执行兜底逻辑确保数据一致性
applyDataConsistencyRules(dto); applyDataConsistencyRules(dto);
return super.update(id, dto);
// 更新主配置
TeamEnvironmentConfigDTO result = super.update(id, dto);
// 保存或更新通知配置
saveOrUpdateNotificationConfig(dto);
return result;
} }
/** /**
@ -113,9 +137,9 @@ public class TeamEnvironmentConfigServiceImpl
* <p>规则 * <p>规则
* <ul> * <ul>
* <li>1. 如果不需要审批approvalRequired = false清空审批人列表</li> * <li>1. 如果不需要审批approvalRequired = false清空审批人列表</li>
* <li>2. 如果不需要通知notificationEnabled = false清空通知渠道</li>
* <li>3. 如果不需要代码审查requireCodeReview = false清空代码审查相关配置</li>
* </ul> * </ul>
*
* <p>注意通知相关配置已迁移到 deploy_team_environment_notification_config 不在此处理
*/ */
private void applyDataConsistencyRules(TeamEnvironmentConfigDTO dto) { private void applyDataConsistencyRules(TeamEnvironmentConfigDTO dto) {
if (dto == null) { if (dto == null) {
@ -130,26 +154,51 @@ public class TeamEnvironmentConfigServiceImpl
dto.setApproverUserIds(null); dto.setApproverUserIds(null);
} }
} }
// 规则2不需要通知时清空通知渠道
if (dto.getNotificationEnabled() != null && !dto.getNotificationEnabled()) {
if (dto.getNotificationChannelId() != null) {
log.info("兜底逻辑触发:团队 {} 环境 {} 不需要通知,清空通知渠道 {}",
dto.getTeamId(), dto.getEnvironmentId(), dto.getNotificationChannelId());
dto.setNotificationChannelId(null);
}
} }
// 规则3不需要代码审查时清空相关配置如果将来有相关字段 /**
if (dto.getRequireCodeReview() != null && !dto.getRequireCodeReview()) { * 保存或更新通知配置
// 目前没有代码审查相关的其他配置字段 * <p>如果 DTO 中包含 notificationConfig
// 如果将来有 codeReviewerUserIds在这里清空 * <ul>
log.debug("团队 {} 环境 {} 不需要代码审查", dto.getTeamId(), dto.getEnvironmentId()); * <li>如果数据库中已存在对应记录则更新</li>
* <li>如果不存在则创建新记录</li>
* </ul>
*
* @param dto 团队环境配置DTO包含 notificationConfig
*/
private void saveOrUpdateNotificationConfig(TeamEnvironmentConfigDTO dto) {
if (dto == null || dto.getNotificationConfig() == null) {
return;
}
TeamEnvironmentNotificationConfigDTO notificationDTO = dto.getNotificationConfig();
// 确保 teamId environmentId 一致
notificationDTO.setTeamId(dto.getTeamId());
notificationDTO.setEnvironmentId(dto.getEnvironmentId());
// 查询是否已存在记录
Optional<TeamEnvironmentNotificationConfig> existingConfig =
teamEnvironmentNotificationConfigRepository.findByTeamIdAndEnvironmentId(
dto.getTeamId(), dto.getEnvironmentId()
);
if (existingConfig.isPresent()) {
// 更新现有记录
TeamEnvironmentNotificationConfig config = existingConfig.get();
notificationConfigConverter.updateEntity(config, notificationDTO);
teamEnvironmentNotificationConfigRepository.save(config);
log.info("更新通知配置: teamId={}, environmentId={}", dto.getTeamId(), dto.getEnvironmentId());
} else {
// 创建新记录
TeamEnvironmentNotificationConfig newConfig = notificationConfigConverter.toEntity(notificationDTO);
teamEnvironmentNotificationConfigRepository.save(newConfig);
log.info("创建通知配置: teamId={}, environmentId={}", dto.getTeamId(), dto.getEnvironmentId());
} }
} }
/** /**
* 批量填充扩展字段environmentNamenotificationChannelNameapplicationCount * 批量填充扩展字段environmentNamenotificationConfigapplicationCount
* *
* <p>使用批量查询避免N+1问题 * <p>使用批量查询避免N+1问题
*/ */
@ -158,14 +207,14 @@ public class TeamEnvironmentConfigServiceImpl
return; return;
} }
// 1. 收集所有环境ID和通知渠道ID // 1. 收集所有团队ID和环境ID
Set<Long> environmentIds = configs.stream() Set<Long> teamIds = configs.stream()
.map(TeamEnvironmentConfigDTO::getEnvironmentId) .map(TeamEnvironmentConfigDTO::getTeamId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Set<Long> channelIds = configs.stream() Set<Long> environmentIds = configs.stream()
.map(TeamEnvironmentConfigDTO::getNotificationChannelId) .map(TeamEnvironmentConfigDTO::getEnvironmentId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -177,7 +226,24 @@ public class TeamEnvironmentConfigServiceImpl
); );
} }
// 3. 批量查询通知渠道信息 // 3. 批量查询通知配置来自 deploy_team_environment_notification_config
Map<String, TeamEnvironmentNotificationConfig> notificationConfigMap = new HashMap<>();
if (!teamIds.isEmpty() && !environmentIds.isEmpty()) {
List<TeamEnvironmentNotificationConfig> notificationConfigs =
teamEnvironmentNotificationConfigRepository.findByTeamIdInAndEnvironmentIdIn(teamIds, environmentIds);
notificationConfigs.forEach(nc -> {
String key = nc.getTeamId() + "_" + nc.getEnvironmentId();
notificationConfigMap.put(key, nc);
});
}
// 4. 批量查询通知渠道信息
Set<Long> channelIds = notificationConfigMap.values().stream()
.map(TeamEnvironmentNotificationConfig::getNotificationChannelId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, NotificationChannel> channelMap = new HashMap<>(); Map<Long, NotificationChannel> channelMap = new HashMap<>();
if (!channelIds.isEmpty()) { if (!channelIds.isEmpty()) {
notificationChannelRepository.findAllById(channelIds).forEach(channel -> notificationChannelRepository.findAllById(channelIds).forEach(channel ->
@ -185,7 +251,7 @@ public class TeamEnvironmentConfigServiceImpl
); );
} }
// 4. 填充扩展字段 // 5. 填充扩展字段
configs.forEach(config -> { configs.forEach(config -> {
// 填充环境名称 // 填充环境名称
if (config.getEnvironmentId() != null) { if (config.getEnvironmentId() != null) {
@ -195,11 +261,24 @@ public class TeamEnvironmentConfigServiceImpl
} }
} }
// 填充通知配置
if (config.getTeamId() != null && config.getEnvironmentId() != null) {
String key = config.getTeamId() + "_" + config.getEnvironmentId();
TeamEnvironmentNotificationConfig notificationConfig = notificationConfigMap.get(key);
if (notificationConfig != null) {
TeamEnvironmentNotificationConfigDTO notificationConfigDTO =
notificationConfigConverter.toDto(notificationConfig);
// 填充通知渠道名称 // 填充通知渠道名称
if (config.getNotificationChannelId() != null) { if (notificationConfig.getNotificationChannelId() != null) {
NotificationChannel channel = channelMap.get(config.getNotificationChannelId()); NotificationChannel channel = channelMap.get(notificationConfig.getNotificationChannelId());
if (channel != null) { if (channel != null) {
config.setNotificationChannelName(channel.getName()); notificationConfigDTO.setNotificationChannelName(channel.getName());
}
}
config.setNotificationConfig(notificationConfigDTO);
} }
} }

View File

@ -844,10 +844,6 @@ CREATE TABLE deploy_team_environment_config
approval_required BIT NOT NULL DEFAULT 0 COMMENT '是否需要审批', approval_required BIT NOT NULL DEFAULT 0 COMMENT '是否需要审批',
approver_user_ids JSON NULL COMMENT '审批人用户ID列表[1, 4, 7]', approver_user_ids JSON NULL COMMENT '审批人用户ID列表[1, 4, 7]',
-- 通知配置
notification_channel_id BIGINT NULL COMMENT '通知渠道ID',
notification_enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用部署通知',
-- 安全策略 -- 安全策略
require_code_review BIT NOT NULL DEFAULT 0 COMMENT '是否要求代码审查通过', require_code_review BIT NOT NULL DEFAULT 0 COMMENT '是否要求代码审查通过',
@ -862,6 +858,56 @@ CREATE TABLE deploy_team_environment_config
CONSTRAINT fk_team_env_config_env FOREIGN KEY (environment_id) REFERENCES deploy_environment (id) CONSTRAINT fk_team_env_config_env FOREIGN KEY (environment_id) REFERENCES deploy_environment (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队环境配置表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队环境配置表';
-- 团队环境通知配置表
CREATE TABLE deploy_team_environment_notification_config
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
team_id BIGINT NOT NULL COMMENT '团队ID',
environment_id BIGINT NOT NULL COMMENT '环境ID',
notification_channel_id BIGINT NULL COMMENT '通知渠道ID关联sys_notification_channel',
deploy_notification_enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用部署通知',
build_notification_enabled BIT NOT NULL DEFAULT 0 COMMENT '是否启用构建通知',
build_failure_log_enabled BIT NOT NULL DEFAULT 0 COMMENT '构建失败时是否附加错误日志到通知0不附加1附加',
UNIQUE INDEX uk_team_env (team_id, environment_id),
INDEX idx_team (team_id),
INDEX idx_env (environment_id),
INDEX idx_channel (notification_channel_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队环境通知配置表';
-- Jenkins构建通知记录表记录通知状态防止重复通知
CREATE TABLE deploy_jenkins_build_notification
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
build_id BIGINT NOT NULL COMMENT '构建记录ID关联deploy_jenkins_build',
team_id BIGINT NOT NULL COMMENT '团队ID',
environment_id BIGINT NOT NULL COMMENT '环境ID',
build_start_notice BIT NOT NULL DEFAULT 0 COMMENT '构建开始是否已通知0未通知1已通知',
build_end_notice BIT NOT NULL DEFAULT 0 COMMENT '构建结束是否已通知0未通知1已通知',
UNIQUE INDEX uk_build_team_env (build_id, team_id, environment_id),
INDEX idx_build (build_id),
INDEX idx_team_env (team_id, environment_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Jenkins构建通知记录表';
-- -------------------------------------------------------------------------------------- -- --------------------------------------------------------------------------------------
-- 通知渠道表 -- 通知渠道表
-- -------------------------------------------------------------------------------------- -- --------------------------------------------------------------------------------------