From 048d53855c260e6d220066bc331f595d276afeed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=9A=E8=BE=B0=E5=85=88=E7=94=9F?= Date: Sun, 8 Dec 2024 20:05:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=84=9A=E6=9C=AC=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=B7=A5=E5=8E=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/framework/enums/ResponseCode.java | 1 + .../backend/workflow/dto/NodeConfigDTO.java | 38 +++++ .../backend/workflow/dto/NodeDefinition.java | 38 +++++ .../backend/workflow/dto/NodeInstanceDTO.java | 15 +- .../engine/DefaultWorkflowEngine.java | 146 +++++++++++------ .../gateway/ConditionalGatewayConfig.java | 2 +- .../gateway/ExclusiveGatewayConfig.java | 2 +- .../gateway/InclusiveGatewayConfig.java | 2 +- .../gateway/ParallelGatewayConfig.java | 4 +- .../executor/node/ScriptNodeExecutor.java | 31 ++-- .../node/config/ScriptNodeConfig.java | 3 +- .../node/script/command/ScriptCommand.java | 16 ++ .../script/command/ScriptLanguageSupport.java | 17 ++ .../command/impl/PythonScriptCommand.java | 31 ++++ .../command/impl/ShellScriptCommand.java | 31 ++++ .../registry/ScriptCommandRegistry.java | 57 +++++++ .../parser/WorkflowDefinitionParser.java | 58 +++++-- .../engine/transition/TransitionRule.java | 6 +- .../transition/TransitionRuleEngine.java | 4 +- .../backend/workflow/entity/NodeConfig.java | 13 +- .../workflow/entity/TransitionConfig.java | 34 ++-- .../workflow/enums/ScriptLanguageEnum.java | 31 ++++ .../repository/INodeConfigRepository.java | 26 ++++ .../ITransitionConfigRepository.java | 26 ++++ .../db/migration/V1.0.0__init_schema.sql | 23 +++ .../db/migration/V1.0.1__init_data.sql | 147 +++++++++++++----- .../src/main/resources/messages.properties | 5 +- .../main/resources/messages_en_US.properties | 2 + .../main/resources/messages_zh_CN.properties | 3 +- 29 files changed, 661 insertions(+), 151 deletions(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeConfigDTO.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinition.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptCommand.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptLanguageSupport.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/PythonScriptCommand.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/ShellScriptCommand.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/registry/ScriptCommandRegistry.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/ScriptLanguageEnum.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeConfigRepository.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/ITransitionConfigRepository.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 1f96cfa6..64888d17 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 @@ -113,6 +113,7 @@ public enum ResponseCode { WORKFLOW_NODE_TIMEOUT(2724, "workflow.node.timeout"), WORKFLOW_NODE_CONFIG_ERROR(2725, "workflow.node.config.error"), WORKFLOW_EXECUTION_ERROR(2726, "workflow.execution.error"), + WORKFLOW_CONFIG_ERROR(2727, "workflow.config.error"), WORKFLOW_VARIABLE_NOT_FOUND(2730, "workflow.variable.not.found"), WORKFLOW_VARIABLE_TYPE_INVALID(2731, "workflow.variable.type.invalid"), WORKFLOW_PERMISSION_DENIED(2740, "workflow.permission.denied"), diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeConfigDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeConfigDTO.java new file mode 100644 index 00000000..2125b4d2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeConfigDTO.java @@ -0,0 +1,38 @@ +package com.qqchen.deploy.backend.workflow.dto; + +import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; +import lombok.Data; + +import java.util.Map; + +/** + * 节点配置DTO + * 用于前端传递节点配置信息 + */ +@Data +public class NodeConfigDTO { + /** + * 节点ID + */ + private String id; + + /** + * 节点类型 + */ + private NodeTypeEnum type; + + /** + * 节点名称 + */ + private String name; + + /** + * 节点配置 + */ + private Map config; + + /** + * 节点描述 + */ + private String description; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinition.java new file mode 100644 index 00000000..cde44ce8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinition.java @@ -0,0 +1,38 @@ +package com.qqchen.deploy.backend.workflow.dto; + +import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 节点定义DTO,用于解析工作流定义中的节点配置 + */ +@Data +public class NodeDefinition { + /** + * 节点ID + */ + private String id; + + /** + * 节点类型 + */ + private NodeTypeEnum type; + + /** + * 节点名称 + */ + private String name; + + /** + * 下一个节点的ID列表 + */ + private List next; + + /** + * 节点配置,不同类型的节点有不同的配置 + */ + private Map config; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeInstanceDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeInstanceDTO.java index c03df50c..395494e8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeInstanceDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeInstanceDTO.java @@ -10,16 +10,6 @@ import lombok.EqualsAndHashCode; import java.time.LocalDateTime; -import com.qqchen.deploy.backend.framework.dto.BaseDTO; -import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum; -import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - /** * 节点实例DTO */ @@ -72,6 +62,11 @@ public class NodeInstanceDTO extends BaseDTO { */ private String config; + /** + * 节点描述 + */ + private String description; + /** * 输入参数(JSON) */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java index 5adc9f51..f4602ccf 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java @@ -17,7 +17,9 @@ import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum; import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum; import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnum; import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnum; +import com.qqchen.deploy.backend.workflow.repository.INodeConfigRepository; import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository; +import com.qqchen.deploy.backend.workflow.repository.ITransitionConfigRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository; import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations; @@ -43,6 +45,12 @@ public class DefaultWorkflowEngine implements WorkflowEngine { @Resource private INodeInstanceRepository nodeInstanceRepository; + @Resource + private INodeConfigRepository nodeConfigRepository; + + @Resource + private ITransitionConfigRepository transitionConfigRepository; + @Resource private Map nodeExecutors; @@ -82,19 +90,51 @@ public class DefaultWorkflowEngine implements WorkflowEngine { variableOperations.setVariables(instance.getId(), variables); } - // 5. 创建并执行开始节点 - NodeInstance startNode = new NodeInstance(); - startNode.setWorkflowInstance(instance); - startNode.setNodeId("start"); - startNode.setNodeType(NodeTypeEnum.START); - startNode.setName("开始节点"); - startNode.setStatus(NodeStatusEnum.PENDING); - startNode.setCreateTime(LocalDateTime.now()); - nodeInstanceRepository.save(startNode); + // 5. 解析并保存节点和流转配置 + try { + // 清除旧的配置(如果存在) + nodeConfigRepository.deleteByWorkflowDefinitionId(definition.getId()); + transitionConfigRepository.deleteByWorkflowDefinitionId(definition.getId()); - // 6. 执行开始节点 - executeNode(startNode.getId()); - return instance; + // 解析并保存新的配置 + List nodeConfigs = workflowDefinitionParser.parseNodeConfig(definition.getNodeConfig()); + List transitions = workflowDefinitionParser.parseTransitionConfig(definition.getTransitionConfig()); + + // 保存节点配置 + for (NodeConfig config : nodeConfigs) { + config.setWorkflowDefinitionId(definition.getId()); + nodeConfigRepository.save(config); + } + + // 保存流转配置 + for (TransitionConfig config : transitions) { + config.setWorkflowDefinitionId(definition.getId()); + transitionConfigRepository.save(config); + } + + // 6. 查找并创建开始节点 + NodeConfig startNodeConfig = nodeConfigs.stream() + .filter(n -> n.getType() == NodeTypeEnum.START) + .findFirst() + .orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, "Start node not found")); + + NodeInstance startNode = new NodeInstance(); + startNode.setWorkflowInstance(instance); + startNode.setNodeId(startNodeConfig.getNodeId()); + startNode.setNodeType(startNodeConfig.getType()); + startNode.setName(startNodeConfig.getName()); + startNode.setConfig(objectMapper.writeValueAsString(startNodeConfig.getConfig())); + startNode.setStatus(NodeStatusEnum.PENDING); + startNode.setCreateTime(LocalDateTime.now()); + nodeInstanceRepository.save(startNode); + + // 7. 执行开始节点 + executeNode(startNode.getId()); + + return instance; + } catch (JsonProcessingException e) { + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_ERROR, e); + } } @Override @@ -120,47 +160,65 @@ public class DefaultWorkflowEngine implements WorkflowEngine { .variableOperations(variableOperations) .build(); - // 执行节点 - executor.execute(nodeInstance, context); + try { + // 执行节点 + executor.execute(nodeInstance, context); - // 3. 更新节点状态 - nodeInstance.setStatus(NodeStatusEnum.COMPLETED); - nodeInstance.setEndTime(LocalDateTime.now()); - nodeInstanceRepository.save(nodeInstance); + // 更新节点状态 + nodeInstance.setStatus(NodeStatusEnum.COMPLETED); + nodeInstance.setEndTime(LocalDateTime.now()); + nodeInstanceRepository.save(nodeInstance); - // 4. 获取并执行后续节点 - WorkflowDefinition definition = instance.getWorkflowDefinition(); - List transitions = workflowDefinitionParser.parseTransitionConfig(definition.getTransitionConfig()); - List nodeConfigs = workflowDefinitionParser.parseNodeConfig(definition.getNodeConfig()); + // 从数据库获取流转配置 + List transitions = transitionConfigRepository + .findByWorkflowDefinitionId(instance.getWorkflowDefinition().getId()); - // 获取当前节点的后续节点 - List nextNodeIds = transitions.stream() - .filter(t -> t.getSourceNodeId().equals(nodeInstance.getNodeId())) - .map(TransitionConfig::getTargetNodeId) - .toList(); + // 获取当前节点的后续节点 + List nextNodeIds = transitions.stream() + .filter(t -> t.getFrom().equals(nodeInstance.getNodeId())) + .map(TransitionConfig::getTo) + .toList(); - // 创建并执行后续节点 - for (String nextNodeId : nextNodeIds) { - NodeConfig nodeConfig = nodeConfigs.stream() - .filter(n -> n.getNodeId().equals(nextNodeId)) - .findFirst() - .orElse(null); + // 获取节点配置 + List nodeConfigs = nodeConfigRepository + .findByWorkflowDefinitionId(instance.getWorkflowDefinition().getId()); - if (nodeConfig == null) { - log.error("Node configuration not found for node: {}", nextNodeId); - continue; + // 创建并执行后续节点 + for (String nextNodeId : nextNodeIds) { + NodeConfig nodeConfig = nodeConfigs.stream() + .filter(n -> n.getNodeId().equals(nextNodeId)) + .findFirst() + .orElse(null); + + if (nodeConfig == null) { + log.error("Node configuration not found for node: {}", nextNodeId); + continue; + } + + createAndExecuteNextNode(instance, nextNodeId, nodeConfig); } - createAndExecuteNextNode(instance, nextNodeId, nodeConfig); - } + // 检查是否所有节点都已完成 + List uncompletedNodes = nodeInstanceRepository + .findByWorkflowInstanceAndStatusNot(instance, NodeStatusEnum.COMPLETED); - // 5. 检查是否所有节点都已完成 - List uncompletedNodes = nodeInstanceRepository.findByWorkflowInstanceAndStatusNot( - instance, NodeStatusEnum.COMPLETED); + if (uncompletedNodes.isEmpty()) { + instance.complete(); + workflowInstanceRepository.save(instance); + } + } catch (Exception e) { + // 更新节点状态为失败 + nodeInstance.setStatus(NodeStatusEnum.FAILED); + nodeInstance.setError(e.getMessage()); + nodeInstance.setEndTime(LocalDateTime.now()); + nodeInstanceRepository.save(nodeInstance); - if (uncompletedNodes.isEmpty()) { - instance.complete(); + // 更新工作流实例状态为失败 + instance.setStatus(WorkflowInstanceStatusEnum.FAILED); + instance.setError(e.getMessage()); workflowInstanceRepository.save(instance); + + throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e); } } @@ -178,7 +236,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine { // 递归执行后续节点 executeNode(nextNode.getId()); } catch (JsonProcessingException e) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e); + throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID, e); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ConditionalGatewayConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ConditionalGatewayConfig.java index e26ca0bb..fb6fdfe5 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ConditionalGatewayConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ConditionalGatewayConfig.java @@ -36,7 +36,7 @@ public abstract class ConditionalGatewayConfig extends GatewayConfig { /** * 目标节点ID */ - private String targetNodeId; + private String to; /** * 分支描述 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ExclusiveGatewayConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ExclusiveGatewayConfig.java index d1c92929..107f4e63 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ExclusiveGatewayConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ExclusiveGatewayConfig.java @@ -23,7 +23,7 @@ public class ExclusiveGatewayConfig extends ConditionalGatewayConfig { // 返回第一个满足条件的分支的targetNodeId for (ConditionalBranch branch : getBranches()) { if (evaluateCondition(branch.getCondition(), context)) { - return Collections.singletonList(branch.getTargetNodeId()); + return Collections.singletonList(branch.getTo()); } } return Collections.singletonList(getDefaultNodeId()); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/InclusiveGatewayConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/InclusiveGatewayConfig.java index 1f70365b..9e6913bc 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/InclusiveGatewayConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/InclusiveGatewayConfig.java @@ -28,7 +28,7 @@ public class InclusiveGatewayConfig extends ConditionalGatewayConfig { // 返回所有满足条件的分支的targetNodeId List nextNodeIds = getBranches().stream() .filter(branch -> evaluateCondition(branch.getCondition(), context)) - .map(ConditionalBranch::getTargetNodeId) + .map(ConditionalBranch::getTo) .collect(Collectors.toList()); // 如果没有满足条件的分支,使用默认分支 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ParallelGatewayConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ParallelGatewayConfig.java index c552f71f..70b22e76 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ParallelGatewayConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/gateway/ParallelGatewayConfig.java @@ -33,7 +33,7 @@ public class ParallelGatewayConfig extends GatewayConfig { /** * 目标节点ID */ - private String targetNodeId; + private String to; /** * 分支描述 @@ -45,7 +45,7 @@ public class ParallelGatewayConfig extends GatewayConfig { public List getNextNodeIds(WorkflowContextOperations context) { // 返回所有分支的targetNodeId return branches.stream() - .map(ParallelBranch::getTargetNodeId) + .map(ParallelBranch::getTo) .collect(Collectors.toList()); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/ScriptNodeExecutor.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/ScriptNodeExecutor.java index cee632e5..652aea38 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/ScriptNodeExecutor.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/ScriptNodeExecutor.java @@ -21,6 +21,9 @@ import java.util.List; import java.util.Map; import java.util.concurrent.*; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptCommand; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.registry.ScriptCommandRegistry; + /** * 脚本节点执行器 * 支持多种脚本语言(Python, Shell, JavaScript等) @@ -35,6 +38,9 @@ public class ScriptNodeExecutor extends AbstractNodeExecutor { @Resource private WorkflowVariableOperations variableOperations; + @Resource + private ScriptCommandRegistry commandRegistry; + private final ExecutorService executorService = Executors.newCachedThreadPool(); @Override @@ -51,7 +57,7 @@ public class ScriptNodeExecutor extends AbstractNodeExecutor { throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Script content cannot be empty"); } // 验证脚本语言 - if (scriptConfig.getLanguage() == null || scriptConfig.getLanguage().trim().isEmpty()) { + if (scriptConfig.getLanguage() == null) { throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Script language must be specified"); } // 验证其他参数 @@ -108,26 +114,11 @@ public class ScriptNodeExecutor extends AbstractNodeExecutor { private void executeScript(ScriptNodeConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) throws Exception { ProcessBuilder processBuilder = new ProcessBuilder(); - // 根据脚本语言设置命令 - List command = new ArrayList<>(); - switch (config.getLanguage().toLowerCase()) { - case "python": - command.add(config.getInterpreter() != null ? config.getInterpreter() : "python"); - command.add("-c"); - command.add(config.getScript()); - break; - case "shell": - command.add("sh"); - command.add("-c"); - command.add(config.getScript()); - break; - // TODO: 添加其他语言支持 - default: - throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, - "Unsupported script language: " + config.getLanguage()); - } + // 获取命令实现并构建命令 + ScriptCommand command = commandRegistry.getCommand(config.getLanguage()); + List commandList = command.buildCommand(config); - processBuilder.command(command); + processBuilder.command(commandList); // 设置工作目录 if (config.getWorkingDirectory() != null && !config.getWorkingDirectory().trim().isEmpty()) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/config/ScriptNodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/config/ScriptNodeConfig.java index 2d2a2fe0..1a750008 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/config/ScriptNodeConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/config/ScriptNodeConfig.java @@ -1,5 +1,6 @@ package com.qqchen.deploy.backend.workflow.engine.executor.node.config; +import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum; import lombok.Data; import lombok.EqualsAndHashCode; @@ -20,7 +21,7 @@ public class ScriptNodeConfig extends NodeConfig { /** * 脚本语言:python/shell/javascript */ - private String language; + private ScriptLanguageEnum language; /** * 解释器路径(可选) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptCommand.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptCommand.java new file mode 100644 index 00000000..3fd608d4 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptCommand.java @@ -0,0 +1,16 @@ +package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command; + +import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeConfig; +import java.util.List; + +/** + * 脚本命令构建接口 + */ +public interface ScriptCommand { + /** + * 构建脚本执行命令 + * @param config 脚本配置 + * @return 命令行参数列表 + */ + List buildCommand(ScriptNodeConfig config); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptLanguageSupport.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptLanguageSupport.java new file mode 100644 index 00000000..2c3a3f0b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/ScriptLanguageSupport.java @@ -0,0 +1,17 @@ +package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command; + +import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum; +import java.lang.annotation.*; + +/** + * 脚本语言支持注解 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ScriptLanguageSupport { + /** + * 支持的脚本语言类型 + */ + ScriptLanguageEnum value(); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/PythonScriptCommand.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/PythonScriptCommand.java new file mode 100644 index 00000000..b570bf6b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/PythonScriptCommand.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.impl; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException; +import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeConfig; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptCommand; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptLanguageSupport; +import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum; +import org.springframework.stereotype.Component; +import java.util.Arrays; +import java.util.List; + +/** + * Python脚本命令构建实现 + */ +@Component +@ScriptLanguageSupport(ScriptLanguageEnum.PYTHON) +public class PythonScriptCommand implements ScriptCommand { + @Override + public List buildCommand(ScriptNodeConfig config) { + if (config.getInterpreter() == null || config.getInterpreter().trim().isEmpty()) { + throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, + "Python interpreter path must be specified"); + } + return Arrays.asList( + config.getInterpreter(), + "-c", + config.getScript() + ); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/ShellScriptCommand.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/ShellScriptCommand.java new file mode 100644 index 00000000..b2f281ef --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/command/impl/ShellScriptCommand.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.impl; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException; +import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeConfig; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptCommand; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptLanguageSupport; +import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum; +import org.springframework.stereotype.Component; +import java.util.Arrays; +import java.util.List; + +/** + * Shell脚本命令构建实现 + */ +@Component +@ScriptLanguageSupport(ScriptLanguageEnum.SHELL) +public class ShellScriptCommand implements ScriptCommand { + @Override + public List buildCommand(ScriptNodeConfig config) { + if (config.getInterpreter() == null || config.getInterpreter().trim().isEmpty()) { + throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, + "Shell interpreter path must be specified"); + } + return Arrays.asList( + config.getInterpreter(), + "-c", + config.getScript() + ); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/registry/ScriptCommandRegistry.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/registry/ScriptCommandRegistry.java new file mode 100644 index 00000000..f68fc23f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/node/script/registry/ScriptCommandRegistry.java @@ -0,0 +1,57 @@ +package com.qqchen.deploy.backend.workflow.engine.executor.node.script.registry; + +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptCommand; +import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptLanguageSupport; +import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 脚本命令注册表 + */ +@Component +public class ScriptCommandRegistry { + private final Map commands = new ConcurrentHashMap<>(); + + @Autowired + private List scriptCommands; + + @PostConstruct + public void init() { + scriptCommands.forEach(command -> { + ScriptLanguageSupport support = command.getClass().getAnnotation(ScriptLanguageSupport.class); + if (support != null) { + registerCommand(support.value(), command); + } + }); + } + + /** + * 注册脚本命令实现 + * @param language 脚本语言 + * @param command 命令实现 + */ + public void registerCommand(ScriptLanguageEnum language, ScriptCommand command) { + commands.put(language, command); + } + + /** + * 获取脚本命令实现 + * @param language 脚本语言 + * @return 命令实现 + */ + public ScriptCommand getCommand(ScriptLanguageEnum language) { + ScriptCommand command = commands.get(language); + if (command == null) { + throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, + "No command implementation found for language: " + language.getDescription()); + } + return command; + } +} 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 273fd5f0..fd7bdbeb 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 @@ -15,6 +15,7 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * 工作流定义解析器 @@ -36,14 +37,30 @@ public class WorkflowDefinitionParser { try { log.debug("Parsing node config: {}", nodeConfig); JsonNode rootNode = objectMapper.readTree(nodeConfig); - JsonNode nodesNode = rootNode.get("nodeConfig"); + JsonNode nodesNode = rootNode.get("nodes"); if (nodesNode == null || !nodesNode.isArray()) { throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID); } List nodes = new ArrayList<>(); for (JsonNode node : nodesNode) { - NodeConfig config = objectMapper.treeToValue(node, NodeConfig.class); + NodeConfig config = new NodeConfig(); + // 将前端的id映射到nodeId + config.setNodeId(node.get("id").asText()); + config.setName(node.get("name").asText()); + config.setType(NodeTypeEnum.valueOf(node.get("type").asText())); + + // 解析节点配置 + if (node.has("config")) { + config.setConfig(objectMapper.convertValue(node.get("config"), + new TypeReference>() {})); + } + + // 可选字段 + if (node.has("description")) { + config.setDescription(node.get("description").asText()); + } + nodes.add(config); log.debug("Parsed node: id={}, type={}", config.getNodeId(), config.getType()); } @@ -64,18 +81,32 @@ public class WorkflowDefinitionParser { try { log.debug("Parsing transition config: {}", transitionConfig); JsonNode rootNode = objectMapper.readTree(transitionConfig); - JsonNode transitionsNode = rootNode.get("transitionConfig"); + JsonNode transitionsNode = rootNode.get("transitions"); if (transitionsNode == null || !transitionsNode.isArray()) { throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID); } List transitions = new ArrayList<>(); for (JsonNode node : transitionsNode) { - TransitionConfig config = objectMapper.treeToValue(node, TransitionConfig.class); + TransitionConfig config = new TransitionConfig(); + config.setFrom(node.get("from").asText()); + config.setTo(node.get("to").asText()); + + // 可选字段 + if (node.has("condition")) { + config.setCondition(node.get("condition").asText()); + } + if (node.has("description")) { + config.setDescription(node.get("description").asText()); + } + if (node.has("priority")) { + config.setPriority(node.get("priority").asInt()); + } + transitions.add(config); log.debug("Parsed transition: {} -> {}, priority={}", - config.getSourceNodeId(), - config.getTargetNodeId(), + config.getFrom(), + config.getTo(), config.getPriority()); } return transitions; @@ -97,6 +128,10 @@ public class WorkflowDefinitionParser { boolean hasEnd = false; for (NodeConfig node : nodes) { if (node.getType() == NodeTypeEnum.START) { + if (hasStart) { + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, + "工作流只能有一个开始节点"); + } hasStart = true; } else if (node.getType() == NodeTypeEnum.END) { hasEnd = true; @@ -104,7 +139,8 @@ public class WorkflowDefinitionParser { } if (!hasStart || !hasEnd) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, "工作流必须包含开始节点和结束节点"); + throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, + "工作流必须包含开始节点和结束节点"); } // 2. 检查流转配置的完整性 @@ -113,10 +149,10 @@ public class WorkflowDefinitionParser { boolean targetExists = false; for (NodeConfig node : nodes) { - if (node.getNodeId().equals(transition.getSourceNodeId())) { + if (node.getNodeId().equals(transition.getFrom())) { sourceExists = true; } - if (node.getNodeId().equals(transition.getTargetNodeId())) { + if (node.getNodeId().equals(transition.getTo())) { targetExists = true; } } @@ -124,8 +160,8 @@ public class WorkflowDefinitionParser { if (!sourceExists || !targetExists) { throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, String.format("流转配置中的节点不存在: %s -> %s", - transition.getSourceNodeId(), - transition.getTargetNodeId())); + transition.getFrom(), + transition.getTo())); } } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRule.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRule.java index eb72e0a4..17135644 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRule.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRule.java @@ -9,12 +9,12 @@ public class TransitionRule { /** * 源节点ID */ - private String sourceNodeId; + private String from; /** * 目标节点ID */ - private String targetNodeId; + private String to; /** * 条件表达式 @@ -25,4 +25,4 @@ public class TransitionRule { * 优先级 */ private Integer priority; -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRuleEngine.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRuleEngine.java index 2a5ee74f..5682e907 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRuleEngine.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/transition/TransitionRuleEngine.java @@ -35,7 +35,7 @@ public class TransitionRuleEngine { // 过滤当前节点的规则并按优先级排序 List nodeRules = rules.stream() - .filter(rule -> rule.getSourceNodeId().equals(currentNode.getNodeId())) + .filter(rule -> rule.getFrom().equals(currentNode.getNodeId())) .sorted(Comparator.comparing(TransitionRule::getPriority)) .toList(); @@ -43,7 +43,7 @@ public class TransitionRuleEngine { List nextNodeIds = new ArrayList<>(); for (TransitionRule rule : nodeRules) { if (evaluateCondition(rule.getCondition(), context)) { - nextNodeIds.add(rule.getTargetNodeId()); + nextNodeIds.add(rule.getTo()); } } 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 index b35146a8..17c31487 100644 --- 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 @@ -47,6 +47,13 @@ public class NodeConfig extends Entity { @Column(nullable = false) private NodeTypeEnum type; + /** + * 所属工作流定义ID + */ + @NotNull(message = "工作流定义ID不能为空") + @Column(name = "workflow_definition_id", nullable = false) + private Long workflowDefinitionId; + /** * 节点配置,不同类型的节点有不同的配置 * TASK节点: @@ -61,10 +68,10 @@ public class NodeConfig extends Entity { private Map config; /** - * 工作流定义ID + * 节点描述,用于说明节点的用途 */ - @Column(name = "workflow_definition_id", nullable = false) - private Long workflowDefinitionId; + @Column(columnDefinition = "text") + private String description; /** * 检查节点配置是否有效 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 index 0f51bd7d..11946af0 100644 --- 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 @@ -1,5 +1,6 @@ package com.qqchen.deploy.backend.workflow.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.qqchen.deploy.backend.framework.domain.Entity; import jakarta.persistence.Column; import jakarta.persistence.Table; @@ -19,19 +20,17 @@ import lombok.EqualsAndHashCode; public class TransitionConfig extends Entity { /** * 源节点ID - * 流转的起始节点 */ @NotBlank(message = "源节点ID不能为空") - @Column(name = "source_node_id", nullable = false) - private String sourceNodeId; + @Column(name = "`from`", nullable = false) + private String from; /** * 目标节点ID - * 流转的目标节点 */ @NotBlank(message = "目标节点ID不能为空") - @Column(name = "target_node_id", nullable = false) - private String targetNodeId; + @Column(name = "`to`", nullable = false) + private String to; /** * 流转条件,使用SpEL表达式 @@ -41,8 +40,8 @@ public class TransitionConfig extends Entity { * - "${amount > 1000}" * - "${result.code == 200 && result.data != null}" */ - @Column(name = "transition_condition", columnDefinition = "text") - private String transitionCondition; + @Column(name = "`condition`", columnDefinition = "text") + private String condition; /** * 优先级,数字越小优先级越高 @@ -54,24 +53,31 @@ public class TransitionConfig extends Entity { private Integer priority = 0; /** - * 工作流定义ID + * 所属工作流定义ID */ + @NotNull(message = "工作流定义ID不能为空") @Column(name = "workflow_definition_id", nullable = false) private Long workflowDefinitionId; + /** + * 流转描述,用于说明流转的用途 + */ + @Column(columnDefinition = "text") + private String description; + /** * 检查流转配置是否有效 * * @return true if valid, false otherwise */ public boolean isValid() { - if (sourceNodeId == null || sourceNodeId.trim().isEmpty()) { + if (from == null || from.trim().isEmpty()) { return false; } - if (targetNodeId == null || targetNodeId.trim().isEmpty()) { + if (to == null || to.trim().isEmpty()) { return false; } - if (sourceNodeId.equals(targetNodeId)) { + if (from.equals(to)) { return false; // 不允许自循环 } if (workflowDefinitionId == null) { @@ -86,7 +92,7 @@ public class TransitionConfig extends Entity { * @return true if conditional, false otherwise */ public boolean isConditional() { - return transitionCondition != null && !transitionCondition.trim().isEmpty(); + return condition != null && !condition.trim().isEmpty(); } /** @@ -97,4 +103,4 @@ public class TransitionConfig extends Entity { public int getPriorityValue() { return priority != null ? priority : 0; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/ScriptLanguageEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/ScriptLanguageEnum.java new file mode 100644 index 00000000..1fd9bdd4 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/ScriptLanguageEnum.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.workflow.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 脚本语言类型枚举 + */ +@Getter +@RequiredArgsConstructor +public enum ScriptLanguageEnum { + + SHELL("shell", "Shell脚本"), + PYTHON("python", "Python脚本"), + JAVASCRIPT("javascript", "JavaScript脚本"), + GROOVY("groovy", "Groovy脚本"); + + @JsonValue + private final String code; + private final String description; + + public static ScriptLanguageEnum fromCode(String code) { + for (ScriptLanguageEnum language : values()) { + if (language.getCode().equals(code)) { + return language; + } + } + throw new IllegalArgumentException("Unknown script language code: " + code); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeConfigRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeConfigRepository.java new file mode 100644 index 00000000..acbb9774 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeConfigRepository.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.workflow.entity.NodeConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface INodeConfigRepository extends JpaRepository { + + /** + * 根据工作流定义ID查找所有节点配置 + * + * @param workflowDefinitionId 工作流定义ID + * @return 节点配置列表 + */ + List findByWorkflowDefinitionId(Long workflowDefinitionId); + + /** + * 根据工作流定义ID删除所有节点配置 + * + * @param workflowDefinitionId 工作流定义ID + */ + void deleteByWorkflowDefinitionId(Long workflowDefinitionId); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/ITransitionConfigRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/ITransitionConfigRepository.java new file mode 100644 index 00000000..48bb5aac --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/ITransitionConfigRepository.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.workflow.repository; + +import com.qqchen.deploy.backend.workflow.entity.TransitionConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ITransitionConfigRepository extends JpaRepository { + + /** + * 根据工作流定义ID查找所有流转配置 + * + * @param workflowDefinitionId 工作流定义ID + * @return 流转配置列表 + */ + List findByWorkflowDefinitionId(Long workflowDefinitionId); + + /** + * 根据工作流定义ID删除所有流转配置 + * + * @param workflowDefinitionId 工作流定义ID + */ + void deleteByWorkflowDefinitionId(Long workflowDefinitionId); +} diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index 4cc4ab01..37328c24 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -420,12 +420,34 @@ CREATE TABLE wf_node_definition ( name VARCHAR(100) NOT NULL COMMENT '节点名称', type TINYINT NOT NULL COMMENT '节点类型(0:开始节点,1:结束节点,2:任务节点,3:网关节点,4:子流程节点,5:Shell脚本节点,6:审批节点,7:Jenkins任务节点,8:Git操作节点)', config TEXT NULL COMMENT '节点配置(JSON)', + description TEXT NULL COMMENT '节点描述', order_num INT NOT NULL DEFAULT 0 COMMENT '排序号', CONSTRAINT FK_node_definition_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_workflow_definition (id), CONSTRAINT UK_node_definition_workflow_node UNIQUE (workflow_definition_id, node_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点定义表'; +-- 流转配置表 +CREATE TABLE wf_transition_config ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(255) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除(0:未删除,1:已删除)', + update_by VARCHAR(255) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + + workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID', + `from` VARCHAR(100) NOT NULL COMMENT '源节点ID', + `to` VARCHAR(100) NOT NULL COMMENT '目标节点ID', + `condition` TEXT NULL COMMENT '流转条件', + description TEXT NULL COMMENT '流转描述', + priority INT NOT NULL DEFAULT 0 COMMENT '优先级', + + CONSTRAINT FK_transition_config_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_workflow_definition (id), + CONSTRAINT UK_transition_config_workflow_nodes UNIQUE (workflow_definition_id, `from`, `to`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流转配置表'; + -- 工作流实例表 CREATE TABLE wf_workflow_instance ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', @@ -464,6 +486,7 @@ CREATE TABLE wf_node_instance ( start_time DATETIME(6) NULL COMMENT '开始时间', end_time DATETIME(6) NULL COMMENT '结束时间', config TEXT NULL COMMENT '节点配置(JSON)', + description TEXT NULL COMMENT '节点描述', input TEXT NULL COMMENT '输入参数(JSON)', output TEXT NULL COMMENT '输出结果(JSON)', error TEXT NULL COMMENT '错误信息', diff --git a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql index cbdc6e66..c0b38ef6 100644 --- a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql +++ b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql @@ -296,28 +296,67 @@ true, NOW(), 'system', NOW(), 'system', 1, false), true, NOW(), 'system', NOW(), 'system', 1, false), -- 任务节点类型 -(2003, 'SHELL', 'Shell脚本节点', '执行Shell脚本的任务节点', 'TASK', 'code', '#1890ff', +(2003, 'SCRIPT', '脚本执行节点', '执行各种脚本语言的节点', 'TASK', 'code', '#13c2c2', '[{ - "code": "SHELL", - "name": "Shell脚本执行器", - "description": "执行Shell脚本,支持配置超时时间和工作目录", + "code": "SCRIPT", + "name": "脚本执行器", + "description": "支持执行多种脚本语言(Shell、Python、JavaScript等)", + "supportedLanguages": ["shell"], "configSchema": { "type": "object", - "required": ["script"], + "required": ["name", "script", "language"], "properties": { + "name": { + "type": "string", + "title": "节点名称", + "minLength": 1, + "maxLength": 50 + }, + "description": { + "type": "string", + "title": "节点描述", + "maxLength": 200 + }, "script": { "type": "string", "title": "脚本内容", - "format": "shell", - "description": "需要执行的Shell脚本内容" + "format": "script", + "description": "需要执行的脚本内容" }, - "timeout": { - "type": "number", - "title": "超时时间", - "description": "脚本执行的最大时间(秒)", - "minimum": 1, - "maximum": 3600, - "default": 300 + "language": { + "type": "string", + "title": "脚本语言", + "enum": ["shell", "python", "javascript", "groovy"], + "enumNames": ["Shell脚本 (已支持)", "Python脚本 (开发中)", "JavaScript脚本 (开发中)", "Groovy脚本 (开发中)"], + "default": "shell", + "description": "脚本语言类型", + "oneOf": [ + { + "const": "shell", + "title": "Shell脚本" + }, + { + "const": "python", + "title": "Python脚本", + "readOnly": true + }, + { + "const": "javascript", + "title": "JavaScript脚本", + "readOnly": true + }, + { + "const": "groovy", + "title": "Groovy脚本", + "readOnly": true + } + ] + }, + "interpreter": { + "type": "string", + "title": "解释器路径", + "description": "脚本解释器的路径,例如:/bin/bash, /usr/bin/python3", + "default": "/bin/bash" }, "workingDirectory": { "type": "string", @@ -325,30 +364,26 @@ true, NOW(), 'system', NOW(), 'system', 1, false), "description": "脚本执行的工作目录", "default": "/tmp" }, - "retryTimes": { - "type": "number", - "title": "重试次数", - "minimum": 0, - "maximum": 10, - "default": 0 - }, - "retryInterval": { - "type": "number", - "title": "重试间隔(秒)", + "timeout": { + "type": "integer", + "title": "超时时间", + "description": "脚本执行的最大时间(秒)", "minimum": 1, "maximum": 3600, - "default": 60 + "default": 300 }, "environment": { "type": "object", "title": "环境变量", "description": "脚本执行时的环境变量", - "additionalProperties": {"type": "string"} + "additionalProperties": { + "type": "string" + } }, "successExitCode": { - "type": "number", + "type": "integer", "title": "成功退出码", - "description": "脚本执行成功的退出码", + "description": "脚本执行成功时的退出码", "default": 0 } } @@ -356,6 +391,7 @@ true, NOW(), 'system', NOW(), 'system', 1, false), }]', '{ "type": "object", + "required": ["name", "script", "language"], "properties": { "name": { "type": "string", @@ -368,16 +404,57 @@ true, NOW(), 'system', NOW(), 'system', 1, false), "title": "节点描述", "maxLength": 200 }, - "executor": { + "script": { "type": "string", - "title": "执行器", - "enum": ["SHELL"], - "enumNames": ["Shell脚本执行器"] + "title": "脚本内容", + "format": "script", + "description": "需要执行的脚本内容" + }, + "language": { + "type": "string", + "title": "脚本语言", + "enum": ["shell", "python", "javascript", "groovy"], + "enumNames": ["Shell脚本 (已支持)", "Python脚本 (开发中)", "JavaScript脚本 (开发中)", "Groovy脚本 (开发中)"], + "default": "shell", + "description": "脚本语言类型" + }, + "interpreter": { + "type": "string", + "title": "解释器路径", + "description": "脚本解释器的路径,例如:/bin/bash, /usr/bin/python3", + "default": "/bin/bash" + }, + "workingDirectory": { + "type": "string", + "title": "工作目录", + "description": "脚本执行的工作目录", + "default": "/tmp" + }, + "timeout": { + "type": "integer", + "title": "超时时间", + "description": "脚本执行的最大时间(秒)", + "minimum": 1, + "maximum": 3600, + "default": 300 + }, + "environment": { + "type": "object", + "title": "环境变量", + "description": "脚本执行时的环境变量", + "additionalProperties": { + "type": "string" + } + }, + "successExitCode": { + "type": "integer", + "title": "成功退出码", + "description": "脚本执行成功时的退出码", + "default": 0 } - }, - "required": ["name", "executor"] + } }', -'{"name": "Shell脚本", "executor": "SHELL"}', +'{"name": "脚本执行", "language": "shell", "script": "echo \"Hello World\""}', true, NOW(), 'system', NOW(), 'system', 1, false), -- Git节点类型 diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 93d429e5..7e668ff3 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -127,7 +127,6 @@ workflow.node.not.found=\u5DE5\u4F5C\u6D41\u8282\u70B9\u4E0D\u5B58\u5728 workflow.node.type.not.supported=\u4E0D\u652F\u6301\u7684\u8282\u70B9\u7C7B\u578B workflow.node.config.invalid=\u8282\u70B9\u914D\u7F6E\u65E0\u6548 workflow.node.execution.failed=\u8282\u70B9\u6267\u884C\u5931\u8D25 -workflow.node.timeout=\u8282\u70B9\u6267\u884C\u8D85\u65F6 workflow.variable.not.found=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u4E0D\u5B58\u5728 workflow.variable.type.invalid=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u7C7B\u578B\u65E0\u6548 workflow.permission.denied=\u5DE5\u4F5C\u6D41\u6743\u9650\u4E0D\u8DB3 @@ -136,7 +135,7 @@ workflow.approval.rejected=\u5BA1\u6279\u88AB\u62D2\u7EDD workflow.dependency.not.satisfied=\u4F9D\u8D56\u6761\u4EF6\u4E0D\u6EE1\u8DB3 workflow.circular.dependency=\u5B58\u5728\u5FAA\u73AF\u4F9D\u8D56 workflow.schedule.invalid=\u8C03\u5EA6\u914D\u7F6E\u65E0\u6548 -workflow.concurrent.limit.exceeded=\u8D85\u51FA\u5E76\u53D1\u9650\u5236 +workflow.concurrent.limit.exceeded=\u8FC7\u51FA\u5E76\u53D1\u9650\u5236 # Workflow error messages workflow.not.found=\u5DE5\u4F5C\u6D41\u5B9A\u4E49\u4E0D\u5B58\u5728 @@ -240,3 +239,5 @@ workflow.node.executor.not.found=\u672A\u627E\u5230\u5DE5\u4F5C\u6D41\u8282\u70B workflow.variable.serialize.error=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u5E8F\u5217\u5316\u5931\u8D25: {0} workflow.variable.deserialize.error=\u5DE5\u4F5C\u6D41\u53D8\u91CF\u53CD\u5E8F\u5217\u5316\u5931\u8D25: {0} + +workflow.config.error=\u5DE5\u4F5C\u6D41\u914D\u7F6E\u9519\u8BEF: {0} diff --git a/backend/src/main/resources/messages_en_US.properties b/backend/src/main/resources/messages_en_US.properties index b0e896f6..6e20372b 100644 --- a/backend/src/main/resources/messages_en_US.properties +++ b/backend/src/main/resources/messages_en_US.properties @@ -44,6 +44,8 @@ workflow.circular.dependency=Circular dependency detected in workflow workflow.schedule.invalid=Invalid workflow schedule configuration workflow.concurrent.limit.exceeded=Workflow concurrent limit exceeded +workflow.config.error=Workflow configuration error: {0} + # Workflow Node Type Errors (2200-2299) workflow.node.type.not.found=Node type does not exist or has been deleted workflow.node.type.disabled=Node type is disabled and cannot be used diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties index 0fd63599..36949c3c 100644 --- a/backend/src/main/resources/messages_zh_CN.properties +++ b/backend/src/main/resources/messages_zh_CN.properties @@ -131,4 +131,5 @@ workflow.approval.rejected=审批被拒绝 workflow.dependency.not.satisfied=工作流依赖条件未满足 workflow.circular.dependency=工作流存在循环依赖 workflow.schedule.invalid=工作流调度配置无效 -workflow.concurrent.limit.exceeded=工作流并发限制超出 \ No newline at end of file +workflow.concurrent.limit.exceeded=工作流并发限制超出 +workflow.config.error=工作流配置错误:{0} \ No newline at end of file