diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java index 65728385..d244e691 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java @@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.deploy.integration.impl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.qqchen.deploy.backend.framework.utils.DateUtils; import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus; import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration; @@ -505,8 +506,12 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration try { // 构建 tree 参数,只获取我们需要的字段 String treeQuery = "number,url,result,duration,timestamp,building," + - "changeSets[items[commitId,author,message]]," + - "artifacts[fileName,relativePath,displayPath]"; + // 变更集字段更精确:author.fullName 与 msg + "changeSets[kind,items[commitId,author[fullName],msg,timestamp,affectedPaths]]," + + // 制品 + "artifacts[fileName,relativePath,displayPath]," + + // Git 插件 BuildData,兜底的 commit 信息 + "actions[_class,lastBuiltRevision[SHA1,branch[name,SHA1]]]"; // 直接使用原始系统信息构建URL(URL不需要解密) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) @@ -537,11 +542,76 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration JenkinsBuildResponse buildResponse = mapper.readValue(response.getBody(), JenkinsBuildResponse.class); // 清理URL,移除基础URL部分 + String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/"); if (buildResponse.getUrl() != null) { - String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/"); buildResponse.setUrl(buildResponse.getUrl().replace(baseUrl, "")); } + // 计算结束时间(毫秒与格式化),当 timestamp 与 duration 均存在 + if (buildResponse.getTimestamp() != null && buildResponse.getDuration() != null) { + long endMillis = buildResponse.getTimestamp() + buildResponse.getDuration(); + buildResponse.setEndTimeMillis(endMillis); + try { + buildResponse.setEndTime(DateUtils.formatMillis(endMillis)); + } catch (Exception ignore) { + // 忽略格式化异常,保留 endTimeMillis + } + } + + // 提取 gitCommitId:优先 changeSets.items[0].commitId,兜底 actions.BuildData.lastBuiltRevision.SHA1 + String commitId = null; + try { + // 先尝试从 changeSets 拿 + if (buildResponse.getChangeSets() != null && !buildResponse.getChangeSets().isEmpty()) { + var first = buildResponse.getChangeSets().get(0); + if (first.getItems() != null && !first.getItems().isEmpty()) { + commitId = first.getItems().get(0).getCommitId(); + } + } + if (commitId == null) { + // 兜底:解析原始 JSON 的 actions 查找 BuildData + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(response.getBody()); + com.fasterxml.jackson.databind.JsonNode actions = root.get("actions"); + if (actions != null && actions.isArray()) { + for (com.fasterxml.jackson.databind.JsonNode act : actions) { + com.fasterxml.jackson.databind.JsonNode clazz = act.get("_class"); + if (clazz != null && "hudson.plugins.git.util.BuildData".equals(clazz.asText())) { + com.fasterxml.jackson.databind.JsonNode lbr = act.get("lastBuiltRevision"); + if (lbr != null && lbr.get("SHA1") != null) { + commitId = lbr.get("SHA1").asText(); + break; + } + } + } + } + } + } catch (Exception ex) { + log.warn("Failed to extract git commit id from build details: {}", ex.getMessage()); + } + buildResponse.setGitCommitId(commitId); + + // 拼接制品完整URL串(逗号分隔) + try { + if (buildResponse.getArtifacts() != null && !buildResponse.getArtifacts().isEmpty()) { + String buildPath = buildResponse.getUrl() != null ? buildResponse.getUrl() : ( + "/job/" + jobName + "/" + buildNumber + "/"); + String buildPageUrl = baseUrl + (buildPath.startsWith("/") ? buildPath : "/" + buildPath); + if (!buildPageUrl.endsWith("/")) { + buildPageUrl = buildPageUrl + "/"; + } + final String artifactBaseUrl = buildPageUrl; + String joined = buildResponse.getArtifacts().stream() + .map(a -> artifactBaseUrl + "artifact/" + a.getRelativePath()) + .collect(java.util.stream.Collectors.joining(",")); + buildResponse.setArtifactUrl(joined); + } else { + buildResponse.setArtifactUrl(""); + } + } catch (Exception ex) { + log.warn("Failed to build artifact urls: {}", ex.getMessage()); + buildResponse.setArtifactUrl(""); + } + return buildResponse; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java index 5ab374d4..4738c1a2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/JenkinsBuildResponse.java @@ -1,6 +1,7 @@ package com.qqchen.deploy.backend.deploy.integration.response; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import java.util.List; import java.util.Map; @@ -9,6 +10,7 @@ import java.util.Map; * Jenkins构建响应对象 */ @Data +@JsonIgnoreProperties(ignoreUnknown = true) public class JenkinsBuildResponse { /** * 构建编号 @@ -45,6 +47,16 @@ public class JenkinsBuildResponse { * 构建持续时间(毫秒) */ private Long duration; + + /** + * 构建结束时间(毫秒)= timestamp + duration + */ + private Long endTimeMillis; + + /** + * 构建结束时间(格式化字符串),例如:yyyy-MM-dd HH:mm:ss + */ + private String endTime; /** * 预计持续时间 @@ -100,13 +112,23 @@ public class JenkinsBuildResponse { * 构建制品 */ private List artifacts; + /** + * 构建制品URL(逗号分隔的完整URL串),由服务层拼接 + */ + private String artifactUrl; /** * 构建控制台输出 */ private String consoleLog; + + /** + * 提取的Git提交ID(兜底自 actions.BuildData.lastBuiltRevision.SHA1 或 changeSets) + */ + private String gitCommitId; @Data + @JsonIgnoreProperties(ignoreUnknown = true) public static class BuildParameter { @JsonProperty("_class") private String type; @@ -143,4 +165,4 @@ public class JenkinsBuildResponse { private String fileName; private String relativePath; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/DateUtils.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/DateUtils.java new file mode 100644 index 00000000..9d921e1a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/DateUtils.java @@ -0,0 +1,24 @@ +package com.qqchen.deploy.backend.framework.utils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class DateUtils { + + public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + public static String formatMillis(long millis) { + return formatMillis(millis, DEFAULT_PATTERN); + } + + public static String formatMillis(long millis, String pattern) { + LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()); + return time.format(DateTimeFormatter.ofPattern(pattern)); + } + + public static LocalDateTime toLocalDateTime(long millis) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java index 4645a90e..0477f8e2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/notification/adapter/impl/WeworkChannelAdapter.java @@ -92,8 +92,8 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter "## " + title + "\n\n" + content; - case TEXT -> "【" + title + "】\n" + content; + case MARKDOWN -> title + "\n\n" + content; + case TEXT -> title + "\n" + content; case FILE -> content; // 文件类型不处理标题 }; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseNodeDelegate.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseNodeDelegate.java index 2fbd52e9..57a82e35 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseNodeDelegate.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseNodeDelegate.java @@ -102,7 +102,7 @@ public abstract class BaseNodeDelegate implements JavaDelegate { log.info("Stored NodeContext for: {}", currentNodeId); } catch (Exception e) { - // ❌ 业务异常:根据 continueOnFailure 配置决定行为 + // 业务异常:根据 continueOnFailure 配置决定行为 log.error("Business exception in node: {}", currentNodeId, e); boolean continueOnFailure = WorkflowUtils.getContinueOnFailure(currentInputMapping); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java index 59ce3ff5..f2f659da 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java @@ -24,6 +24,7 @@ import org.flowable.engine.delegate.BpmnError; import org.flowable.engine.delegate.DelegateExecution; import org.springframework.stereotype.Component; + import java.util.HashMap; import java.util.Map; @@ -83,7 +84,6 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate buildInfo.getBuildUrl() + "artifact/" + artifact.getRelativePath()) - .collect(java.util.stream.Collectors.joining(",")); - output.setArtifactUrl(artifactUrls); - } else { - log.warn("No artifacts found in build details"); - output.setArtifactUrl(""); - } + // 提取 Git Commit ID(由服务层兜底填充) + output.setGitCommitId(buildDetails.getGitCommitId() != null ? buildDetails.getGitCommitId() : ""); + + // 直接使用服务层拼接的制品URL串 + output.setArtifactUrl(buildDetails.getArtifactUrl() != null ? buildDetails.getArtifactUrl() : ""); // 记录完成日志 workflowNodeLogService.info(execution.getProcessInstanceId(), execution.getCurrentActivityId(), LogSource.JENKINS, "Jenkins 构建任务执行完成"); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/JenkinsBuildOutputs.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/JenkinsBuildOutputs.java index f5b6c8fd..69196c1f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/JenkinsBuildOutputs.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/JenkinsBuildOutputs.java @@ -43,5 +43,9 @@ public class JenkinsBuildOutputs extends BaseNodeOutputs { * 构建时长(秒) */ private Integer buildDuration; + + private Long deployEndTimeMillis; + + private String deployEndTime; }