增加构建通知

This commit is contained in:
dengqichen 2025-11-14 11:18:00 +08:00
parent 123b9054da
commit 3645d8d062
4 changed files with 201 additions and 127 deletions

View File

@ -21,13 +21,6 @@ public interface IJenkinsServiceIntegration extends IExternalSystemIntegration {
*/ */
boolean testConnection(ExternalSystem system); boolean testConnection(ExternalSystem system);
/**
* 获取Jenkins Crumb用于CSRF保护
*
* @param externalSystem Jenkins系统配置
* @return Crumb响应信息
*/
JenkinsCrumbIssuerResponse getJenkinsCrumbIssue(ExternalSystem externalSystem);
/** /**
* 使用参数触发构建 * 使用参数触发构建
@ -88,7 +81,7 @@ public interface IJenkinsServiceIntegration extends IExternalSystemIntegration {
* 获取任务详情 * 获取任务详情
* *
* @param externalSystem Jenkins系统配置 * @param externalSystem Jenkins系统配置
* @param jobName 任务名称 * @param jobName 任务名称
* @return 任务详情 * @return 任务详情
*/ */
JenkinsJobResponse job(ExternalSystem externalSystem, String jobName); JenkinsJobResponse job(ExternalSystem externalSystem, String jobName);

View File

@ -7,6 +7,7 @@ import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus; import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration; import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsJobResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsJobResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsCrumbIssuerResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsCrumbIssuerResponse;
@ -29,18 +30,22 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.StringReader; import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.xml.transform.Transformer; import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -48,6 +53,7 @@ import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@Service @Service
@ -58,14 +64,78 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
// Jenkins Crumb缓存 - 线程安全
private static final Map<Long, JenkinsCrumbCache> CRUMB_CACHE = new ConcurrentHashMap<>();
private static final long CRUMB_EXPIRE_TIME = 25 * 60 * 1000; // 25分钟过期
/**
* Jenkins Crumb缓存内部类
*/
private static class JenkinsCrumbCache {
final JenkinsCrumbIssuerResponse crumb;
final ExternalSystem decryptedSystem;
final long expireTime;
JenkinsCrumbCache(JenkinsCrumbIssuerResponse crumb, ExternalSystem system) {
this.crumb = crumb;
this.decryptedSystem = system;
this.expireTime = System.currentTimeMillis() + CRUMB_EXPIRE_TIME;
}
boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
/**
* 线程安全地获取Jenkins Crumb缓存
* 如果缓存不存在或已过期会重新获取和解密
*/
private synchronized JenkinsCrumbCache getCrumbCache(ExternalSystem system) {
Long systemId = system.getId();
JenkinsCrumbCache cache = CRUMB_CACHE.get(systemId);
if (cache == null || cache.isExpired()) {
log.debug("Jenkins Crumb缓存失效重新获取: systemId={}", systemId);
// 解密系统信息只在这里解密一次
ExternalSystem decryptedSystem = decryptSystem(system);
// 获取新的crumb
JenkinsCrumbIssuerResponse crumb = fetchCrumbFromJenkins(decryptedSystem);
// 创建新缓存
cache = new JenkinsCrumbCache(crumb, decryptedSystem);
CRUMB_CACHE.put(systemId, cache);
log.debug("Jenkins Crumb缓存已更新: systemId={}, expireTime={}", systemId, cache.expireTime);
}
return cache;
}
/**
* 直接从Jenkins服务器获取Crumb不经过缓存
*/
private JenkinsCrumbIssuerResponse fetchCrumbFromJenkins(ExternalSystem decryptedSystem) {
String url = decryptedSystem.getUrl() + "/crumbIssuer/api/json";
HttpHeaders headers = createBasicHeaders(decryptedSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return convertResponse(response);
}
@Override @Override
public boolean testConnection(ExternalSystem system) { public boolean testConnection(ExternalSystem system) {
// 解密系统信息
system = decryptSystem(system);
try { try {
// 直接使用原始系统信息构建URLURL不需要解密
String url = system.getUrl() + "/api/json"; String url = system.getUrl() + "/api/json";
// 创建请求头内部自动处理解密和crumb
HttpHeaders headers = createHeaders(system); HttpHeaders headers = createHeaders(system);
// 打印实际发送的请求头 // 打印实际发送的请求头
@ -88,71 +158,67 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
} }
} }
/**
* 创建包含认证信息和Jenkins Crumb的完整请求头
* 内部自动处理缓存获取和解密
*/
private HttpHeaders createHeaders(ExternalSystem system) { private HttpHeaders createHeaders(ExternalSystem system) {
HttpHeaders headers = new HttpHeaders(); // 获取缓存的解密系统和crumb
switch (system.getAuthType()) { JenkinsCrumbCache cache = getCrumbCache(system);
case BASIC -> { ExternalSystem decryptedSystem = cache.decryptedSystem;
String auth = system.getUsername() + ":" + system.getPassword(); JenkinsCrumbIssuerResponse crumb = cache.crumb;
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
headers.set("Authorization", "Basic " + new String(encodedAuth)); // 创建基础认证头
HttpHeaders headers = createBasicHeaders(decryptedSystem);
// 自动添加Jenkins Crumb和Cookie如果存在
if (crumb != null) {
if (crumb.getCrumb() != null) {
headers.set("Jenkins-Crumb", crumb.getCrumb());
}
if (crumb.getCookie() != null) {
headers.set("Cookie", crumb.getCookie());
} }
case TOKEN -> headers.set("Authorization", "Bearer " + system.getToken());
case OAUTH -> headers.set("Authorization", "Bearer " + system.getToken());
} }
return headers; return headers;
} }
private HttpEntity<String> createHttpEntity(ExternalSystem jenkins) { /**
* 创建基础认证头不包含crumb用于获取crumb时避免循环依赖
*/
private HttpHeaders createBasicHeaders(ExternalSystem decryptedSystem) {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
switch (decryptedSystem.getAuthType()) {
// 根据认证类型设置认证信息
switch (jenkins.getAuthType()) {
case BASIC -> { case BASIC -> {
// Basic认证 String auth = decryptedSystem.getUsername() + ":" + decryptedSystem.getPassword();
String auth = jenkins.getUsername() + ":" + jenkins.getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
String authHeader = "Basic " + new String(encodedAuth); headers.set("Authorization", "Basic " + new String(encodedAuth));
headers.set("Authorization", authHeader);
} }
case TOKEN -> { case TOKEN -> {
// Jenkins API Token认证也使用Basic Auth格式username:apiToken // Jenkins API Token认证也使用Basic Auth格式username:apiToken
String auth = jenkins.getUsername() + ":" + jenkins.getToken(); String auth = decryptedSystem.getUsername() + ":" + decryptedSystem.getToken();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
String authHeader = "Basic " + new String(encodedAuth); headers.set("Authorization", "Basic " + new String(encodedAuth));
headers.set("Authorization", authHeader);
} }
default -> throw new RuntimeException("Unsupported authentication type: " + jenkins.getAuthType()); default -> throw new RuntimeException("Unsupported authentication type: " + decryptedSystem.getAuthType());
} }
// 设置接受JSON响应 // 设置接受JSON响应
headers.set("Accept", "application/json"); headers.set("Accept", "application/json");
return headers;
return new HttpEntity<>(headers);
} }
@Override
public JenkinsCrumbIssuerResponse getJenkinsCrumbIssue(ExternalSystem externalSystem) {
externalSystem = decryptSystem(externalSystem);
String url = externalSystem.getUrl() + "/crumbIssuer/api/json";
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
return convertResponse(restTemplate.exchange(url, HttpMethod.GET, entity, String.class));
}
@Override @Override
public String buildWithParameters(ExternalSystem externalSystem, String jobName, Map<String, String> parameters) { public String buildWithParameters(ExternalSystem externalSystem, String jobName, Map<String, String> parameters) {
externalSystem = decryptSystem(externalSystem);
try { try {
// 如果参数中包含PIPELINE_SCRIPT确保Job配置中有该参数 // 如果参数中包含PIPELINE_SCRIPT确保Job配置中有该参数
if (parameters.containsKey("PIPELINE_SCRIPT")) { if (parameters.containsKey("PIPELINE_SCRIPT")) {
ensurePipelineScriptParameter(externalSystem, jobName); ensurePipelineScriptParameter(externalSystem, jobName);
} }
// 1. 获取Crumb // 直接使用原始系统信息构建URLURL不需要解密
JenkinsCrumbIssuerResponse jenkinsCrumbIssue = getJenkinsCrumbIssue(externalSystem);
// 2. 构建URL
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/") .path("/job/")
.path(jobName) .path(jobName)
@ -160,11 +226,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
.build() .build()
.toUriString(); .toUriString();
// 3. 构建请求头 // 创建请求头自动包含认证和crumb
HttpHeaders headers = createHeaders(externalSystem); HttpHeaders headers = createHeaders(externalSystem);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb());
headers.set("Cookie", jenkinsCrumbIssue.getCookie());
// 4. 构建请求体 // 4. 构建请求体
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(); MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
@ -196,20 +260,15 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
*/ */
private void ensurePipelineScriptParameter(ExternalSystem externalSystem, String jobName) { private void ensurePipelineScriptParameter(ExternalSystem externalSystem, String jobName) {
try { try {
// 获取 Crumb // 直接使用原始系统信息构建Job配置URLURL不需要解密
JenkinsCrumbIssuerResponse jenkinsCrumbIssue = getJenkinsCrumbIssue(externalSystem);
// 获取Job配置
String configUrl = String.format("%s/job/%s/config.xml", externalSystem.getUrl(), jobName); String configUrl = String.format("%s/job/%s/config.xml", externalSystem.getUrl(), jobName);
// 创建请求头自动包含认证和crumb
HttpHeaders headers = createHeaders(externalSystem); HttpHeaders headers = createHeaders(externalSystem);
// 设置请求和响应的字符编码
headers.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8"); headers.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
headers.set(HttpHeaders.CONTENT_TYPE, "application/xml;charset=UTF-8"); headers.set(HttpHeaders.CONTENT_TYPE, "application/xml;charset=UTF-8");
headers.setAcceptCharset(Collections.singletonList(Charset.forName("UTF-8"))); headers.setAcceptCharset(Collections.singletonList(Charset.forName("UTF-8")));
headers.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb());
headers.set("Cookie", jenkinsCrumbIssue.getCookie());
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
// 配置RestTemplate确保使用UTF-8编码 // 配置RestTemplate确保使用UTF-8编码
@ -327,8 +386,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
updateHeaders.setContentType(MediaType.APPLICATION_XML); updateHeaders.setContentType(MediaType.APPLICATION_XML);
updateHeaders.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8"); updateHeaders.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
updateHeaders.set("Content-Type", "application/xml; charset=utf-8"); updateHeaders.set("Content-Type", "application/xml; charset=utf-8");
updateHeaders.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb());
updateHeaders.set("Cookie", jenkinsCrumbIssue.getCookie());
HttpEntity<String> updateEntity = new HttpEntity<>(updatedConfig, updateHeaders); HttpEntity<String> updateEntity = new HttpEntity<>(updatedConfig, updateHeaders);
restTemplate.exchange( restTemplate.exchange(
configUrl, configUrl,
@ -373,9 +430,14 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
@Override @Override
public JenkinsQueueBuildInfoResponse getQueuedBuildInfo(ExternalSystem externalSystem, String queueId) { public JenkinsQueueBuildInfoResponse getQueuedBuildInfo(ExternalSystem externalSystem, String queueId) {
externalSystem = decryptSystem(externalSystem); // 直接使用原始系统信息构建URLURL不需要解密
String queueUrl = String.format("%s/queue/item/%s/api/json", externalSystem.getUrl().trim(), queueId); String queueUrl = String.format("%s/queue/item/%s/api/json", externalSystem.getUrl().trim(), queueId);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(queueUrl, HttpMethod.GET, createHttpEntity(externalSystem), new ParameterizedTypeReference<>() {
// 创建请求头内部自动处理解密和crumb
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(queueUrl, HttpMethod.GET, entity, new ParameterizedTypeReference<>() {
}); });
Map<String, Object> queueInfo = response.getBody(); Map<String, Object> queueInfo = response.getBody();
@ -404,13 +466,18 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
@Override @Override
public JenkinsBuildStatus getBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) { public JenkinsBuildStatus getBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
externalSystem = decryptSystem(externalSystem);
try { try {
// 直接使用原始系统信息构建URLURL不需要解密
String url = String.format("%s/job/%s/%d/api/json", externalSystem.getUrl().trim(), jobName, buildNumber); String url = String.format("%s/job/%s/%d/api/json", externalSystem.getUrl().trim(), jobName, buildNumber);
// 创建请求头内部自动处理解密和crumb
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange( ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url, url,
HttpMethod.GET, HttpMethod.GET,
createHttpEntity(externalSystem), entity,
new ParameterizedTypeReference<>() { new ParameterizedTypeReference<>() {
} }
); );
@ -435,13 +502,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
@Override @Override
public JenkinsBuildResponse getBuildDetails(ExternalSystem externalSystem, String jobName, Integer buildNumber) { public JenkinsBuildResponse getBuildDetails(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
externalSystem = decryptSystem(externalSystem);
try { try {
// 构建 tree 参数只获取我们需要的字段 // 构建 tree 参数只获取我们需要的字段
String treeQuery = "number,url,result,duration,timestamp,building," + String treeQuery = "number,url,result,duration,timestamp,building," +
"changeSets[items[commitId,author,message]]," + "changeSets[items[commitId,author,message]]," +
"artifacts[fileName,relativePath,displayPath]"; "artifacts[fileName,relativePath,displayPath]";
// 直接使用原始系统信息构建URLURL不需要解密
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/") .path("/job/")
.path(jobName) .path(jobName)
@ -452,7 +519,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
.build() .build()
.toUriString(); .toUriString();
HttpEntity<String> entity = new HttpEntity<>(createHeaders(externalSystem)); // 创建请求头自动包含认证和crumb
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
url, url,
HttpMethod.GET, HttpMethod.GET,
@ -563,10 +632,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
* @return 视图列表 * @return 视图列表
*/ */
public List<JenkinsViewResponse> listViews(ExternalSystem externalSystem) { public List<JenkinsViewResponse> listViews(ExternalSystem externalSystem) {
final ExternalSystem decryptedSystem = decryptSystem(externalSystem);
String treeQuery = "views[name,url,description,_class]"; String treeQuery = "views[name,url,description,_class]";
List<JenkinsViewResponse> views = callJenkinsApi( List<JenkinsViewResponse> views = callJenkinsApi(
decryptedSystem, externalSystem,
"/api/json", "/api/json",
treeQuery, treeQuery,
"views", "views",
@ -574,7 +642,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
); );
// 过滤和清洗数据 // 过滤和清洗数据
final String baseUrl = StringUtils.removeEnd(decryptedSystem.getUrl(), "/"); final String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/");
return views.stream() return views.stream()
.filter(view -> !"all".equalsIgnoreCase(view.getName())) .filter(view -> !"all".equalsIgnoreCase(view.getName()))
.peek(view -> { .peek(view -> {
@ -598,7 +666,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
* @return 任务列表 * @return 任务列表
*/ */
public List<JenkinsJobResponse> listJobs(ExternalSystem externalSystem, String viewName) { public List<JenkinsJobResponse> listJobs(ExternalSystem externalSystem, String viewName) {
externalSystem = decryptSystem(externalSystem);
// 只查询必要的字段确保包含lastBuild的timestamp字段 // 只查询必要的字段确保包含lastBuild的timestamp字段
String treeQuery = "jobs[name,url,description,buildable,nextBuildNumber," + String treeQuery = "jobs[name,url,description,buildable,nextBuildNumber," +
"lastBuild[number,timestamp,result],color]"; "lastBuild[number,timestamp,result],color]";
@ -632,9 +699,8 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
@Override @Override
public JenkinsJobResponse job(ExternalSystem externalSystem, String jobName) { public JenkinsJobResponse job(ExternalSystem externalSystem, String jobName) {
externalSystem = decryptSystem(externalSystem);
try { try {
// 1. 构建请求URL // 直接使用原始系统信息构建URLURL不需要解密
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/") .path("/job/")
.path(jobName) .path(jobName)
@ -681,7 +747,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
* @return 构建信息列表 * @return 构建信息列表
*/ */
public List<JenkinsBuildResponse> listBuilds(ExternalSystem externalSystem, String jobName) { public List<JenkinsBuildResponse> listBuilds(ExternalSystem externalSystem, String jobName) {
externalSystem = decryptSystem(externalSystem);
// Jenkins API 默认只返回最近的若干个构建通过 {from,to} 可以指定范围 // Jenkins API 默认只返回最近的若干个构建通过 {from,to} 可以指定范围
String treeQuery = "builds[number,url,result,timestamp,duration,building,actions[_class,parameters[_class,name,value]]]"; String treeQuery = "builds[number,url,result,timestamp,duration,building,actions[_class,parameters[_class,name,value]]]";
List<JenkinsBuildResponse> builds = callJenkinsApi( List<JenkinsBuildResponse> builds = callJenkinsApi(
@ -703,18 +768,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
} }
@Override @Override
public com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse getConsoleOutput( public JenkinsConsoleOutputResponse getConsoleOutput(ExternalSystem externalSystem, String jobName, Integer buildNumber, Long startOffset) {
ExternalSystem externalSystem,
String jobName,
Integer buildNumber,
Long startOffset) {
try { try {
if (startOffset == null) { if (startOffset == null) {
startOffset = 0L; startOffset = 0L;
} }
// 构建 Jenkins Progressive Text API URL // 直接使用原始系统信息构建 Jenkins Progressive Text API URLURL不需要解密
// Jenkins API: /job/{jobName}/{buildNumber}/logText/progressiveText?start={offset}
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/") .path("/job/")
.path(jobName) .path(jobName)
@ -725,7 +785,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
.build() .build()
.toUriString(); .toUriString();
// 发送请求 // 创建请求头自动包含认证和crumb
HttpHeaders headers = createHeaders(externalSystem); HttpHeaders headers = createHeaders(externalSystem);
headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN)); headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
@ -738,8 +798,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
); );
// 解析响应 // 解析响应
com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse result = JenkinsConsoleOutputResponse result = new JenkinsConsoleOutputResponse();
new com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse();
// 从响应体获取日志内容按行分割 // 从响应体获取日志内容按行分割
String logContent = response.getBody(); String logContent = response.getBody();
@ -767,12 +826,10 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
return result; return result;
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to get Jenkins console output: job={}, build={}, offset={}, error={}", log.error("Failed to get Jenkins console output: job={}, build={}, offset={}, error={}", jobName, buildNumber, startOffset, e.getMessage(), e);
jobName, buildNumber, startOffset, e.getMessage(), e);
// 返回空结果而不是抛异常避免中断轮询 // 返回空结果而不是抛异常避免中断轮询
com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse emptyResult = JenkinsConsoleOutputResponse emptyResult = new JenkinsConsoleOutputResponse();
new com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse();
emptyResult.setLines(Collections.emptyList()); emptyResult.setLines(Collections.emptyList());
emptyResult.setNextOffset(startOffset); emptyResult.setNextOffset(startOffset);
emptyResult.setHasMoreData(false); emptyResult.setHasMoreData(false);

View File

@ -43,8 +43,6 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
@Resource @Resource
private IExternalSystemRepository externalSystemRepository; private IExternalSystemRepository externalSystemRepository;
@Resource
private IJenkinsJobRepository jenkinsJobRepository;
@Resource @Resource
private IWorkflowNodeLogService workflowNodeLogService; private IWorkflowNodeLogService workflowNodeLogService;

View File

@ -1,11 +1,15 @@
package com.qqchen.deploy.backend.workflow.delegate; package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest; import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel; import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum; import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository; import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
import com.qqchen.deploy.backend.notification.service.INotificationService; import com.qqchen.deploy.backend.notification.service.INotificationService;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping; import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs; import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs;
@ -34,6 +38,9 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
@Resource @Resource
private INotificationChannelRepository notificationChannelRepository; private INotificationChannelRepository notificationChannelRepository;
@Resource
private INotificationTemplateRepository notificationTemplateRepository;
@Override @Override
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) { protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
// 1. 参数校验 // 1. 参数校验
@ -42,21 +49,22 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
return; return;
} }
try { try {
// 2. 查询渠道信息仅用于确定渠道类型 // 2. 查询渠道和模板信息
NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId()) NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId()).orElseThrow(() -> new RuntimeException("通知渠道不存在: " + input.getChannelId()));
.orElseThrow(() -> new RuntimeException("通知渠道不存在: " + input.getChannelId())); NotificationTemplate template = notificationTemplateRepository.findById(input.getNotificationTemplateId()).orElseThrow(() -> new RuntimeException("通知模板不存在: " + input.getNotificationTemplateId()));
// 3. 构建SendNotificationRequest // 3. 构建SendNotificationRequest
SendNotificationRequest request = new SendNotificationRequest(); SendNotificationRequest request = new SendNotificationRequest();
request.setNotificationTemplateId(input.getNotificationTemplateId()); request.setNotificationTemplateId(input.getNotificationTemplateId());
request.setTemplateParams(execution.getVariables()); request.setTemplateParams(execution.getVariables());
// 4. 根据渠道类型创建基础的sendRequest具体配置由NotificationService处理 // 4. 根据渠道类型创建sendRequest并从模板配置中获取参数
switch (channel.getChannelType()) { switch (channel.getChannelType()) {
case WEWORK -> { case WEWORK -> {
WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest(); WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest();
weworkRequest.setChannelId(input.getChannelId()); weworkRequest.setChannelId(input.getChannelId());
// 其他配置消息类型等由NotificationService根据模板配置自动设置 // 从模板配置中获取消息类型
weworkRequest.setMessageType(getWeworkMessageType(template));
request.setSendRequest(weworkRequest); request.setSendRequest(weworkRequest);
} }
case EMAIL -> { case EMAIL -> {
@ -81,6 +89,24 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
} }
} }
/**
* 从模板配置中获取企业微信消息类型
*/
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
try {
if (template.getTemplateConfig() != null) {
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
return weworkConfig.getMessageType();
}
}
} catch (Exception e) {
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
}
// 默认使用TEXT类型
return WeworkMessageTypeEnum.TEXT;
}
/** /**
* 获取邮件收件人列表 * 获取邮件收件人列表