From c486a6edaa1018b29905248aa67648aef5f7f0bd Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 4 Dec 2024 18:22:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E6=AD=A3=E5=B8=B8=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/framework/enums/ResponseCode.java | 5 +- .../workflow/engine/model/NodeConfig.java | 1 - .../engine/model/TransitionConfig.java | 1 - .../parser/WorkflowDefinitionParser.java | 25 ++-- .../backend/workflow/entity/NodeConfig.java | 119 ++++++++++++++++++ .../workflow/entity/TransitionConfig.java | 100 +++++++++++++++ .../src/main/resources/messages.properties | 37 +++++- 7 files changed, 273 insertions(+), 15 deletions(-) delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/NodeConfig.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/TransitionConfig.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeConfig.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index 547bbe7d..5fce0eff 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -120,7 +120,10 @@ public enum ResponseCode { WORKFLOW_DEPENDENCY_NOT_SATISFIED(2750, "workflow.dependency.not.satisfied"), WORKFLOW_CIRCULAR_DEPENDENCY(2751, "workflow.circular.dependency"), WORKFLOW_SCHEDULE_INVALID(2752, "workflow.schedule.invalid"), - WORKFLOW_CONCURRENT_LIMIT_EXCEEDED(2753, "workflow.concurrent.limit.exceeded"); + WORKFLOW_CONCURRENT_LIMIT_EXCEEDED(2753, "workflow.concurrent.limit.exceeded"), + WORKFLOW_CONFIG_INVALID(2754, "workflow.config.invalid"), + WORKFLOW_TRANSITION_INVALID(2755, "workflow.transition.invalid"), + WORKFLOW_CONDITION_INVALID(2756, "workflow.condition.invalid"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/NodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/NodeConfig.java deleted file mode 100644 index 0519ecba..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/NodeConfig.java +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/TransitionConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/TransitionConfig.java deleted file mode 100644 index 0519ecba..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/model/TransitionConfig.java +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/parser/WorkflowDefinitionParser.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/parser/WorkflowDefinitionParser.java index 9c51ea5e..46eeffa0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/parser/WorkflowDefinitionParser.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/parser/WorkflowDefinitionParser.java @@ -1,11 +1,14 @@ package com.qqchen.deploy.backend.workflow.engine.parser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException; -import com.qqchen.deploy.backend.workflow.engine.node.NodeConfig; +import com.qqchen.deploy.backend.workflow.entity.NodeConfig; +import com.qqchen.deploy.backend.workflow.entity.TransitionConfig; +import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -42,7 +45,7 @@ public class WorkflowDefinitionParser { for (JsonNode node : nodesNode) { NodeConfig config = objectMapper.treeToValue(node, NodeConfig.class); nodes.add(config); - log.debug("Parsed node: id={}, type={}", config.getId(), config.getType()); + log.debug("Parsed node: id={}, type={}", config.getNodeId(), config.getType()); } return nodes; } catch (JsonProcessingException e) { @@ -75,7 +78,7 @@ public class WorkflowDefinitionParser { return transitions; } catch (JsonProcessingException e) { log.error("Failed to parse transition config: {}", e.getMessage(), e); - throw new WorkflowEngineException(ResponseCode.WORKFLOW_TRANSITION_CONFIG_INVALID, e); + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, e); } } @@ -97,7 +100,7 @@ public class WorkflowDefinitionParser { } if (!hasStart || !hasEnd) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID, + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, "Workflow must have both START and END nodes"); } @@ -107,16 +110,16 @@ public class WorkflowDefinitionParser { boolean targetExists = false; for (NodeConfig node : nodes) { - if (node.getId().equals(transition.getSourceNodeId())) { + if (node.getNodeId().equals(transition.getSourceNodeId())) { sourceExists = true; } - if (node.getId().equals(transition.getTargetNodeId())) { + if (node.getNodeId().equals(transition.getTargetNodeId())) { targetExists = true; } } if (!sourceExists || !targetExists) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_TRANSITION_CONFIG_INVALID, + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, String.format("Invalid node reference in transition: %s -> %s", transition.getSourceNodeId(), transition.getTargetNodeId())); } @@ -132,17 +135,17 @@ public class WorkflowDefinitionParser { boolean hasOutgoing = false; for (TransitionConfig transition : transitions) { - if (transition.getTargetNodeId().equals(node.getId())) { + if (transition.getTargetNodeId().equals(node.getNodeId())) { hasIncoming = true; } - if (transition.getSourceNodeId().equals(node.getId())) { + if (transition.getSourceNodeId().equals(node.getNodeId())) { hasOutgoing = true; } } if (!hasIncoming || !hasOutgoing) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID, - String.format("Isolated node found: %s", node.getId())); + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, + String.format("Isolated node found: %s", node.getNodeId())); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeConfig.java new file mode 100644 index 00000000..b35146a8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeConfig.java @@ -0,0 +1,119 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.Map; + +/** + * 节点配置 + * 用于定义工作流中的节点,包括节点的基本信息和特定配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "wf_node_config") +public class NodeConfig extends Entity { + /** + * 节点ID,在同一个工作流中必须唯一 + */ + @NotBlank(message = "节点ID不能为空") + @Column(nullable = false) + private String nodeId; + + /** + * 节点名称,用于显示 + */ + @NotBlank(message = "节点名称不能为空") + @Column(nullable = false) + private String name; + + /** + * 节点类型,决定了节点的行为 + * START: 开始节点,每个工作流必须有且只有一个 + * END: 结束节点,每个工作流必须至少有一个 + * TASK: 任务节点,执行具体的任务 + * GATEWAY: 网关节点,控制流程的分支和合并 + */ + @NotNull(message = "节点类型不能为空") + @Column(nullable = false) + private NodeTypeEnum type; + + /** + * 节点配置,不同类型的节点有不同的配置 + * TASK节点: + * - type: SHELL/HTTP/JAVA + * - config: 具体的任务配置 + * GATEWAY节点: + * - type: EXCLUSIVE/PARALLEL/INCLUSIVE + * - conditions: 分支条件配置 + */ + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "json") + private Map config; + + /** + * 工作流定义ID + */ + @Column(name = "workflow_definition_id", nullable = false) + private Long workflowDefinitionId; + + /** + * 检查节点配置是否有效 + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + if (nodeId == null || nodeId.trim().isEmpty()) { + return false; + } + if (name == null || name.trim().isEmpty()) { + return false; + } + if (type == null) { + return false; + } + + // 检查特定类型节点的配置 + if (type == NodeTypeEnum.TASK && (config == null || !config.containsKey("type"))) { + return false; + } + if (type == NodeTypeEnum.GATEWAY && (config == null || !config.containsKey("type"))) { + return false; + } + + return true; + } + + /** + * 获取任务类型(仅对TASK类型节点有效) + * + * @return 任务类型 + */ + public String getTaskType() { + if (type != NodeTypeEnum.TASK || config == null) { + return null; + } + return (String) config.get("type"); + } + + /** + * 获取网关类型(仅对GATEWAY类型节点有效) + * + * @return 网关类型 + */ + public String getGatewayType() { + if (type != NodeTypeEnum.GATEWAY || config == null) { + return null; + } + return (String) config.get("type"); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java new file mode 100644 index 00000000..8c9970f9 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java @@ -0,0 +1,100 @@ +package com.qqchen.deploy.backend.workflow.entity; + +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 流转配置 + * 用于定义工作流中节点之间的连接关系和流转条件 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "wf_transition_config") +public class TransitionConfig extends Entity { + /** + * 源节点ID + * 流转的起始节点 + */ + @NotBlank(message = "源节点ID不能为空") + @Column(name = "source_node_id", nullable = false) + private String sourceNodeId; + + /** + * 目标节点ID + * 流转的目标节点 + */ + @NotBlank(message = "目标节点ID不能为空") + @Column(name = "target_node_id", nullable = false) + private String targetNodeId; + + /** + * 流转条件,使用SpEL表达式 + * 为空表示无条件流转 + * 示例: + * - "${status == 'SUCCESS'}" + * - "${amount > 1000}" + * - "${result.code == 200 && result.data != null}" + */ + @Column(columnDefinition = "text") + private String condition; + + /** + * 优先级,数字越小优先级越高 + * 用于控制多个出向流转的执行顺序 + * 默认为0 + */ + @NotNull(message = "优先级不能为空") + @Column(nullable = false) + private Integer priority = 0; + + /** + * 工作流定义ID + */ + @Column(name = "workflow_definition_id", nullable = false) + private Long workflowDefinitionId; + + /** + * 检查流转配置是否有效 + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + if (sourceNodeId == null || sourceNodeId.trim().isEmpty()) { + return false; + } + if (targetNodeId == null || targetNodeId.trim().isEmpty()) { + return false; + } + if (sourceNodeId.equals(targetNodeId)) { + return false; // 不允许自循环 + } + if (workflowDefinitionId == null) { + return false; + } + return true; + } + + /** + * 是否是条件流转 + * + * @return true if conditional, false otherwise + */ + public boolean isConditional() { + return condition != null && !condition.trim().isEmpty(); + } + + /** + * 获取优先级,如果未设置则返回默认值0 + * + * @return 优先级值 + */ + public int getPriorityValue() { + return priority != null ? priority : 0; + } +} \ No newline at end of file diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 4616a745..719a3e53 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -148,4 +148,39 @@ workflow.node.config.error=工作流节点配置错误 workflow.execution.error=工作流执行错误 workflow.not.draft=只有草稿状态的工作流定义可以发布 workflow.not.published=只有已发布状态的工作流定义可以禁用 -workflow.not.disabled=只有已禁用状态的工作流定义可以启用 \ No newline at end of file +workflow.not.disabled=只有已禁用状态的工作流定义可以启用 + +# System level messages (1xxx) +success=操作成功 +system.error=系统错误 +param.error=参数错误 +unauthorized=未授权 +forbidden=禁止访问 +not.found=资源不存在 +method.not.allowed=方法不允许 +conflict=资源冲突 +too.many.requests=请求过于频繁 +internal.server.error=内部服务器错误 + +# Business level messages (2xxx) +# Common business messages (2000-2099) +business.error=业务错误 +data.not.found=数据不存在 +data.already.exists=数据已存在 +data.validation.failed=数据验证失败 +operation.not.allowed=操作不允许 + +# Workflow related messages (2100-2199) +workflow.not.found=工作流不存在 +workflow.already.exists=工作流已存在 +workflow.not.published=工作流未发布 +workflow.config.invalid=工作流配置无效 +workflow.node.not.found=工作流节点不存在 +workflow.node.execution.failed=工作流节点执行失败 +workflow.instance.not.found=工作流实例不存在 +workflow.instance.not.running=工作流实例未运行 +workflow.variable.not.found=工作流变量不存在 +workflow.log.not.found=工作流日志不存在 +workflow.transition.invalid=工作流流转配置无效 +workflow.node.type.not.supported=不支持的节点类型 +workflow.condition.invalid=工作流条件配置无效 \ No newline at end of file