增加生成后端服务代码。

This commit is contained in:
dengqichen 2025-10-22 22:44:18 +08:00
parent 197439a381
commit 332f27fac1
10 changed files with 234 additions and 35 deletions

View File

@ -52,6 +52,16 @@ public interface IJenkinsServiceIntegration extends IExternalSystemIntegration {
*/
JenkinsBuildStatus getBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber);
/**
* 获取构建详细信息
*
* @param externalSystem Jenkins系统配置
* @param jobName 任务名称
* @param buildNumber 构建编号
* @return 构建详细信息
*/
JenkinsBuildResponse getBuildDetails(ExternalSystem externalSystem, String jobName, Integer buildNumber);
/**
* 查询所有视图
*

View File

@ -419,6 +419,56 @@ public class JenkinsServiceIntegrationImpl implements IJenkinsServiceIntegration
}
}
@Override
public JenkinsBuildResponse getBuildDetails(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
try {
// 构建 tree 参数只获取我们需要的字段
String treeQuery = "number,url,result,duration,timestamp,building," +
"changeSets[items[commitId,author,message]]," +
"artifacts[fileName,relativePath,displayPath]";
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/")
.path(jobName)
.path("/")
.path(buildNumber.toString())
.path("/api/json")
.queryParam("tree", treeQuery)
.build()
.toUriString();
HttpEntity<String> entity = new HttpEntity<>(createHeaders(externalSystem));
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
// 打印原始响应用于调试
log.info("Jenkins build details raw response: {}", response.getBody());
ObjectMapper mapper = new ObjectMapper();
JenkinsBuildResponse buildResponse = mapper.readValue(response.getBody(), JenkinsBuildResponse.class);
// 清理URL移除基础URL部分
if (buildResponse.getUrl() != null) {
String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/");
buildResponse.setUrl(buildResponse.getUrl().replace(baseUrl, ""));
}
return buildResponse;
}
throw new RuntimeException("Failed to get build details: empty response");
} catch (Exception e) {
log.error("Failed to get build details: job={}, buildNumber={}, error={}",
jobName, buildNumber, e.getMessage(), e);
throw new RuntimeException("获取Jenkins构建详情失败: " + jobName + "#" + buildNumber, e);
}
}
private String extractQueueId(String location) {
// location格式: http://jenkins-url/queue/item/12345/
return location.replaceAll(".*/item/(\\d+)/.*", "$1");

View File

@ -10,6 +10,7 @@ import com.qqchen.deploy.backend.deploy.repository.IJenkinsJobRepository;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.JenkinsBuildInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.JenkinsBuildOutputs;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.BpmnError;
@ -49,31 +50,79 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
Map<String, Object> configs,
JenkinsBuildInputMapping input
) {
log.info("Jenkins Build - serverId: {}, project: {}",
log.info("Jenkins Build - serverId: {}, jobName: {}",
input.getJenkinsServerId(), input.getProject());
// 1. 获取外部系统和Jenkins Job
// 1. 获取外部系统
ExternalSystem externalSystem = externalSystemRepository.findById(input.getJenkinsServerId().longValue())
.orElseThrow(() -> new RuntimeException("Jenkins服务器不存在: " + input.getJenkinsServerId()));
// 2. 触发构建这里简化处理实际需要根据project获取jenkinsJob
// TODO: 根据input.getProject()查找对应的JenkinsJob
Map<String, String> parameters = new HashMap<>();
// parameters.put("PROJECT", input.getProject());
String jobName = input.getProject();
// String queueId = jenkinsServiceIntegration.buildWithParameters(
// externalSystem, input.getProject(), parameters);
// JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId);
// JenkinsBuildStatus status = pollBuildStatus(externalSystem, input.getProject(), buildInfo.getBuildNumber());
// 2. 触发构建
Map<String, String> parameters = new HashMap<>();
// 可以根据需要添加构建参数
// parameters.put("BRANCH", "main");
String queueId = jenkinsServiceIntegration.buildWithParameters(
externalSystem, jobName, parameters);
// 3. 等待构建从队列中开始
JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId);
// 4. 轮询构建状态直到完成
// 注意如果构建失败或被取消pollBuildStatus 会抛出 BpmnError触发错误边界事件
// 只有成功时才会返回到这里
JenkinsBuildStatus buildStatus = pollBuildStatus(externalSystem, jobName, buildInfo.getBuildNumber());
// 3. 构造输出结果
// 5. 获取构建详细信息包括 duration, changeSets, artifacts
com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse buildDetails =
jenkinsServiceIntegration.getBuildDetails(externalSystem, jobName, buildInfo.getBuildNumber());
// 打印调试信息
log.info("Build details - changeSets: {}, artifacts: {}",
buildDetails.getChangeSets(), buildDetails.getArtifacts());
// 6. 构造输出结果执行到这里说明构建成功
JenkinsBuildOutputs outputs = new JenkinsBuildOutputs();
outputs.setBuildStatus("SUCCESS"); // TODO: 从实际构建状态获取
outputs.setBuildNumber(123); // TODO: 从buildInfo获取
outputs.setBuildUrl("http://jenkins.example.com/job/" + input.getProject() + "/123/");
outputs.setGitCommitId("a3f5e8d2c4b1a5e9f2d3e7b8c9d1a2f3e4b5c6d7");
outputs.setBuildDuration(120);
outputs.setArtifactUrl("http://jenkins.example.com/job/" + input.getProject() + "/123/artifact/target/app.jar");
// 设置统一的执行状态为成功
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS);
// 设置 Jenkins 特有字段
outputs.setBuildStatus(buildStatus.name());
outputs.setBuildNumber(buildInfo.getBuildNumber());
outputs.setBuildUrl(buildInfo.getBuildUrl());
// 从构建详情中提取信息
outputs.setBuildDuration(buildDetails.getDuration() != null ? buildDetails.getDuration().intValue() : 0);
// 提取 Git Commit ID changeSets 中获取第一个
if (buildDetails.getChangeSets() != null && !buildDetails.getChangeSets().isEmpty()) {
log.info("Found {} changeSets", buildDetails.getChangeSets().size());
var changeSet = buildDetails.getChangeSets().get(0);
if (changeSet.getItems() != null && !changeSet.getItems().isEmpty()) {
log.info("Found {} items in changeSet", changeSet.getItems().size());
outputs.setGitCommitId(changeSet.getItems().get(0).getCommitId());
}
} else {
log.warn("No changeSets found in build details");
}
if (outputs.getGitCommitId() == null) {
outputs.setGitCommitId("");
}
// 提取构建制品URL如果有多个制品拼接成逗号分隔的列表
if (buildDetails.getArtifacts() != null && !buildDetails.getArtifacts().isEmpty()) {
log.info("Found {} artifacts", buildDetails.getArtifacts().size());
String artifactUrls = buildDetails.getArtifacts().stream()
.map(artifact -> buildInfo.getBuildUrl() + "artifact/" + artifact.getRelativePath())
.collect(java.util.stream.Collectors.joining(","));
outputs.setArtifactUrl(artifactUrls);
} else {
log.warn("No artifacts found in build details");
outputs.setArtifactUrl("");
}
return outputs;
}
@ -101,7 +150,7 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Build did not start within %d seconds", MAX_QUEUE_POLLS * QUEUE_POLL_INTERVAL));
}
private void pollBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
private JenkinsBuildStatus pollBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) {
int attempts = 0;
while (attempts < MAX_BUILD_POLLS) {
try {
@ -114,27 +163,34 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
switch (status) {
case SUCCESS:
// 构建成功返回继续流程
return;
// 构建成功返回状态
log.info("Jenkins build succeeded: job={}, buildNumber={}", jobName, buildNumber);
return status;
case FAILURE:
// 构建失败抛出错误
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build failed: job=%s, buildNumber=%d", jobName, buildNumber));
// 构建失败抛出错误触发边界事件
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build failed: job=%s, buildNumber=%d", jobName, buildNumber));
case ABORTED:
// 构建被取消抛出错误
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build was aborted: job=%s, buildNumber=%d", jobName, buildNumber));
// 构建被取消抛出错误触发边界事件
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build was aborted: job=%s, buildNumber=%d", jobName, buildNumber));
case IN_PROGRESS:
// 继续轮询
attempts++;
break;
case NOT_FOUND:
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build not found: job=%s, buildNumber=%d", jobName, buildNumber));
// 构建记录丢失抛出系统异常
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build not found: job=%s, buildNumber=%d", jobName, buildNumber));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Build status polling was interrupted");
}
}
// 超过最大轮询次数视为超时
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d", MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber));
// 超过最大轮询次数视为超时系统异常
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d",
MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber));
}
}

View File

@ -46,13 +46,15 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
// 3. 返回成功结果
NotificationOutputs outputs = new NotificationOutputs();
outputs.setStatus("SUCCESS");
outputs.setStatus(com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum.SUCCESS);
outputs.setMessage("通知发送成功");
return outputs;
} catch (Exception e) {
log.error("Failed to send notification", e);
NotificationOutputs outputs = new NotificationOutputs();
outputs.setStatus("FAILURE");
outputs.setStatus(com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum.FAILURE);
outputs.setMessage("通知发送失败: " + e.getMessage());
return outputs;
}
}

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.workflow.dto.outputs;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 审批节点输出
@ -9,7 +10,8 @@ import lombok.Data;
* @since 2025-10-22
*/
@Data
public class ApprovalOutputs {
@EqualsAndHashCode(callSuper = true)
public class ApprovalOutputs extends BaseNodeOutputs {
/**
* 审批结果

View File

@ -0,0 +1,22 @@
package com.qqchen.deploy.backend.workflow.dto.outputs;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import lombok.Data;
/**
* 节点输出基类
* 所有节点输出类的公共父类包含统一的执行状态字段
*
* @author qqchen
* @since 2025-10-22
*/
@Data
public class BaseNodeOutputs {
/**
* 节点执行状态
* 用于流程条件判断所有节点统一使用此字段
*/
private NodeExecutionStatusEnum status;
}

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.workflow.dto.outputs;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Jenkins构建节点输出
@ -9,7 +10,8 @@ import lombok.Data;
* @since 2025-10-22
*/
@Data
public class JenkinsBuildOutputs {
@EqualsAndHashCode(callSuper = true)
public class JenkinsBuildOutputs extends BaseNodeOutputs {
/**
* 构建编号

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.workflow.dto.outputs;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知节点输出
@ -9,11 +10,12 @@ import lombok.Data;
* @since 2025-10-22
*/
@Data
public class NotificationOutputs {
@EqualsAndHashCode(callSuper = true)
public class NotificationOutputs extends BaseNodeOutputs {
/**
* 执行状态
* 通知发送结果详细信息
*/
private String status;
private String message;
}

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.workflow.dto.outputs;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Shell脚本节点输出
@ -9,7 +10,8 @@ import lombok.Data;
* @since 2025-10-22
*/
@Data
public class ShellOutputs {
@EqualsAndHashCode(callSuper = true)
public class ShellOutputs extends BaseNodeOutputs {
/**
* 退出码

View File

@ -0,0 +1,51 @@
package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
/**
* 节点执行状态枚举
* 所有工作流节点的统一执行状态
*
* @author qqchen
* @since 2025-10-22
*/
@Getter
public enum NodeExecutionStatusEnum {
/**
* 执行成功
*/
SUCCESS("SUCCESS", "执行成功"),
/**
* 执行失败
*/
FAILURE("FAILURE", "执行失败");
@JsonValue
private final String code;
private final String description;
NodeExecutionStatusEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据编码获取枚举
*
* @param code 状态编码
* @return 枚举实例
*/
public static NodeExecutionStatusEnum fromCode(String code) {
for (NodeExecutionStatusEnum status : values()) {
if (status.code.equals(code)) {
return status;
}
}
throw new IllegalArgumentException("Unknown NodeExecutionStatus code: " + code);
}
}