diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java index 9a41cbd2..c88ab0f3 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/DeployRecordStatusEnums.java @@ -16,6 +16,11 @@ public enum DeployRecordStatusEnums { */ CREATED("CREATED", "已创建"), + /** + * 待审批 + */ + PENDING_APPROVAL("PENDING_APPROVAL", "待审批"), + /** * 运行中 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java index b97da6c1..3aa682c7 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java @@ -47,9 +47,7 @@ public class DeployRecordServiceImpl extends BaseServiceImpl示例: + *
+     * Map map = Map.of("outputs", Map.of("status", "SUCCESS"));
+     * Object value = NestedMapUtils.getValue(map, "outputs.status");
+     * // 返回: "SUCCESS"
+     * 
+ * + * @param map 源 Map + * @param fieldPath 字段路径(点号分隔,如:outputs.status) + * @return 字段值,如果路径不存在则返回 null + */ + public static Object getValue(Map 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 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 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 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 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 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 返回值类型 + * @return 指定类型的值,如果路径不存在或转换失败则返回 null + */ + @SuppressWarnings("unchecked") + public static T getValue(Map map, String fieldPath, Class 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 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.userIds、sid_xxx.outputs.status + * + *

示例: + *

+     * // 流程变量中有:approval = {required: true, userIds: "admin"}
+     * Object value = NestedMapUtils.getValueFromExecution(execution, "approval.userIds");
+     * // 返回: "admin"
+     * 
+ * + * @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 map = (Map) value; + return getValue(map, parts[1]); + } else { + log.debug("变量 {} 不是 Map 类型,无法解析嵌套路径: {}", topLevelVar, variablePath); + return null; + } + } + + /** + * 从 Flowable 任务上下文中解析变量表达式 + * 支持嵌套路径,例如:approval.userIds + * + *

示例: + *

+     * // 流程变量中有:approval = {required: true, userIds: "admin"}
+     * Object value = NestedMapUtils.getValueFromTask(task, "approval.userIds");
+     * // 返回: "admin"
+     * 
+ * + * @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 map = (Map) value; + return getValue(map, parts[1]); + } else { + log.debug("变量 {} 不是 Map 类型,无法解析嵌套路径: {}", topLevelVar, variablePath); + return null; + } + } + + /** + * 从 Flowable 上下文中解析变量表达式(支持 ${...} 格式) + * 自动去除表达式包装,支持嵌套路径 + * + *

示例: + *

+     * Object value1 = NestedMapUtils.resolveExpression(task, "${approval.userIds}");
+     * Object value2 = NestedMapUtils.resolveExpression(task, "approval.userIds");
+     * // 两种格式都能正确解析
+     * 
+ * + * @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); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java index 2b569647..f6ea7a3c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java @@ -112,8 +112,9 @@ public class ApprovalTaskListener extends BaseTaskListener) value).get(parts[i]); - } else { - log.warn("变量 {} 不是 Map 类型,无法继续解析路径", parts[i - 1]); - return null; - } - } - - return value; - } - /** * 将审批相关信息保存为任务变量 * 这样前端查询任务时可以获取到这些信息 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 a4253192..c0b10439 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 @@ -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 implements JavaDelegate { String currentNodeId = null; NodeContext nodeContext = new NodeContext<>(); + Map 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 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 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 implements JavaDelegate { */ protected Map resolveExpressions(Map inputMap, DelegateExecution execution) { Map resolvedMap = new HashMap<>(); - + log.debug("开始解析 inputMap: {}", inputMap); for (Map.Entry entry : inputMap.entrySet()) { @@ -193,8 +188,9 @@ public abstract class BaseNodeDelegate 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 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 map = (Map) 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 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()); - } - } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/BaseNodeOutputs.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/BaseNodeOutputs.java index ea9e76e5..59036698 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/BaseNodeOutputs.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/outputs/BaseNodeOutputs.java @@ -19,5 +19,8 @@ public class BaseNodeOutputs { * 用于流程条件判断,所有节点统一使用此字段 */ private NodeExecutionStatusEnum status; + + private String message; + } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java index 9bccebbc..f66589aa 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java @@ -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); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java index 3ac9e022..e9f7d9f6 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java @@ -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; + } + /** * 收集节点执行变量 * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/model/NodeContext.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/model/NodeContext.java index 6d985888..cc21d38f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/model/NodeContext.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/model/NodeContext.java @@ -11,16 +11,21 @@ import java.util.Map; * 节点执行上下文(强类型) * 统一封装节点的配置、输入映射和输出结果 * - *

数据结构: + *

数据结构(规范): *

  * {
  *   "configs": { ... },      // 节点配置(来自 BPMN)
  *   "inputMapping": { ... }, // 输入映射(运行时数据,强类型)
- *   "outputs": { ... },      // 输出结果(完整嵌套,强类型,用于问题调查)
- *   // ... outputs 的字段也会平铺到顶层(供 BPMN 表达式访问)
+ *   "outputs": { ... }       // 输出结果(强类型,所有输出数据只存储在此)
  * }
  * 
* + *

BPMN 表达式访问规范: + *

+ * ${nodeId.outputs.字段名}
+ * 示例:${sid_xxx.outputs.approvalResult == 'APPROVED'}
+ * 
+ * * @param InputMapping 类型 * @param Outputs 类型 * @author qqchen @@ -46,9 +51,14 @@ public class NodeContext { /** * 转换为 Map(用于 Flowable 变量存储) * - * 存储格式: - * - configs、inputMapping、outputs 保留嵌套结构 - * - outputs 的字段同时平铺到顶层(供 BPMN 表达式访问) + * 存储格式(严格遵守): + * { + * "configs": { ... }, // 节点配置 + * "inputMapping": { ... }, // 节点输入 + * "outputs": { ... } // 节点输出(所有输出数据只存储在此) + * } + * + * BPMN 表达式访问:${nodeId.outputs.字段名} */ public Map toMap(ObjectMapper objectMapper) { Map map = new HashMap<>(); @@ -67,11 +77,8 @@ public class NodeContext { Map outputsMap = objectMapper.convertValue(outputs, new TypeReference>() {}); - // 保存完整的嵌套 outputs(用于问题调查) + // 只保存嵌套的 outputs(保持数据结构规范) map.put("outputs", outputsMap); - - // 同时将 outputs 字段平铺到顶层(供 BPMN 表达式访问) - map.putAll(outputsMap); } return map;