From e7211f8699cf745c3f684a65dbc07e8a20c4ef0a Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 28 Nov 2025 09:36:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9E=84=E5=BB=BA=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/JenkinsBuildServiceImpl.java | 372 ++++++++---------- .../db/changelog/changes/v1.0.0-data.sql | 14 +- 2 files changed, 176 insertions(+), 210 deletions(-) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java index 82790ac5..c4d28f96 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsBuildServiceImpl.java @@ -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 - implements IJenkinsBuildService { +public class JenkinsBuildServiceImpl extends BaseServiceImpl implements IJenkinsBuildService { @Resource private IExternalSystemRepository externalSystemRepository; @@ -96,8 +108,6 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl syncView(context.getExternalSystem(), view), threadPoolTaskExecutor )) - .collect(Collectors.toList()); + .toList(); // 3. 等待所有任务完成并汇总结果 int totalSyncedBuilds = futures.stream() @@ -160,7 +170,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl jobs = jenkinsJobRepository.findByExternalSystemIdAndViewId( externalSystem.getId(), view.getId()); - + if (jobs.isEmpty()) { return 0; } @@ -223,7 +233,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl getNewBuilds( - ExternalSystem externalSystem, - JenkinsJob job, - JenkinsJobResponse jobResponse, - Optional lastBuild) { - + ExternalSystem externalSystem, + JenkinsJob job, + JenkinsJobResponse jobResponse, + Optional lastBuild) { + // 1. 获取最新构建号(从jobResponse中获取,避免额外的API调用) if (jobResponse.getLastBuild() == null) { log.info("No builds found for job: {}", job.getJobName()); @@ -268,7 +278,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl builds = jenkinsServiceIntegration.listBuilds(externalSystem, job.getJobName()); - + // 5. 过滤出需要的构建 List newBuilds = builds.stream() .filter(build -> build.getNumber() >= fromBuildNumber && build.getNumber() <= latestBuildNumber) @@ -280,9 +290,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl builds) { + ExternalSystem externalSystem, + JenkinsJob job, + List builds) { List jenkinsBuilds = builds.stream() .map(buildResponse -> { JenkinsBuild build = new JenkinsBuild(); @@ -310,9 +320,9 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl recentBuilds = jenkinsBuildRepository .findByExternalSystemIdAndStarttimeAfter(externalSystemId, recentSince); - + // 2. 查询未完成通知的构建ID(用于发送结束通知和兜底处理) List pendingNotifications = jenkinsBuildNotificationRepository .findByBuildEndNoticeFalseAndDeletedFalse(); Set pendingBuildIds = pendingNotifications.stream() .map(JenkinsBuildNotification::getBuildId) .collect(Collectors.toSet()); - + // 3. 查询未完成通知对应的构建记录 - List pendingBuilds = pendingBuildIds.isEmpty() + List 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 buildMap = new HashMap<>(); recentBuilds.forEach(b -> buildMap.put(b.getId(), b)); pendingBuilds.forEach(b -> buildMap.put(b.getId(), b)); List buildsToProcess = new ArrayList<>(buildMap.values()); - + if (buildsToProcess.isEmpty()) { log.info("没有需要处理的构建记录"); return; } - + // 2. 反查团队绑定关系(只查询构建类型为JENKINS的应用) List teamApps = teamApplicationRepository.findByDeploySystemIdAndBuildType(externalSystemId, BuildTypeEnum.JENKINS); - + if (teamApps.isEmpty()) { log.info("没有团队绑定该Jenkins系统: externalSystemId={}", externalSystemId); return; } - + // 3. 按 deploy_job 分组 TeamApplication Map> teamAppsByJob = teamApps.stream() .collect(Collectors.groupingBy(TeamApplication::getDeployJob)); - + // 4. 查询启用了构建通知的团队环境配置 Set teamIds = teamApps.stream() .map(TeamApplication::getTeamId) .collect(Collectors.toSet()); - + Set envIds = teamApps.stream() .map(TeamApplication::getEnvironmentId) .collect(Collectors.toSet()); - - List configs = + + List configs = teamEnvironmentNotificationConfigRepository .findByTeamIdInAndEnvironmentIdInAndBuildNotificationEnabledTrue(teamIds, envIds); - + if (configs.isEmpty()) { log.info("没有团队启用构建通知"); return; } - + // 5. 按 team_id + environment_id 分组配置 Map configMap = configs.stream() .collect(Collectors.toMap( cfg -> cfg.getTeamId() + "_" + cfg.getEnvironmentId(), cfg -> cfg )); - + // 6. 批量查询通知渠道 Set channelIds = configs.stream() .map(TeamEnvironmentNotificationConfig::getNotificationChannelId) .filter(Objects::nonNull) .collect(Collectors.toSet()); - - Map channelMap = + + Map channelMap = notificationChannelRepository.findAllById(channelIds).stream() .collect(Collectors.toMap(NotificationChannel::getId, c -> c)); - + // 7. 批量查询 Job Set jobIds = buildsToProcess.stream() .map(JenkinsBuild::getJobId) .collect(Collectors.toSet()); - + Map jobMap = jenkinsJobRepository .findAllById(jobIds).stream() .collect(Collectors.toMap(JenkinsJob::getId, j -> j)); - + // 8. 按 job_name 分组构建记录 Map> buildsByJobName = buildsToProcess.stream() .collect(Collectors.groupingBy( build -> jobMap.get(build.getJobId()).getJobName() )); - + // 9. 批量查询应用信息 Set applicationIds = teamApps.stream() .map(TeamApplication::getApplicationId) @@ -549,66 +559,54 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl 6) { return; } - + // 6分钟内的新构建,发送"构建中"通知 record = new JenkinsBuildNotification(); record.setBuildId(build.getId()); @@ -619,120 +617,106 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl= 2) { - log.warn("构建超时未完成,强制标记结束通知: buildId={}, teamId={}, envId={}, startTime={}", + log.warn("构建超时未完成,强制标记结束通知: buildId={}, teamId={}, envId={}, startTime={}", build.getId(), config.getTeamId(), config.getEnvironmentId(), build.getStarttime()); record.setBuildEndNotice(true); jenkinsBuildNotificationRepository.save(record); } } } - + } catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) { // 乐观锁冲突,说明记录已被其他线程更新,跳过即可 - log.warn("构建通知记录乐观锁冲突,跳过处理: teamId={}, envId={}, buildId={}", - config.getTeamId(), config.getEnvironmentId(), build.getId()); + log.warn("构建通知记录乐观锁冲突,跳过处理: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId()); } catch (Exception e) { - log.error("处理构建通知失败: teamId={}, envId={}, buildId={}", - config.getTeamId(), config.getEnvironmentId(), build.getId(), e); + log.error("处理构建通知失败: teamId={}, envId={}, buildId={}", config.getTeamId(), config.getEnvironmentId(), build.getId(), e); } } - + /** * 判断构建是否已结束 */ private boolean isBuildFinished(JenkinsBuild build) { String status = build.getBuildStatus(); - return "SUCCESS".equals(status) || - "FAILURE".equals(status) || - "ABORTED".equals(status); + return "SUCCESS".equals(status) || "FAILURE".equals(status) || "ABORTED".equals(status); } - + /** * 发送通知(使用模板) */ - private void sendNotification( - TeamEnvironmentNotificationConfig config, - NotificationChannel channel, - JenkinsJob job, - JenkinsBuild build, - String status, - ExternalSystem externalSystem, - Application application, - Environment environment) { - + private void sendNotification(TeamEnvironmentNotificationConfig config, NotificationChannel channel, JenkinsJob job, JenkinsBuild build, String status, ExternalSystem externalSystem, Application application, Environment environment) { + try { // 1. 检查是否配置了构建通知模板 if (config.getBuildNotificationTemplateId() == null) { - log.warn("未配置构建通知模板,跳过通知: teamId={}, envId={}", + log.warn("未配置构建通知模板,跳过通知: teamId={}, envId={}", config.getTeamId(), config.getEnvironmentId()); return; } - + // 2. 查询模板 - NotificationTemplate template = notificationTemplateRepository - .findById(config.getBuildNotificationTemplateId()) - .orElse(null); + NotificationTemplate template = notificationTemplateRepository.findById(config.getBuildNotificationTemplateId()).orElse(null); if (template == null) { log.warn("构建通知模板不存在: templateId={}", config.getBuildNotificationTemplateId()); return; } - + // 3. 构建模板参数 Map templateParams = new HashMap<>(); - + // 应用信息 if (application != null) { templateParams.put("applicationId", application.getId()); templateParams.put("applicationCode", application.getAppCode()); templateParams.put("applicationName", application.getAppName()); } - + // 环境信息 if (environment != null) { templateParams.put("environmentId", environment.getId()); templateParams.put("environmentCode", environment.getEnvCode()); templateParams.put("environmentName", environment.getEnvName()); } - + // Jenkins 构建信息 templateParams.put("jobName", job.getJobName()); templateParams.put("buildNumber", build.getBuildNumber()); templateParams.put("buildUrl", build.getBuildUrl()); templateParams.put("buildStatus", status); - + // 时间格式化 - java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); if (build.getStarttime() != null) { templateParams.put("buildStartTime", build.getStarttime().format(formatter)); } - + // 构建结束时间(如果有) if (build.getStarttime() != null && build.getDuration() != null) { LocalDateTime endTime = build.getStarttime().plusNanos(build.getDuration() * 1_000_000); templateParams.put("buildEndTime", endTime.format(formatter)); } - + // 耗时格式化 if (build.getDuration() != null) { long totalSeconds = build.getDuration() / 1000; @@ -741,49 +725,44 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl 0) { @@ -844,7 +822,7 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl 0) { commitMessages.append("\n"); @@ -864,78 +842,72 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl map = - new org.springframework.util.LinkedMultiValueMap<>(); + FileSystemResource fileResource = new FileSystemResource(filePath); + + LinkedMultiValueMap 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> requestEntity = - new org.springframework.http.HttpEntity<>(map, headers); - - org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate(); - org.springframework.http.ResponseEntity response = - restTemplate.postForEntity(uploadUrl, requestEntity, String.class); - + + HttpEntity> requestEntity = new HttpEntity<>(map, headers); + + RestTemplate restTemplate = new org.springframework.web.client.RestTemplate(); + ResponseEntity response = restTemplate.postForEntity(uploadUrl, requestEntity, String.class); + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { // 解析响应,获取 media_id JsonNode jsonNode = JsonUtils.parseJson(response.getBody()); @@ -980,27 +948,27 @@ public class JenkinsBuildServiceImpl extends BaseServiceImpl message = new HashMap<>(); message.put("msgtype", "file"); - + Map 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> requestEntity = - new org.springframework.http.HttpEntity<>(message, headers); - - org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate(); - org.springframework.http.ResponseEntity response = - restTemplate.postForEntity(webhookUrl, requestEntity, String.class); - + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> requestEntity = new HttpEntity<>(message, headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity 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); } } - + /** * 清理临时文件 */ diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql index 14f473a3..4cff1be9 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql @@ -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), -- 表单数据详情(隐藏路由,用于查看表单数据)