增加构建通知
This commit is contained in:
parent
8b15f8ae71
commit
a974b6fea4
@ -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> {
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -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 部署系统ID(Jenkins系统)
|
||||||
|
* @return 团队应用列表
|
||||||
|
*/
|
||||||
|
List<TeamApplication> findByDeploySystemId(Long deploySystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量填充扩展字段:environmentName、notificationChannelName、applicationCount
|
* 批量填充扩展字段:environmentName、notificationConfig、applicationCount
|
||||||
*
|
*
|
||||||
* <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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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构建通知记录表';
|
||||||
|
|
||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
-- 通知渠道表
|
-- 通知渠道表
|
||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user