增加脚本适配工厂

This commit is contained in:
戚辰先生 2024-12-08 20:05:59 +08:00
parent 79edbbcb82
commit 048d53855c
29 changed files with 661 additions and 151 deletions

View File

@ -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"),

View File

@ -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<String, Object> config;
/**
* 节点描述
*/
private String description;
}

View File

@ -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<String> next;
/**
* 节点配置不同类型的节点有不同的配置
*/
private Map<String, Object> config;
}

View File

@ -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)
*/

View File

@ -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<NodeTypeEnum, NodeExecutor> 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<NodeConfig> nodeConfigs = workflowDefinitionParser.parseNodeConfig(definition.getNodeConfig());
List<TransitionConfig> 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<TransitionConfig> transitions = workflowDefinitionParser.parseTransitionConfig(definition.getTransitionConfig());
List<NodeConfig> nodeConfigs = workflowDefinitionParser.parseNodeConfig(definition.getNodeConfig());
// 从数据库获取流转配置
List<TransitionConfig> transitions = transitionConfigRepository
.findByWorkflowDefinitionId(instance.getWorkflowDefinition().getId());
// 获取当前节点的后续节点
List<String> nextNodeIds = transitions.stream()
.filter(t -> t.getSourceNodeId().equals(nodeInstance.getNodeId()))
.map(TransitionConfig::getTargetNodeId)
.toList();
// 获取当前节点的后续节点
List<String> 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<NodeConfig> 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<NodeInstance> uncompletedNodes = nodeInstanceRepository
.findByWorkflowInstanceAndStatusNot(instance, NodeStatusEnum.COMPLETED);
// 5. 检查是否所有节点都已完成
List<NodeInstance> 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);
}
}

View File

@ -36,7 +36,7 @@ public abstract class ConditionalGatewayConfig extends GatewayConfig {
/**
* 目标节点ID
*/
private String targetNodeId;
private String to;
/**
* 分支描述

View File

@ -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());

View File

@ -28,7 +28,7 @@ public class InclusiveGatewayConfig extends ConditionalGatewayConfig {
// 返回所有满足条件的分支的targetNodeId
List<String> nextNodeIds = getBranches().stream()
.filter(branch -> evaluateCondition(branch.getCondition(), context))
.map(ConditionalBranch::getTargetNodeId)
.map(ConditionalBranch::getTo)
.collect(Collectors.toList());
// 如果没有满足条件的分支使用默认分支

View File

@ -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<String> getNextNodeIds(WorkflowContextOperations context) {
// 返回所有分支的targetNodeId
return branches.stream()
.map(ParallelBranch::getTargetNodeId)
.map(ParallelBranch::getTo)
.collect(Collectors.toList());
}
}

View File

@ -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<String> 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<String> commandList = command.buildCommand(config);
processBuilder.command(command);
processBuilder.command(commandList);
// 设置工作目录
if (config.getWorkingDirectory() != null && !config.getWorkingDirectory().trim().isEmpty()) {

View File

@ -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;
/**
* 解释器路径可选

View File

@ -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<String> buildCommand(ScriptNodeConfig config);
}

View File

@ -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();
}

View File

@ -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<String> 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()
);
}
}

View File

@ -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<String> 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()
);
}
}

View File

@ -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<ScriptLanguageEnum, ScriptCommand> commands = new ConcurrentHashMap<>();
@Autowired
private List<ScriptCommand> 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;
}
}

View File

@ -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<NodeConfig> 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<Map<String, Object>>() {}));
}
// 可选字段
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<TransitionConfig> 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()));
}
}
}

View File

@ -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;
}
}

View File

@ -35,7 +35,7 @@ public class TransitionRuleEngine {
// 过滤当前节点的规则并按优先级排序
List<TransitionRule> 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<String> nextNodeIds = new ArrayList<>();
for (TransitionRule rule : nodeRules) {
if (evaluateCondition(rule.getCondition(), context)) {
nextNodeIds.add(rule.getTargetNodeId());
nextNodeIds.add(rule.getTo());
}
}

View File

@ -47,6 +47,13 @@ public class NodeConfig extends Entity<Long> {
@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<Long> {
private Map<String, Object> config;
/**
* 工作流定义ID
* 节点描述用于说明节点的用途
*/
@Column(name = "workflow_definition_id", nullable = false)
private Long workflowDefinitionId;
@Column(columnDefinition = "text")
private String description;
/**
* 检查节点配置是否有效

View File

@ -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<Long> {
/**
* 源节点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<Long> {
* - "${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<Long> {
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<Long> {
* @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<Long> {
public int getPriorityValue() {
return priority != null ? priority : 0;
}
}
}

View File

@ -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);
}
}

View File

@ -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<NodeConfig, Long> {
/**
* 根据工作流定义ID查找所有节点配置
*
* @param workflowDefinitionId 工作流定义ID
* @return 节点配置列表
*/
List<NodeConfig> findByWorkflowDefinitionId(Long workflowDefinitionId);
/**
* 根据工作流定义ID删除所有节点配置
*
* @param workflowDefinitionId 工作流定义ID
*/
void deleteByWorkflowDefinitionId(Long workflowDefinitionId);
}

View File

@ -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<TransitionConfig, Long> {
/**
* 根据工作流定义ID查找所有流转配置
*
* @param workflowDefinitionId 工作流定义ID
* @return 流转配置列表
*/
List<TransitionConfig> findByWorkflowDefinitionId(Long workflowDefinitionId);
/**
* 根据工作流定义ID删除所有流转配置
*
* @param workflowDefinitionId 工作流定义ID
*/
void deleteByWorkflowDefinitionId(Long workflowDefinitionId);
}

View File

@ -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子流程节点5Shell脚本节点6审批节点7Jenkins任务节点8Git操作节点',
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 '错误信息',

View File

@ -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节点类型

View File

@ -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}

View File

@ -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

View File

@ -131,4 +131,5 @@ workflow.approval.rejected=审批被拒绝
workflow.dependency.not.satisfied=工作流依赖条件未满足
workflow.circular.dependency=工作流存在循环依赖
workflow.schedule.invalid=工作流调度配置无效
workflow.concurrent.limit.exceeded=工作流并发限制超出
workflow.concurrent.limit.exceeded=工作流并发限制超出
workflow.config.error=工作流配置错误:{0}