打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-04 10:58:02 +08:00
parent c5b4b6046a
commit c3ee349760
10 changed files with 436 additions and 171 deletions

View File

@ -16,6 +16,11 @@ public enum DeployRecordStatusEnums {
*/
CREATED("CREATED", "已创建"),
/**
* 待审批
*/
PENDING_APPROVAL("PENDING_APPROVAL", "待审批"),
/**
* 运行中
*/

View File

@ -47,9 +47,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
String deployRemark
) {
// 检查是否已存在
DeployRecord existing = deployRecordRepository
.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId)
.orElse(null);
DeployRecord existing = deployRecordRepository.findByWorkflowInstanceIdAndDeletedFalse(workflowInstanceId).orElse(null);
if (existing != null) {
log.warn("部署记录已存在: workflowInstanceId={}", workflowInstanceId);
@ -70,8 +68,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
record.setStartTime(LocalDateTime.now());
DeployRecord saved = deployRecordRepository.save(record);
log.info("创建部署记录成功: id={}, workflowInstanceId={}, applicationId={}, environmentId={}",
saved.getId(), workflowInstanceId, applicationId, environmentId);
log.info("创建部署记录成功: id={}, workflowInstanceId={}, applicationId={}, environmentId={}", saved.getId(), workflowInstanceId, applicationId, environmentId);
return deployRecordConverter.toDto(saved);
}

View File

@ -503,6 +503,7 @@ public class DeployServiceImpl implements IDeployService {
// 转换为 MapFlowable 只支持基本类型
variables.put("jenkins", objectMapper.convertValue(jenkinsInput, Map.class));
variables.put("approval", Map.of("required", true, "userIds", "admin"));
variables.put("notification", Map.of("channelId", 1));
}

View File

@ -0,0 +1,348 @@
package com.qqchen.deploy.backend.framework.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.task.service.delegate.DelegateTask;
import java.util.Map;
/**
* 嵌套 Map 工具类
* 使用 Jackson JsonPointer (RFC 6901) 标准解析嵌套路径
*
* @author qqchen
* @since 2025-11-04
*/
@Slf4j
public class NestedMapUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* 从嵌套 Map 中获取值
* 支持多层嵌套路径访问例如outputs.statusa.b.c.d
*
* <p>示例
* <pre>
* Map<String, Object> map = Map.of("outputs", Map.of("status", "SUCCESS"));
* Object value = NestedMapUtils.getValue(map, "outputs.status");
* // 返回: "SUCCESS"
* </pre>
*
* @param map Map
* @param fieldPath 字段路径点号分隔outputs.status
* @return 字段值如果路径不存在则返回 null
*/
public static Object getValue(Map<String, Object> map, String fieldPath) {
if (fieldPath == null || fieldPath.isEmpty() || map == null) {
return null;
}
try {
// 1. Map 转换为 JsonNode
JsonNode rootNode = OBJECT_MAPPER.valueToTree(map);
// 2. 将点号路径转换为 JsonPointer 路径格式
// 例如outputs.status -> /outputs/status
String jsonPointerPath = "/" + fieldPath.replace(".", "/");
// 3. 使用 JsonPointer 访问嵌套路径
JsonNode resultNode = rootNode.at(jsonPointerPath);
// 4. 检查节点是否存在
if (resultNode.isMissingNode()) {
log.debug("嵌套路径不存在: {}", fieldPath);
return null;
}
// 5. JsonNode 转换回 Java 对象
return OBJECT_MAPPER.convertValue(resultNode, Object.class);
} catch (Exception e) {
log.warn("嵌套路径解析异常: {} - {}", fieldPath, e.getMessage());
return null;
}
}
/**
* 从嵌套 Map 中获取字符串值
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @return 字符串值如果路径不存在或值为 null 则返回 null
*/
public static String getString(Map<String, Object> map, String fieldPath) {
Object value = getValue(map, fieldPath);
return value != null ? value.toString() : null;
}
/**
* 从嵌套 Map 中获取字符串值带默认值
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @param defaultValue 默认值
* @return 字符串值如果路径不存在则返回默认值
*/
public static String getString(Map<String, Object> map, String fieldPath, String defaultValue) {
String value = getString(map, fieldPath);
return value != null ? value : defaultValue;
}
/**
* 从嵌套 Map 中获取 Integer
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @return Integer 如果路径不存在或转换失败则返回 null
*/
public static Integer getInteger(Map<String, Object> map, String fieldPath) {
Object value = getValue(map, fieldPath);
if (value == null) {
return null;
}
if (value instanceof Integer) {
return (Integer) value;
}
try {
return Integer.valueOf(value.toString());
} catch (NumberFormatException e) {
log.warn("无法将值转换为 Integer: {} = {}", fieldPath, value);
return null;
}
}
/**
* 从嵌套 Map 中获取 Long
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @return Long 如果路径不存在或转换失败则返回 null
*/
public static Long getLong(Map<String, Object> map, String fieldPath) {
Object value = getValue(map, fieldPath);
if (value == null) {
return null;
}
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Integer) {
return ((Integer) value).longValue();
}
try {
return Long.valueOf(value.toString());
} catch (NumberFormatException e) {
log.warn("无法将值转换为 Long: {} = {}", fieldPath, value);
return null;
}
}
/**
* 从嵌套 Map 中获取 Boolean
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @return Boolean 如果路径不存在则返回 null
*/
public static Boolean getBoolean(Map<String, Object> map, String fieldPath) {
Object value = getValue(map, fieldPath);
if (value == null) {
return null;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return Boolean.valueOf(value.toString());
}
/**
* 从嵌套 Map 中获取指定类型的值
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @param targetClass 目标类型
* @param <T> 返回值类型
* @return 指定类型的值如果路径不存在或转换失败则返回 null
*/
@SuppressWarnings("unchecked")
public static <T> T getValue(Map<String, Object> map, String fieldPath, Class<T> targetClass) {
Object value = getValue(map, fieldPath);
if (value == null) {
return null;
}
try {
if (targetClass.isInstance(value)) {
return (T) value;
}
return OBJECT_MAPPER.convertValue(value, targetClass);
} catch (Exception e) {
log.warn("无法将值转换为 {}: {} = {}", targetClass.getSimpleName(), fieldPath, value);
return null;
}
}
/**
* 检查路径是否存在
*
* @param map Map
* @param fieldPath 字段路径点号分隔
* @return true 如果路径存在否则返回 false
*/
public static boolean hasPath(Map<String, Object> map, String fieldPath) {
if (fieldPath == null || fieldPath.isEmpty() || map == null) {
return false;
}
try {
JsonNode rootNode = OBJECT_MAPPER.valueToTree(map);
String jsonPointerPath = "/" + fieldPath.replace(".", "/");
JsonNode resultNode = rootNode.at(jsonPointerPath);
return !resultNode.isMissingNode();
} catch (Exception e) {
return false;
}
}
// ==================== Flowable 集成方法 ====================
/**
* Flowable 执行上下文中解析变量表达式
* 支持嵌套路径例如approval.userIdssid_xxx.outputs.status
*
* <p>示例
* <pre>
* // 流程变量中有approval = {required: true, userIds: "admin"}
* Object value = NestedMapUtils.getValueFromExecution(execution, "approval.userIds");
* // 返回: "admin"
* </pre>
*
* @param execution Flowable 执行上下文
* @param variablePath 变量路径点号分隔approval.userIds
* @return 变量值如果路径不存在则返回 null
*/
public static Object getValueFromExecution(DelegateExecution execution, String variablePath) {
if (execution == null || variablePath == null || variablePath.isEmpty()) {
return null;
}
// 1. 切分路径获取顶层变量名
String[] parts = variablePath.split("\\.", 2);
String topLevelVar = parts[0];
// 2. execution 中获取顶层变量
Object value = execution.getVariable(topLevelVar);
// 3. 如果没有嵌套路径直接返回
if (parts.length == 1) {
return value;
}
// 4. 如果有嵌套路径继续解析
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
return getValue(map, parts[1]);
} else {
log.debug("变量 {} 不是 Map 类型,无法解析嵌套路径: {}", topLevelVar, variablePath);
return null;
}
}
/**
* Flowable 任务上下文中解析变量表达式
* 支持嵌套路径例如approval.userIds
*
* <p>示例
* <pre>
* // 流程变量中有approval = {required: true, userIds: "admin"}
* Object value = NestedMapUtils.getValueFromTask(task, "approval.userIds");
* // 返回: "admin"
* </pre>
*
* @param task Flowable 任务上下文
* @param variablePath 变量路径点号分隔approval.userIds
* @return 变量值如果路径不存在则返回 null
*/
public static Object getValueFromTask(DelegateTask task, String variablePath) {
if (task == null || variablePath == null || variablePath.isEmpty()) {
return null;
}
// 1. 切分路径获取顶层变量名
String[] parts = variablePath.split("\\.", 2);
String topLevelVar = parts[0];
// 2. task 中获取顶层变量
Object value = task.getVariable(topLevelVar);
// 3. 如果没有嵌套路径直接返回
if (parts.length == 1) {
return value;
}
// 4. 如果有嵌套路径继续解析
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
return getValue(map, parts[1]);
} else {
log.debug("变量 {} 不是 Map 类型,无法解析嵌套路径: {}", topLevelVar, variablePath);
return null;
}
}
/**
* Flowable 上下文中解析变量表达式支持 ${...} 格式
* 自动去除表达式包装支持嵌套路径
*
* <p>示例
* <pre>
* Object value1 = NestedMapUtils.resolveExpression(task, "${approval.userIds}");
* Object value2 = NestedMapUtils.resolveExpression(task, "approval.userIds");
* // 两种格式都能正确解析
* </pre>
*
* @param task Flowable 任务上下文
* @param expression 表达式可以带 ${} 也可以不带
* @return 变量值如果路径不存在则返回 null
*/
public static Object resolveExpression(DelegateTask task, String expression) {
if (expression == null || expression.isEmpty()) {
return null;
}
// 去除 ${} 包装
String variablePath = expression;
if (expression.startsWith("${") && expression.endsWith("}")) {
variablePath = expression.substring(2, expression.length() - 1);
}
return getValueFromTask(task, variablePath);
}
/**
* Flowable 执行上下文中解析变量表达式支持 ${...} 格式
*
* @param execution Flowable 执行上下文
* @param expression 表达式可以带 ${} 也可以不带
* @return 变量值如果路径不存在则返回 null
*/
public static Object resolveExpression(DelegateExecution execution, String expression) {
if (expression == null || expression.isEmpty()) {
return null;
}
// 去除 ${} 包装
String variablePath = expression;
if (expression.startsWith("${") && expression.endsWith("}")) {
variablePath = expression.substring(2, expression.length() - 1);
}
return getValueFromExecution(execution, variablePath);
}
}

View File

@ -112,8 +112,9 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
String approverVariableExpression = input.getApproverVariable();
log.info("解析审批人变量表达式: {}", approverVariableExpression);
// 解析表达式 ${approval.userIds}
Object approverValue = resolveExpression(task, approverVariableExpression);
// 使用工具类直接解析表达式支持 ${approval.userIds} approval.userIds
Object approverValue = com.qqchen.deploy.backend.framework.utils.NestedMapUtils
.resolveExpression(task, approverVariableExpression);
if (approverValue != null) {
log.info("解析得到审批人: {}", approverValue);
@ -148,48 +149,6 @@ public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping,
}
}
/**
* 解析流程变量表达式
* 支持简单表达式 ${approval.userIds}和直接变量名 approval.userIds
*/
private Object resolveExpression(DelegateTask task, String expression) {
try {
// 如果是表达式格式${...}手动解析
if (expression.startsWith("${") && expression.endsWith("}")) {
// 去掉 ${ }
String variablePath = expression.substring(2, expression.length() - 1);
return resolveVariablePath(task, variablePath);
}
// 如果不是表达式格式直接解析路径 approval.userIds
return resolveVariablePath(task, expression);
} catch (Exception e) {
log.error("解析表达式失败: {}", expression, e);
return null;
}
}
/**
* 手动解析变量路径 approval.userIds
*/
private Object resolveVariablePath(DelegateTask task, String path) {
String[] parts = path.split("\\.");
Object value = task.getVariable(parts[0]);
// 如果是嵌套路径继续解析
for (int i = 1; i < parts.length && value != null; i++) {
if (value instanceof Map) {
value = ((Map<?, ?>) value).get(parts[i]);
} else {
log.warn("变量 {} 不是 Map 类型,无法继续解析路径", parts[i - 1]);
return null;
}
}
return value;
}
/**
* 将审批相关信息保存为任务变量
* 这样前端查询任务时可以获取到这些信息

View File

@ -16,10 +16,6 @@ import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_FAILURE;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_SUCCESS;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME;
/**
* 任务委派者基类
@ -57,26 +53,24 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
String currentNodeId = null;
NodeContext<I, O> nodeContext = new NodeContext<>();
Map<String, Object> configsMap = null;
I inputMappingObj = null;
try {
// 1. 获取节点ID
currentNodeId = getFieldValue(nodeId, execution);
log.info("Executing node: {}", currentNodeId);
// 2. 清除前一个节点的状态
clearPreviousNodeStatus(execution);
// 2. 解析配置通用Map
configsMap = parseJsonField(configs, execution);
// 3. 解析并转换InputMapping强类型
inputMappingObj = parseAndConvertInputMapping(execution);
// 3. 解析配置通用Map
Map<String, Object> configsMap = parseJsonField(configs, execution);
// 4. 解析并转换InputMapping强类型
I inputMappingObj = parseAndConvertInputMapping(execution);
// 5. 执行具体的业务逻辑返回强类型输出
// 4. 执行具体的业务逻辑返回强类型输出
O outputsObj = executeInternal(execution, configsMap, inputMappingObj);
// 6. 使用 NodeContext 保存节点数据
// 5. 使用 NodeContext 保存节点数据
nodeContext.setConfigs(configsMap);
nodeContext.setInputMapping(inputMappingObj);
nodeContext.setOutputs(outputsObj);
@ -84,16 +78,17 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
execution.setVariable(currentNodeId, nodeContext.toMap(objectMapper));
log.info("Stored NodeContext for: {}", currentNodeId);
// 7. 设置节点执行状态为成功
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_SUCCESS);
} catch (Exception e) {
// 即使失败也保存完整的 NodeContext包含 configs inputMapping
BaseNodeOutputs failureNodeOutputs = new BaseNodeOutputs();
failureNodeOutputs.setStatus(NodeExecutionStatusEnum.FAILURE);
failureNodeOutputs.setMessage(e.getMessage());
nodeContext.setConfigs(configsMap); // 保存已解析的配置
nodeContext.setInputMapping(inputMappingObj); // 保存已解析的输入
nodeContext.setOutputs((O) failureNodeOutputs);
execution.setVariable(currentNodeId, nodeContext.toMap(objectMapper));
// 设置失败状态
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_FAILURE);
log.error("Task execution failed", e);
}
}
@ -183,7 +178,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
*/
protected Map<String, Object> resolveExpressions(Map<String, Object> inputMap, DelegateExecution execution) {
Map<String, Object> resolvedMap = new HashMap<>();
log.debug("开始解析 inputMap: {}", inputMap);
for (Map.Entry<String, Object> entry : inputMap.entrySet()) {
@ -193,8 +188,9 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
String strValue = (String) value;
if (strValue.contains("${")) {
try {
// 使用简单的变量替换${sid_xxx.fieldName} -> 从execution中获取
String resolvedValue = resolveVariableExpression(strValue, execution);
// 使用工具类直接解析表达式
Object resolvedValue = com.qqchen.deploy.backend.framework.utils.NestedMapUtils
.resolveExpression(execution, strValue);
log.debug("解析表达式: {} = {} -> {}", entry.getKey(), strValue, resolvedValue);
resolvedMap.put(entry.getKey(), resolvedValue);
} catch (Exception e) {
@ -208,69 +204,11 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
resolvedMap.put(entry.getKey(), value);
}
}
log.debug("解析后 resolvedMap: {}", resolvedMap);
return resolvedMap;
}
/**
* 解析变量表达式
* 例如${sid_xxx.buildNumber} -> execution.getVariable("sid_xxx") 中获取 buildNumber 字段
*/
private String resolveVariableExpression(String expression, DelegateExecution execution) {
String result = expression;
// 匹配 ${xxx.yyy} ${xxx} 格式
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\$\\{([^}]+)\\}");
java.util.regex.Matcher matcher = pattern.matcher(expression);
while (matcher.find()) {
String varExpression = matcher.group(1); // 例如sid_xxx.buildNumber
Object value = resolveVariable(varExpression, execution);
if (value != null) {
// 替换 ${xxx.yyy} 为实际值
result = result.replace("${" + varExpression + "}", value.toString());
}
}
return result;
}
/**
* 从execution中解析变量
* 支持sid_xxx.buildNumber 格式
*/
private Object resolveVariable(String varExpression, DelegateExecution execution) {
String[] parts = varExpression.split("\\.", 2);
log.debug("解析变量表达式: {}, parts: {}", varExpression, java.util.Arrays.toString(parts));
if (parts.length == 1) {
// 直接变量${xxx}
Object result = execution.getVariable(parts[0]);
log.debug("直接变量 {} = {}", parts[0], result);
return result;
} else {
// 对象属性${sid_xxx.buildNumber} ${jenkins.systemId}
String varName = parts[0];
String fieldName = parts[1];
Object varValue = execution.getVariable(varName);
log.debug("获取变量 {} = {}, 类型: {}", varName, varValue, varValue != null ? varValue.getClass().getName() : "null");
if (varValue instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) varValue;
Object fieldValue = map.get(fieldName);
log.debug("从 Map 中获取字段 {} = {}", fieldName, fieldValue);
return fieldValue;
}
}
return null;
}
protected String getFieldValue(Expression expression, DelegateExecution execution) {
if (expression == null) return null;
Object value = expression.getValue(execution);
@ -291,13 +229,4 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
}
}
protected void setExecutionStatus(DelegateExecution execution, String status) {
execution.setVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME, status);
}
private void clearPreviousNodeStatus(DelegateExecution execution) {
execution.removeVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME);
log.debug("Cleared previous node status for node: {}", execution.getCurrentActivityId());
}
}

View File

@ -19,5 +19,8 @@ public class BaseNodeOutputs {
* 用于流程条件判断所有节点统一使用此字段
*/
private NodeExecutionStatusEnum status;
private String message;
}

View File

@ -22,9 +22,6 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_FAILURE;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_SUCCESS;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME;
/**
* 审批任务结束监听器
@ -79,13 +76,8 @@ public class ApprovalExecutionListener implements ExecutionListener {
log.info("Stored approval outputs for node: {}, result: {}", nodeId, outputs.getApprovalResult());
// 6. 设置节点执行状态为成功 GlobalNodeExecutionListener 使用
execution.setVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME, WORKFLOW_NODE_EXECUTION_STATE_SUCCESS);
} catch (Exception e) {
log.error("Failed to build approval outputs for node: {}", nodeId, e);
execution.setVariable(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME,
WORKFLOW_NODE_EXECUTION_STATE_FAILURE);
throw new RuntimeException("Failed to build approval outputs: " + nodeId, e);
}
}

View File

@ -19,8 +19,6 @@ import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import static com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants.WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME;
@Slf4j
@Component("globalNodeExecutionListener")
public class GlobalNodeExecutionListener implements ExecutionListener {
@ -50,24 +48,19 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
String variablesJson = null;
String errorMessage = null;
String nodeExecutionStatus = Optional.ofNullable(execution.getVariables())
.map(vars -> vars.get(WORKFLOW_PREVIOUS_NODE_EXECUTION_STATE_VARIABLE_NAME))
.map(Object::toString)
.orElse(null);
switch (eventName) {
case ExecutionListener.EVENTNAME_START:
status = WorkflowNodeInstanceStatusEnums.RUNNING;
startTime = now;
break;
case ExecutionListener.EVENTNAME_END:
if (StringUtils.isEmpty(nodeExecutionStatus)) {
status = WorkflowNodeInstanceStatusEnums.COMPLETED;
// 从节点的 outputs.status 读取执行状态
com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum nodeExecutionStatus = getNodeOutputStatus(execution, nodeId);
if (nodeExecutionStatus == com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum.FAILURE) {
status = WorkflowNodeInstanceStatusEnums.FAILED;
} else {
if (WorkFlowConstants.WORKFLOW_NODE_EXECUTION_STATE_SUCCESS.equals(nodeExecutionStatus)) {
status = WorkflowNodeInstanceStatusEnums.COMPLETED;
} else {
status = WorkflowNodeInstanceStatusEnums.FAILED;
}
status = WorkflowNodeInstanceStatusEnums.COMPLETED;
}
// 收集节点执行变量
@ -98,6 +91,37 @@ public class GlobalNodeExecutionListener implements ExecutionListener {
.build());
}
/**
* 从节点的 outputs.status 获取执行状态
*
* @param execution Flowable执行上下文
* @param nodeId 节点ID
* @return 节点执行状态枚举如果没有则返回 null
*/
private com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum getNodeOutputStatus(DelegateExecution execution, String nodeId) {
try {
// 使用工具类从节点变量中获取 outputs.status
Object statusObj = com.qqchen.deploy.backend.framework.utils.NestedMapUtils
.getValueFromExecution(execution, nodeId + ".outputs.status");
if (statusObj != null) {
// 如果已经是枚举类型直接返回
if (statusObj instanceof com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum) {
return (com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum) statusObj;
}
// 如果是字符串转换为枚举
try {
return com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum.valueOf(statusObj.toString());
} catch (IllegalArgumentException e) {
log.warn("无法将 {} 转换为 NodeExecutionStatusEnum", statusObj);
}
}
} catch (Exception e) {
log.debug("无法获取节点 {} 的 outputs.status: {}", nodeId, e.getMessage());
}
return null;
}
/**
* 收集节点执行变量
*

View File

@ -11,16 +11,21 @@ import java.util.Map;
* 节点执行上下文强类型
* 统一封装节点的配置输入映射和输出结果
*
* <p>数据结构
* <p>数据结构规范
* <pre>
* {
* "configs": { ... }, // 节点配置来自 BPMN
* "inputMapping": { ... }, // 输入映射运行时数据强类型
* "outputs": { ... }, // 输出结果完整嵌套强类型用于问题调查
* // ... outputs 的字段也会平铺到顶层 BPMN 表达式访问
* "outputs": { ... } // 输出结果强类型所有输出数据只存储在此
* }
* </pre>
*
* <p>BPMN 表达式访问规范
* <pre>
* ${nodeId.outputs.字段名}
* 示例${sid_xxx.outputs.approvalResult == 'APPROVED'}
* </pre>
*
* @param <I> InputMapping 类型
* @param <O> Outputs 类型
* @author qqchen
@ -46,9 +51,14 @@ public class NodeContext<I, O> {
/**
* 转换为 Map用于 Flowable 变量存储
*
* 存储格式
* - configsinputMappingoutputs 保留嵌套结构
* - outputs 的字段同时平铺到顶层 BPMN 表达式访问
* 存储格式严格遵守
* {
* "configs": { ... }, // 节点配置
* "inputMapping": { ... }, // 节点输入
* "outputs": { ... } // 节点输出所有输出数据只存储在此
* }
*
* BPMN 表达式访问${nodeId.outputs.字段名}
*/
public Map<String, Object> toMap(ObjectMapper objectMapper) {
Map<String, Object> map = new HashMap<>();
@ -67,11 +77,8 @@ public class NodeContext<I, O> {
Map<String, Object> outputsMap = objectMapper.convertValue(outputs,
new TypeReference<Map<String, Object>>() {});
// 保存完整的嵌套 outputs用于问题调查
// 只保存嵌套的 outputs保持数据结构规范
map.put("outputs", outputsMap);
// 同时将 outputs 字段平铺到顶层 BPMN 表达式访问
map.putAll(outputsMap);
}
return map;