删除掉所有没用的工作流相关的类,重新开发。
This commit is contained in:
parent
1d518aa4d3
commit
216839dd59
@ -1,21 +1,15 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.api;
|
package com.qqchen.deploy.backend.workflow.api;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
|
||||||
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.NodeInstanceDTO;
|
import com.qqchen.deploy.backend.workflow.dto.NodeInstanceDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.WorkflowEngine;
|
|
||||||
import com.qqchen.deploy.backend.workflow.query.NodeInstanceQuery;
|
import com.qqchen.deploy.backend.workflow.query.NodeInstanceQuery;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@ -23,30 +17,9 @@ import java.util.Map;
|
|||||||
@Tag(name = "节点实例管理", description = "节点实例管理相关接口")
|
@Tag(name = "节点实例管理", description = "节点实例管理相关接口")
|
||||||
public class NodeInstanceApiController extends BaseController<NodeInstance, NodeInstanceDTO, Long, NodeInstanceQuery> {
|
public class NodeInstanceApiController extends BaseController<NodeInstance, NodeInstanceDTO, Long, NodeInstanceQuery> {
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowEngine workflowEngine;
|
|
||||||
|
|
||||||
@Operation(summary = "完成节点")
|
|
||||||
@PostMapping("/{id}/complete")
|
|
||||||
public Response<Void> completeNode(
|
|
||||||
@Parameter(description = "节点实例ID", required = true) @PathVariable Long id,
|
|
||||||
@Parameter(description = "输出变量") @RequestBody(required = false) Map<String, Object> variables
|
|
||||||
) {
|
|
||||||
workflowEngine.completeNode(id, variables);
|
|
||||||
return Response.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "重试节点")
|
|
||||||
@PostMapping("/{id}/retry")
|
|
||||||
public Response<Void> retryNode(
|
|
||||||
@Parameter(description = "节点实例ID", required = true) @PathVariable Long id
|
|
||||||
) {
|
|
||||||
workflowEngine.executeNode(id);
|
|
||||||
return Response.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void exportData(HttpServletResponse response, List<NodeInstanceDTO> data) {
|
protected void exportData(HttpServletResponse response, List<NodeInstanceDTO> data) {
|
||||||
// TODO: 实现导出功能
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,14 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.api;
|
package com.qqchen.deploy.backend.workflow.api;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
|
||||||
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.NodeTypeDTO;
|
import com.qqchen.deploy.backend.workflow.dto.NodeTypeDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.query.NodeTypeQuery;
|
import com.qqchen.deploy.backend.workflow.dto.query.NodeTypeQuery;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.definition.TaskExecutorDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeType;
|
import com.qqchen.deploy.backend.workflow.entity.NodeType;
|
||||||
import com.qqchen.deploy.backend.workflow.service.INodeTypeService;
|
import com.qqchen.deploy.backend.workflow.service.INodeTypeService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@ -32,15 +26,6 @@ public class NodeTypeApiController extends BaseController<NodeType, NodeTypeDTO,
|
|||||||
@Resource
|
@Resource
|
||||||
private INodeTypeService nodeTypeService;
|
private INodeTypeService nodeTypeService;
|
||||||
|
|
||||||
@Operation(summary = "获取指定节点类型支持的执行器列表")
|
|
||||||
@GetMapping("/{type}/executors")
|
|
||||||
public Response<List<TaskExecutorDefinition>> getExecutors(
|
|
||||||
@Parameter(description = "节点类型", required = true) @PathVariable String type
|
|
||||||
) {
|
|
||||||
log.debug("获取节点类型[{}]支持的执行器列表", type);
|
|
||||||
return Response.success(nodeTypeService.getExecutors(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void exportData(HttpServletResponse response, List<NodeTypeDTO> data) {
|
protected void exportData(HttpServletResponse response, List<NodeTypeDTO> data) {
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,9 @@ package com.qqchen.deploy.backend.workflow.api;
|
|||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
import com.qqchen.deploy.backend.framework.controller.BaseController;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.request.WorkflowStartRequest;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.WorkflowEngine;
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowEngineService;
|
||||||
import com.qqchen.deploy.backend.workflow.query.WorkflowInstanceQuery;
|
import com.qqchen.deploy.backend.workflow.query.WorkflowInstanceQuery;
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
|
||||||
import com.qqchen.deploy.backend.workflow.converter.WorkflowInstanceConverter;
|
import com.qqchen.deploy.backend.workflow.converter.WorkflowInstanceConverter;
|
||||||
@ -27,7 +28,7 @@ import java.util.Map;
|
|||||||
public class WorkflowInstanceApiController extends BaseController<WorkflowInstance, WorkflowInstanceDTO, Long, WorkflowInstanceQuery> {
|
public class WorkflowInstanceApiController extends BaseController<WorkflowInstance, WorkflowInstanceDTO, Long, WorkflowInstanceQuery> {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowEngine workflowEngine;
|
private IWorkflowEngineService workflowEngine;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IWorkflowInstanceService workflowInstanceService;
|
private IWorkflowInstanceService workflowInstanceService;
|
||||||
@ -37,42 +38,10 @@ public class WorkflowInstanceApiController extends BaseController<WorkflowInstan
|
|||||||
|
|
||||||
@Operation(summary = "启动工作流实例")
|
@Operation(summary = "启动工作流实例")
|
||||||
@PostMapping("/start")
|
@PostMapping("/start")
|
||||||
public Response<WorkflowInstanceDTO> startWorkflow(@RequestBody StartWorkflowRequest request) {
|
public Response<WorkflowInstanceDTO> startWorkflow(@RequestBody WorkflowStartRequest request) {
|
||||||
WorkflowInstance instance = workflowEngine.startWorkflow(
|
return Response.success(converter.toDto(workflowEngine.startWorkflow(request)));
|
||||||
request.getWorkflowCode(),
|
|
||||||
request.getBusinessKey(),
|
|
||||||
request.getVariables()
|
|
||||||
);
|
|
||||||
return Response.success(converter.toDto(instance));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "终止工作流实例")
|
|
||||||
@PostMapping("/{id}/terminate")
|
|
||||||
public Response<Void> terminateWorkflow(
|
|
||||||
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long id,
|
|
||||||
@Parameter(description = "终止原因") @RequestParam(required = false) String reason
|
|
||||||
) {
|
|
||||||
workflowEngine.terminateWorkflow(id, reason);
|
|
||||||
return Response.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "暂停工作流实例")
|
|
||||||
@PostMapping("/{id}/pause")
|
|
||||||
public Response<Void> pauseWorkflow(
|
|
||||||
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long id
|
|
||||||
) {
|
|
||||||
workflowEngine.pauseWorkflow(id);
|
|
||||||
return Response.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "恢复工作流实例")
|
|
||||||
@PostMapping("/{id}/resume")
|
|
||||||
public Response<Void> resumeWorkflow(
|
|
||||||
@Parameter(description = "工作流实例ID", required = true) @PathVariable Long id
|
|
||||||
) {
|
|
||||||
workflowEngine.resumeWorkflow(id);
|
|
||||||
return Response.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exportData(HttpServletResponse response, List<WorkflowInstanceDTO> data) {
|
public void exportData(HttpServletResponse response, List<WorkflowInstanceDTO> data) {
|
||||||
@ -80,16 +49,4 @@ public class WorkflowInstanceApiController extends BaseController<WorkflowInstan
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class StartWorkflowRequest {
|
|
||||||
|
|
||||||
@Parameter(description = "工作流编码", required = true)
|
|
||||||
private String workflowCode;
|
|
||||||
|
|
||||||
@Parameter(description = "业务标识", required = true)
|
|
||||||
private String businessKey;
|
|
||||||
|
|
||||||
@Parameter(description = "工作流变量")
|
|
||||||
private Map<String, Object> variables;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.converter;
|
package com.qqchen.deploy.backend.workflow.converter;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.definition.TaskExecutorDefinition;
|
import com.qqchen.deploy.backend.workflow.engine.definition.NodeExecutorDefinition;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ public interface JsonConverter {
|
|||||||
* @param json JSON字符串
|
* @param json JSON字符串
|
||||||
* @return 执行器定义列表
|
* @return 执行器定义列表
|
||||||
*/
|
*/
|
||||||
List<TaskExecutorDefinition> toExecutorList(String json);
|
List<NodeExecutorDefinition> toExecutorList(String json);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将执行器定义列表转换为JSON字符串
|
* 将执行器定义列表转换为JSON字符串
|
||||||
@ -24,5 +24,5 @@ public interface JsonConverter {
|
|||||||
* @param executors 执行器定义列表
|
* @param executors 执行器定义列表
|
||||||
* @return JSON字符串
|
* @return JSON字符串
|
||||||
*/
|
*/
|
||||||
String fromExecutorList(List<TaskExecutorDefinition> executors);
|
String fromExecutorList(List<NodeExecutorDefinition> executors);
|
||||||
}
|
}
|
||||||
@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
|||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.framework.exception.SystemException;
|
import com.qqchen.deploy.backend.framework.exception.SystemException;
|
||||||
import com.qqchen.deploy.backend.workflow.converter.JsonConverter;
|
import com.qqchen.deploy.backend.workflow.converter.JsonConverter;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.definition.TaskExecutorDefinition;
|
import com.qqchen.deploy.backend.workflow.engine.definition.NodeExecutorDefinition;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -27,7 +27,7 @@ public class DefaultJsonConverter implements JsonConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TaskExecutorDefinition> toExecutorList(String json) {
|
public List<NodeExecutorDefinition> toExecutorList(String json) {
|
||||||
if (json == null || json.isEmpty()) {
|
if (json == null || json.isEmpty()) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
@ -38,14 +38,14 @@ public class DefaultJsonConverter implements JsonConverter {
|
|||||||
throw new SystemException(ResponseCode.WORKFLOW_CONFIG_INVALID, new Object[]{"Executors JSON must be an array"}, null);
|
throw new SystemException(ResponseCode.WORKFLOW_CONFIG_INVALID, new Object[]{"Executors JSON must be an array"}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TaskExecutorDefinition> result = new ArrayList<>();
|
List<NodeExecutorDefinition> result = new ArrayList<>();
|
||||||
for (JsonNode node : rootNode) {
|
for (JsonNode node : rootNode) {
|
||||||
// 将configSchema转换为字符串
|
// 将configSchema转换为字符串
|
||||||
if (node.has("configSchema")) {
|
if (node.has("configSchema")) {
|
||||||
((ObjectNode) node).put("configSchema", node.get("configSchema").toString());
|
((ObjectNode) node).put("configSchema", node.get("configSchema").toString());
|
||||||
}
|
}
|
||||||
// 将处理后的节点转换为TaskExecutorDefinition对象
|
// 将处理后的节点转换为TaskExecutorDefinition对象
|
||||||
result.add(objectMapper.treeToValue(node, TaskExecutorDefinition.class));
|
result.add(objectMapper.treeToValue(node, NodeExecutorDefinition.class));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
@ -54,7 +54,7 @@ public class DefaultJsonConverter implements JsonConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String fromExecutorList(List<TaskExecutorDefinition> executors) {
|
public String fromExecutorList(List<NodeExecutorDefinition> executors) {
|
||||||
if (executors == null || executors.isEmpty()) {
|
if (executors == null || executors.isEmpty()) {
|
||||||
return "[]";
|
return "[]";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.dto;
|
package com.qqchen.deploy.backend.workflow.dto;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.definition.TaskExecutorDefinition;
|
import com.qqchen.deploy.backend.workflow.engine.definition.NodeExecutorDefinition;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnum;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@ -60,7 +60,7 @@ public class NodeTypeDTO extends BaseDTO {
|
|||||||
* 3. 每种执行器都有自己的配置模式
|
* 3. 每种执行器都有自己的配置模式
|
||||||
*/
|
*/
|
||||||
@Valid
|
@Valid
|
||||||
private List<TaskExecutorDefinition> executors;
|
private List<NodeExecutorDefinition> executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点配置模式(JSON)
|
* 节点配置模式(JSON)
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WorkflowStartRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "工作流编码")
|
||||||
|
private String workflowCode;
|
||||||
|
|
||||||
|
@NotBlank(message = "业务标识")
|
||||||
|
private String businessKey;
|
||||||
|
|
||||||
|
@NotBlank(message = "环境变量")
|
||||||
|
private Map<String, Object> variables;
|
||||||
|
}
|
||||||
@ -1,347 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.DefaultWorkflowContext;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.NodeExecutor;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.parser.WorkflowDefinitionParser;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeConfig;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.TransitionConfig;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
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;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class DefaultWorkflowEngine implements WorkflowEngine {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private IWorkflowInstanceRepository workflowInstanceRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private INodeInstanceRepository nodeInstanceRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private INodeConfigRepository nodeConfigRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ITransitionConfigRepository transitionConfigRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private Map<NodeTypeEnum, NodeExecutor> nodeExecutors;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowDefinitionParser workflowDefinitionParser;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public WorkflowInstance startWorkflow(String workflowCode, String businessKey, Map<String, Object> variables) {
|
|
||||||
// 1. 获取工作流定义
|
|
||||||
WorkflowDefinition definition = workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode);
|
|
||||||
if (definition == null) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查工作流定义状态
|
|
||||||
if (definition.getStatus() != WorkflowDefinitionStatusEnum.PUBLISHED) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_PUBLISHED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 创建工作流实例
|
|
||||||
WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
instance.setWorkflowDefinition(definition);
|
|
||||||
instance.setBusinessKey(businessKey);
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
instance.setCreateTime(LocalDateTime.now());
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
|
|
||||||
// 4. 初始化工作流变量
|
|
||||||
if (variables != null && !variables.isEmpty()) {
|
|
||||||
variableOperations.setVariables(instance.getId(), variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 解析并保存节点和流转配置
|
|
||||||
try {
|
|
||||||
// 清除旧的配置(如果存在)
|
|
||||||
nodeConfigRepository.deleteByWorkflowDefinitionId(definition.getId());
|
|
||||||
transitionConfigRepository.deleteByWorkflowDefinitionId(definition.getId());
|
|
||||||
|
|
||||||
// 解析并保存新的配置
|
|
||||||
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.RUNNING);
|
|
||||||
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
|
|
||||||
@Transactional
|
|
||||||
public void executeNode(Long nodeInstanceId) {
|
|
||||||
NodeInstance nodeInstance = nodeInstanceRepository.findById(nodeInstanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_NOT_FOUND));
|
|
||||||
|
|
||||||
WorkflowInstance instance = nodeInstance.getWorkflowInstance();
|
|
||||||
if (!instance.canExecuteNode()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取节点执行器
|
|
||||||
NodeExecutor executor = nodeExecutors.get(nodeInstance.getNodeType());
|
|
||||||
if (executor == null) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTOR_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建执行上下文
|
|
||||||
WorkflowContextOperations context = DefaultWorkflowContext.builder()
|
|
||||||
.workflowInstance(instance)
|
|
||||||
.variableOperations(variableOperations)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 执行节点
|
|
||||||
executor.execute(nodeInstance, context);
|
|
||||||
|
|
||||||
// 更新节点状态
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
|
|
||||||
nodeInstance.setEndTime(LocalDateTime.now());
|
|
||||||
nodeInstanceRepository.save(nodeInstance);
|
|
||||||
|
|
||||||
// 从数据库获取流转配置
|
|
||||||
List<TransitionConfig> transitions = transitionConfigRepository
|
|
||||||
.findByWorkflowDefinitionId(instance.getWorkflowDefinition().getId());
|
|
||||||
|
|
||||||
// 获取当前节点的后续节点
|
|
||||||
List<String> nextNodeIds = transitions.stream()
|
|
||||||
.filter(t -> t.getFrom().equals(nodeInstance.getNodeId()))
|
|
||||||
.map(TransitionConfig::getTo)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// 获取节点配置
|
|
||||||
List<NodeConfig> nodeConfigs = nodeConfigRepository
|
|
||||||
.findByWorkflowDefinitionId(instance.getWorkflowDefinition().getId());
|
|
||||||
|
|
||||||
// 创建并执行后续节点
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否所有节点都已完成
|
|
||||||
List<NodeInstance> uncompletedNodes = nodeInstanceRepository
|
|
||||||
.findByWorkflowInstanceAndStatusNot(instance, NodeStatusEnum.COMPLETED);
|
|
||||||
|
|
||||||
if (uncompletedNodes.isEmpty()) {
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 更新节点状态为失败
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.FAILED);
|
|
||||||
nodeInstance.setError(e.getMessage());
|
|
||||||
nodeInstance.setEndTime(LocalDateTime.now());
|
|
||||||
nodeInstanceRepository.save(nodeInstance);
|
|
||||||
|
|
||||||
// 更新工作流实例状态为失败
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.FAILED);
|
|
||||||
instance.setError(e.getMessage());
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createAndExecuteNextNode(WorkflowInstance instance, String nextNodeId, NodeConfig nodeConfig) {
|
|
||||||
NodeInstance nextNode = new NodeInstance();
|
|
||||||
nextNode.setNodeId(nextNodeId);
|
|
||||||
nextNode.setWorkflowInstance(instance);
|
|
||||||
nextNode.setNodeType(nodeConfig.getType());
|
|
||||||
nextNode.setName(nodeConfig.getName());
|
|
||||||
// nextNode.setConfigObject(nodeExecuteConfigConverter.toNodeExecutorConfig(nodeConfig));
|
|
||||||
// nextNode.setConfigObject(nodeConfig);
|
|
||||||
nextNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
nodeInstanceRepository.save(nextNode);
|
|
||||||
|
|
||||||
// 递归执行后续节点
|
|
||||||
executeNode(nextNode.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void completeNode(Long nodeInstanceId, Map<String, Object> variables) {
|
|
||||||
NodeInstance nodeInstance = nodeInstanceRepository.findById(nodeInstanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_NOT_FOUND));
|
|
||||||
|
|
||||||
WorkflowInstance instance = nodeInstance.getWorkflowInstance();
|
|
||||||
|
|
||||||
// 设置节点输出变量
|
|
||||||
if (variables != null && !variables.isEmpty()) {
|
|
||||||
variableOperations.setVariables(instance.getId(), variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
|
|
||||||
nodeInstance.setEndTime(LocalDateTime.now());
|
|
||||||
nodeInstanceRepository.save(nodeInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void terminateWorkflow(Long instanceId, String reason) {
|
|
||||||
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
|
|
||||||
|
|
||||||
instance.terminate(reason);
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
|
|
||||||
// 清理上下文缓存
|
|
||||||
variableOperations.clearVariables(instance.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void pauseWorkflow(Long instanceId) {
|
|
||||||
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
|
|
||||||
|
|
||||||
if (!instance.canPause()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.pause();
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void resumeWorkflow(Long instanceId) {
|
|
||||||
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
|
|
||||||
|
|
||||||
if (!instance.canResume()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_PAUSED);
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.resume();
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void retryWorkflow(Long instanceId) {
|
|
||||||
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
|
|
||||||
|
|
||||||
if (!instance.canRetry()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重试工作流实例
|
|
||||||
instance.retry();
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
|
|
||||||
// 获取失败的节点
|
|
||||||
List<NodeInstance> failedNodes = nodeInstanceRepository.findByWorkflowInstanceAndStatus(
|
|
||||||
instance, NodeStatusEnum.FAILED);
|
|
||||||
|
|
||||||
// 重试失败的节点
|
|
||||||
for (NodeInstance node : failedNodes) {
|
|
||||||
node.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
node.setError(null);
|
|
||||||
nodeInstanceRepository.save(node);
|
|
||||||
executeNode(node.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void checkTimeout(Long instanceId, long timeoutMillis) {
|
|
||||||
WorkflowInstance instance = workflowInstanceRepository.findById(instanceId)
|
|
||||||
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND));
|
|
||||||
|
|
||||||
if (instance.isTimeout(timeoutMillis)) {
|
|
||||||
String error = "Workflow execution timeout after " + timeoutMillis + " milliseconds";
|
|
||||||
terminateWorkflow(instanceId, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NodeInstance createStartNode(WorkflowDefinition definition, WorkflowInstance instance) {
|
|
||||||
NodeInstance startNode = new NodeInstance();
|
|
||||||
startNode.setWorkflowInstance(instance);
|
|
||||||
startNode.setNodeId("start");
|
|
||||||
startNode.setNodeType(NodeTypeEnum.START);
|
|
||||||
startNode.setName("开始节点");
|
|
||||||
startNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
startNode.setCreateTime(LocalDateTime.now());
|
|
||||||
return nodeInstanceRepository.save(startNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流上下文
|
|
||||||
* 负责管理工作流执行过程中的运行时状态
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class WorkflowContext {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流实例ID
|
|
||||||
*/
|
|
||||||
private final Long instanceId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前节点实例
|
|
||||||
*/
|
|
||||||
private NodeInstance currentNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所有节点实例
|
|
||||||
*/
|
|
||||||
private List<NodeInstance> allNodes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 临时变量(节点间传递,不持久化)
|
|
||||||
*/
|
|
||||||
private final Map<String, Object> tempVariables;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 变量操作服务
|
|
||||||
*/
|
|
||||||
private final WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
public WorkflowContext(Long instanceId, WorkflowVariableOperations variableOperations) {
|
|
||||||
this.instanceId = instanceId;
|
|
||||||
this.tempVariables = new ConcurrentHashMap<>();
|
|
||||||
this.variableOperations = variableOperations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取变量值(委托给 WorkflowVariableOperations)
|
|
||||||
* @deprecated 建议直接使用 WorkflowVariableOperations
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public Object getVariable(String key) {
|
|
||||||
return variableOperations.getVariables(instanceId).get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置变量值(委托给 WorkflowVariableOperations)
|
|
||||||
* @deprecated 建议直接使用 WorkflowVariableOperations
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void setVariable(String key, Object value) {
|
|
||||||
Map<String, Object> vars = new ConcurrentHashMap<>();
|
|
||||||
vars.put(key, value);
|
|
||||||
variableOperations.setVariables(instanceId, vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有变量(委托给 WorkflowVariableOperations)
|
|
||||||
* @deprecated 建议直接使用 WorkflowVariableOperations
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public Map<String, Object> getVariables() {
|
|
||||||
return variableOperations.getVariables(instanceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置多个变量(委托给 WorkflowVariableOperations)
|
|
||||||
* @deprecated 建议直接使用 WorkflowVariableOperations
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void setVariables(Map<String, Object> variables) {
|
|
||||||
variableOperations.setVariables(instanceId, variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取临时变量值
|
|
||||||
*/
|
|
||||||
public Object getTempVariable(String key) {
|
|
||||||
return tempVariables.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置临时变量值
|
|
||||||
*/
|
|
||||||
public void setTempVariable(String key, Object value) {
|
|
||||||
tempVariables.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有临时变量
|
|
||||||
*/
|
|
||||||
public Map<String, Object> getTempVariables() {
|
|
||||||
return new ConcurrentHashMap<>(tempVariables);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除临时变量
|
|
||||||
*/
|
|
||||||
public void clearTempVariables() {
|
|
||||||
tempVariables.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流执行引擎接口
|
|
||||||
*/
|
|
||||||
public interface WorkflowEngine {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动工作流实例
|
|
||||||
*/
|
|
||||||
WorkflowInstance startWorkflow(String workflowCode, String businessKey, Map<String, Object> variables);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行节点
|
|
||||||
*/
|
|
||||||
void executeNode(Long nodeInstanceId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 完成节点
|
|
||||||
*/
|
|
||||||
void completeNode(Long nodeInstanceId, Map<String, Object> variables);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 终止工作流实例
|
|
||||||
*/
|
|
||||||
void terminateWorkflow(Long instanceId, String reason);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 暂停工作流实例
|
|
||||||
*/
|
|
||||||
void pauseWorkflow(Long instanceId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 恢复工作流实例
|
|
||||||
*/
|
|
||||||
void resumeWorkflow(Long instanceId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试工作流实例
|
|
||||||
*/
|
|
||||||
void retryWorkflow(Long instanceId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查工作流实例是否超时
|
|
||||||
* @param instanceId 工作流实例ID
|
|
||||||
* @param timeoutMillis 超时时间(毫秒)
|
|
||||||
*/
|
|
||||||
void checkTimeout(Long instanceId, long timeoutMillis);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.config;
|
package com.qqchen.deploy.backend.workflow.engine.config;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.NodeExecutor;
|
import com.qqchen.deploy.backend.workflow.engine.executor.INodeExecutor;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.task.TaskExecutor;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -15,20 +14,11 @@ import java.util.stream.Collectors;
|
|||||||
public class WorkflowEngineConfig {
|
public class WorkflowEngineConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Map<String, TaskExecutor> taskExecutorMap(List<TaskExecutor> executors) {
|
public Map<NodeTypeEnum, INodeExecutor> nodeExecutorMap(List<INodeExecutor> executors) {
|
||||||
return executors.stream()
|
return executors.stream()
|
||||||
.collect(Collectors.toMap(
|
.collect(Collectors.toMap(
|
||||||
executor -> executor.getClass().getSimpleName().replace("TaskExecutor", "").toUpperCase(),
|
INodeExecutor::getNodeType,
|
||||||
Function.identity()
|
Function.identity()
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Map<NodeTypeEnum, NodeExecutor> nodeExecutorMap(List<NodeExecutor> executors) {
|
|
||||||
return executors.stream()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
NodeExecutor::getNodeType,
|
|
||||||
Function.identity()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,97 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.context;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认工作流上下文实现
|
|
||||||
*/
|
|
||||||
public class DefaultWorkflowContext implements WorkflowContextOperations {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(DefaultWorkflowContext.class);
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final WorkflowInstance workflowInstance;
|
|
||||||
|
|
||||||
private final WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
public DefaultWorkflowContext(WorkflowInstance workflowInstance, WorkflowVariableOperations variableOperations) {
|
|
||||||
this.workflowInstance = workflowInstance;
|
|
||||||
this.variableOperations = variableOperations;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public WorkflowInstance getInstance() {
|
|
||||||
return workflowInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getVariable(String key) {
|
|
||||||
return variableOperations.getVariable(workflowInstance.getId(), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setVariable(String key, Object value) {
|
|
||||||
variableOperations.setVariable(workflowInstance.getId(), key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setVariables(Map<String, Object> variables) {
|
|
||||||
variableOperations.setVariables(workflowInstance.getId(), variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> getVariables() {
|
|
||||||
return variableOperations.getVariables(workflowInstance.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void log(String message, LogLevelEnum level) {
|
|
||||||
switch (level) {
|
|
||||||
case DEBUG -> logger.debug("[Workflow:{}] {}", workflowInstance.getId(), message);
|
|
||||||
case INFO -> logger.info("[Workflow:{}] {}", workflowInstance.getId(), message);
|
|
||||||
case WARN -> logger.warn("[Workflow:{}] {}", workflowInstance.getId(), message);
|
|
||||||
case ERROR -> logger.error("[Workflow:{}] {}", workflowInstance.getId(), message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void log(String message, String detail, LogLevelEnum level) {
|
|
||||||
switch (level) {
|
|
||||||
case DEBUG -> logger.debug("[Workflow:{}] {} - Detail: {}", workflowInstance.getId(), message, detail);
|
|
||||||
case INFO -> logger.info("[Workflow:{}] {} - Detail: {}", workflowInstance.getId(), message, detail);
|
|
||||||
case WARN -> logger.warn("[Workflow:{}] {} - Detail: {}", workflowInstance.getId(), message, detail);
|
|
||||||
case ERROR -> logger.error("[Workflow:{}] {} - Detail: {}", workflowInstance.getId(), message, detail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
private WorkflowInstance workflowInstance;
|
|
||||||
|
|
||||||
private WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
public Builder workflowInstance(WorkflowInstance workflowInstance) {
|
|
||||||
this.workflowInstance = workflowInstance;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder variableOperations(WorkflowVariableOperations variableOperations) {
|
|
||||||
this.variableOperations = variableOperations;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DefaultWorkflowContext build() {
|
|
||||||
return new DefaultWorkflowContext(workflowInstance, variableOperations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.context;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流上下文操作接口
|
|
||||||
* 定义工作流执行过程中的上下文操作能力
|
|
||||||
*/
|
|
||||||
public interface WorkflowContextOperations {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取工作流实例
|
|
||||||
*/
|
|
||||||
WorkflowInstance getInstance();
|
|
||||||
|
|
||||||
void setVariables(Map<String, Object> variables);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有变量
|
|
||||||
* @deprecated 使用 WorkflowVariableOperations 替代
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
Map<String, Object> getVariables();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取变量
|
|
||||||
* @deprecated 使用 WorkflowVariableOperations 替代
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
Object getVariable(String name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置变量
|
|
||||||
* @deprecated 使用 WorkflowVariableOperations 替代
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
void setVariable(String name, Object value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录日志
|
|
||||||
*/
|
|
||||||
void log(String message, LogLevelEnum level);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录日志(带详情)
|
|
||||||
*/
|
|
||||||
void log(String message, String detail, LogLevelEnum level);
|
|
||||||
}
|
|
||||||
@ -3,25 +3,9 @@ package com.qqchen.deploy.backend.workflow.engine.definition;
|
|||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务执行器定义
|
|
||||||
* 用于定义工作流节点支持的任务执行器类型及其配置结构
|
|
||||||
* 主要用于:
|
|
||||||
* 1. 节点类型配置:定义节点类型支持哪些执行器
|
|
||||||
* 2. 流程设计:前端根据执行器定义动态渲染配置表单
|
|
||||||
* 3. 执行引擎:根据执行器定义验证和执行任务
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
public class TaskExecutorDefinition {
|
public class NodeExecutorDefinition {
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行器编码
|
|
||||||
* 用于标识执行器类型,需要与具体的执行器实现类对应
|
|
||||||
* 例如:
|
|
||||||
* - SHELL:对应ShellTaskExecutor
|
|
||||||
* - JENKINS:对应JenkinsTaskExecutor
|
|
||||||
* - HTTP:对应HttpTaskExecutor
|
|
||||||
*/
|
|
||||||
@NotBlank(message = "执行器编码不能为空")
|
@NotBlank(message = "执行器编码不能为空")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
@ -51,7 +35,7 @@ public class TaskExecutorDefinition {
|
|||||||
* 用于:
|
* 用于:
|
||||||
* 1. 前端动态渲染配置表单
|
* 1. 前端动态渲染配置表单
|
||||||
* 2. 后端验证配置参数
|
* 2. 后端验证配置参数
|
||||||
*
|
*
|
||||||
* 示例 - Shell执行器配置模式:
|
* 示例 - Shell执行器配置模式:
|
||||||
* {
|
* {
|
||||||
* "type": "object",
|
* "type": "object",
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.executor;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.NodeConfig;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.WorkflowGraph;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象节点执行器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AbstractNodeExecutor implements INodeExecutor {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
protected ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
protected INodeInstanceRepository nodeInstanceRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
protected IWorkflowLogService workflowLogService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
@Lazy
|
||||||
|
private Map<NodeTypeEnum, INodeExecutor> nodeExecutors;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(WorkflowInstance workflowInstance, WorkflowGraph graph, NodeConfig currentNode) {
|
||||||
|
NodeInstance nodeInstance = null;
|
||||||
|
try {
|
||||||
|
nodeInstance = createNodeInstance(workflowInstance, currentNode);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 1. 创建节点实例
|
||||||
|
// 1. 前置处理
|
||||||
|
beforeExecute(workflowInstance, nodeInstance);
|
||||||
|
|
||||||
|
// 2. 执行节点逻辑
|
||||||
|
doExecute(workflowInstance, nodeInstance);
|
||||||
|
|
||||||
|
// 3. 后置处理
|
||||||
|
afterExecute(workflowInstance, nodeInstance);
|
||||||
|
|
||||||
|
// 4. 更新节点状态
|
||||||
|
updateNodeStatus(nodeInstance, true);
|
||||||
|
|
||||||
|
// 4. 获取并执行下一个节点
|
||||||
|
List<NodeConfig> nextNodes = graph.getNextNodes(currentNode.getNodeId());
|
||||||
|
executeNextNodes(workflowInstance, graph, nextNodes);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 5. 异常处理
|
||||||
|
handleExecutionError(workflowInstance, nodeInstance, e);
|
||||||
|
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行后续节点
|
||||||
|
*/
|
||||||
|
protected void executeNextNodes(WorkflowInstance workflowInstance, WorkflowGraph graph, List<NodeConfig> nextNodes) {
|
||||||
|
for (NodeConfig nextNode : nextNodes) {
|
||||||
|
INodeExecutor executor = nodeExecutors.get(nextNode.getType());
|
||||||
|
executor.execute(workflowInstance, graph, nextNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建节点实例
|
||||||
|
*/
|
||||||
|
protected NodeInstance createNodeInstance(WorkflowInstance workflowInstance, NodeConfig config) throws JsonProcessingException {
|
||||||
|
NodeInstance node = new NodeInstance();
|
||||||
|
node.setWorkflowInstance(workflowInstance);
|
||||||
|
node.setNodeId(config.getNodeId());
|
||||||
|
node.setNodeType(config.getType());
|
||||||
|
node.setName(config.getName());
|
||||||
|
node.setConfig(objectMapper.writeValueAsString(config.getConfig()));
|
||||||
|
node.setStatus(NodeStatusEnum.RUNNING);
|
||||||
|
node.setCreateTime(LocalDateTime.now());
|
||||||
|
node.setStartTime(LocalDateTime.now());
|
||||||
|
return nodeInstanceRepository.save(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行节点逻辑
|
||||||
|
*/
|
||||||
|
protected abstract void doExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前置处理
|
||||||
|
*/
|
||||||
|
protected void beforeExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后置处理
|
||||||
|
*/
|
||||||
|
protected void afterExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
logNodeComplete(nodeInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理执行异常
|
||||||
|
*/
|
||||||
|
protected void handleExecutionError(WorkflowInstance workflowInstance, NodeInstance nodeInstance, Exception e) {
|
||||||
|
log.error("Node execution failed. nodeInstance: {}, error: {}", nodeInstance.getId(), e.getMessage(), e);
|
||||||
|
logSystem(nodeInstance, LogLevelEnum.ERROR, String.format("节点执行失败: %s[%s]", nodeInstance.getName(), nodeInstance.getNodeId()), e.getMessage());
|
||||||
|
logNodeError(nodeInstance, e.getMessage());
|
||||||
|
updateNodeStatus(nodeInstance, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点状态
|
||||||
|
*/
|
||||||
|
private void updateNodeStatus(NodeInstance nodeInstance, boolean success) {
|
||||||
|
nodeInstance.setStatus(success ? NodeStatusEnum.COMPLETED : NodeStatusEnum.FAILED);
|
||||||
|
if (success) {
|
||||||
|
nodeInstance.setEndTime(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
nodeInstanceRepository.save(nodeInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录系统日志
|
||||||
|
*/
|
||||||
|
protected void logSystem(NodeInstance nodeInstance, LogLevelEnum level, String message, String detail) {
|
||||||
|
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(), level, message, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录节点完成日志
|
||||||
|
*/
|
||||||
|
protected void logNodeComplete(NodeInstance nodeInstance) {
|
||||||
|
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(), LogLevelEnum.INFO, String.format("节点执行完成: %s", nodeInstance.getName()), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录节点错误日志
|
||||||
|
*/
|
||||||
|
protected void logNodeError(NodeInstance nodeInstance, String error) {
|
||||||
|
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(), LogLevelEnum.ERROR, String.format("节点执行失败: %s", nodeInstance.getName()), error);
|
||||||
|
nodeInstance.setError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.executor;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束节点执行器
|
||||||
|
* 负责完成工作流实例的执行
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component("endNodeExecutor")
|
||||||
|
public class EndNodeExecutor extends AbstractNodeExecutor {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NodeTypeEnum getNodeType() {
|
||||||
|
return NodeTypeEnum.END;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(String config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminate(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.executor;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.NodeConfig;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.WorkflowGraph;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点执行器接口
|
||||||
|
*/
|
||||||
|
public interface INodeExecutor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的节点类型
|
||||||
|
*/
|
||||||
|
NodeTypeEnum getNodeType();
|
||||||
|
|
||||||
|
void execute(WorkflowInstance workflowInstance, WorkflowGraph graph, NodeConfig currentNode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证节点配置
|
||||||
|
*/
|
||||||
|
void validate(String config);
|
||||||
|
|
||||||
|
void terminate(WorkflowInstance workflowInstance, NodeInstance nodeInstance);
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.executor;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 脚本节点执行器
|
||||||
|
* 支持多种脚本语言(Python, Shell, JavaScript等)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component("scriptNodeExecutor")
|
||||||
|
public class ScriptNodeExecutor extends AbstractNodeExecutor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NodeTypeEnum getNodeType() {
|
||||||
|
return NodeTypeEnum.SCRIPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(String config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminate(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.executor;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始节点执行器
|
||||||
|
* 负责工作流实例的初始化工作
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component("startNodeExecutor")
|
||||||
|
public class StartNodeExecutor extends AbstractNodeExecutor {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NodeTypeEnum getNodeType() {
|
||||||
|
return NodeTypeEnum.START;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(String config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminate(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点配置基类
|
||||||
|
*/
|
||||||
|
public class NodeExecutorConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点ID
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点类型
|
||||||
|
*/
|
||||||
|
private NodeTypeEnum type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
package com.qqchen.deploy.backend.workflow.engine.executor.config;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分支汇聚配置
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class BranchConvergeConfig {
|
|
||||||
/**
|
|
||||||
* 汇聚策略
|
|
||||||
*/
|
|
||||||
private ConvergeStrategy strategy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 需要完成的分支数量(N个完成时使用)
|
|
||||||
*/
|
|
||||||
private Integer requiredCount;
|
|
||||||
|
|
||||||
public enum ConvergeStrategy {
|
|
||||||
ALL("全部完成"),
|
|
||||||
ANY("任一完成"),
|
|
||||||
N("N个完成");
|
|
||||||
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
ConvergeStrategy(String description) {
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于条件的网关配置基类
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public abstract class ConditionalGatewayConfig extends GatewayConfig {
|
|
||||||
/**
|
|
||||||
* 条件分支列表
|
|
||||||
*/
|
|
||||||
private List<ConditionalBranch> branches;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认分支节点ID
|
|
||||||
*/
|
|
||||||
private String defaultNodeId;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class ConditionalBranch {
|
|
||||||
/**
|
|
||||||
* 分支名称
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 条件表达式
|
|
||||||
*/
|
|
||||||
private String condition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 目标节点ID
|
|
||||||
*/
|
|
||||||
private String to;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分支描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import org.springframework.expression.ExpressionParser;
|
|
||||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
|
||||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 排他网关配置
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class ExclusiveGatewayConfig extends ConditionalGatewayConfig {
|
|
||||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> getNextNodeIds(WorkflowContextOperations context) {
|
|
||||||
// 返回第一个满足条件的分支的targetNodeId
|
|
||||||
for (ConditionalBranch branch : getBranches()) {
|
|
||||||
if (evaluateCondition(branch.getCondition(), context)) {
|
|
||||||
return Collections.singletonList(branch.getTo());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Collections.singletonList(getDefaultNodeId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean evaluateCondition(String condition, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(context);
|
|
||||||
return Boolean.TRUE.equals(expressionParser.parseExpression(condition)
|
|
||||||
.getValue(evaluationContext, Boolean.class));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.GatewayTypeEnum;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 网关配置基类
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
|
||||||
@JsonSubTypes({
|
|
||||||
@JsonSubTypes.Type(value = ExclusiveGatewayConfig.class, name = "EXCLUSIVE"),
|
|
||||||
@JsonSubTypes.Type(value = ParallelGatewayConfig.class, name = "PARALLEL"),
|
|
||||||
@JsonSubTypes.Type(value = InclusiveGatewayConfig.class, name = "INCLUSIVE")
|
|
||||||
})
|
|
||||||
public abstract class GatewayConfig {
|
|
||||||
/**
|
|
||||||
* 网关类型
|
|
||||||
*/
|
|
||||||
private GatewayTypeEnum type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 网关名称
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 网关描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取下一个节点IDs
|
|
||||||
*/
|
|
||||||
public abstract List<String> getNextNodeIds(WorkflowContextOperations context);
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.NodeExecutor;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class GatewayNodeExecutor implements NodeExecutor {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NodeTypeEnum getNodeType() {
|
|
||||||
return NodeTypeEnum.GATEWAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
// 解析网关配置
|
|
||||||
GatewayConfig config = objectMapper.readValue(nodeInstance.getConfig(), GatewayConfig.class);
|
|
||||||
|
|
||||||
// 获取下一个节点IDs
|
|
||||||
List<String> nextNodeIds = config.getNextNodeIds(context);
|
|
||||||
|
|
||||||
// 设置输出
|
|
||||||
nodeInstance.setOutput(objectMapper.createObjectNode()
|
|
||||||
.put("nextNodeIds", String.join(",", nextNodeIds))
|
|
||||||
.toString());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Gateway node execution failed", e);
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validate(String config) {
|
|
||||||
try {
|
|
||||||
objectMapper.readValue(config, GatewayConfig.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// Gateway nodes are instant operations, no need to terminate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import org.springframework.expression.ExpressionParser;
|
|
||||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
|
||||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 包容网关配置
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class InclusiveGatewayConfig extends ConditionalGatewayConfig {
|
|
||||||
/**
|
|
||||||
* 汇聚配置
|
|
||||||
*/
|
|
||||||
private BranchConvergeConfig convergeConfig;
|
|
||||||
|
|
||||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> getNextNodeIds(WorkflowContextOperations context) {
|
|
||||||
// 返回所有满足条件的分支的targetNodeId
|
|
||||||
List<String> nextNodeIds = getBranches().stream()
|
|
||||||
.filter(branch -> evaluateCondition(branch.getCondition(), context))
|
|
||||||
.map(ConditionalBranch::getTo)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 如果没有满足条件的分支,使用默认分支
|
|
||||||
if (nextNodeIds.isEmpty() && getDefaultNodeId() != null) {
|
|
||||||
nextNodeIds.add(getDefaultNodeId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextNodeIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean evaluateCondition(String condition, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(context);
|
|
||||||
return Boolean.TRUE.equals(expressionParser.parseExpression(condition)
|
|
||||||
.getValue(evaluationContext, Boolean.class));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.gateway;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 并行网关配置
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class ParallelGatewayConfig extends GatewayConfig {
|
|
||||||
/**
|
|
||||||
* 并行分支列表
|
|
||||||
*/
|
|
||||||
private List<ParallelBranch> branches;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 汇聚配置
|
|
||||||
*/
|
|
||||||
private BranchConvergeConfig convergeConfig;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class ParallelBranch {
|
|
||||||
/**
|
|
||||||
* 分支名称
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 目标节点ID
|
|
||||||
*/
|
|
||||||
private String to;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分支描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> getNextNodeIds(WorkflowContextOperations context) {
|
|
||||||
// 返回所有分支的targetNodeId
|
|
||||||
return branches.stream()
|
|
||||||
.map(ParallelBranch::getTo)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowLogService;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 抽象节点执行器
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class AbstractNodeExecutor implements NodeExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
protected WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
protected INodeInstanceRepository nodeInstanceRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
protected IWorkflowLogService workflowLogService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
// 1. 前置处理
|
|
||||||
beforeExecute(nodeInstance, context);
|
|
||||||
|
|
||||||
// 2. 执行节点逻辑
|
|
||||||
doExecute(nodeInstance, context);
|
|
||||||
|
|
||||||
// 3. 后置处理
|
|
||||||
afterExecute(nodeInstance, context);
|
|
||||||
|
|
||||||
// 4. 更新节点状态
|
|
||||||
updateNodeStatus(nodeInstance, true);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 5. 异常处理
|
|
||||||
handleExecutionError(nodeInstance, context, e);
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行节点逻辑
|
|
||||||
*/
|
|
||||||
protected abstract void doExecute(NodeInstance nodeInstance, WorkflowContextOperations context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 前置处理
|
|
||||||
*/
|
|
||||||
protected void beforeExecute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
context.log("Starting node execution: " + nodeInstance.getName(), LogLevelEnum.INFO);
|
|
||||||
logNodeStart(nodeInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 后置处理
|
|
||||||
*/
|
|
||||||
protected void afterExecute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
context.log("Node execution completed: " + nodeInstance.getName(), LogLevelEnum.INFO);
|
|
||||||
logNodeComplete(nodeInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理执行异常
|
|
||||||
*/
|
|
||||||
protected void handleExecutionError(NodeInstance nodeInstance, WorkflowContextOperations context, Exception e) {
|
|
||||||
log.error("Node execution failed. nodeInstance: {}, error: {}", nodeInstance.getId(), e.getMessage(), e);
|
|
||||||
context.log("Node execution failed: " + e.getMessage(), LogLevelEnum.ERROR);
|
|
||||||
logSystem(nodeInstance, LogLevelEnum.ERROR,
|
|
||||||
String.format("节点执行失败: %s[%s]", nodeInstance.getName(), nodeInstance.getNodeId()),
|
|
||||||
e.getMessage());
|
|
||||||
logNodeError(nodeInstance, e.getMessage());
|
|
||||||
updateNodeStatus(nodeInstance, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新节点状态
|
|
||||||
*/
|
|
||||||
private void updateNodeStatus(NodeInstance nodeInstance, boolean success) {
|
|
||||||
nodeInstance.setStatus(success ? NodeStatusEnum.COMPLETED : NodeStatusEnum.FAILED);
|
|
||||||
if (success) {
|
|
||||||
nodeInstance.setEndTime(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
nodeInstanceRepository.save(nodeInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录系统日志
|
|
||||||
*/
|
|
||||||
protected void logSystem(NodeInstance nodeInstance, LogLevelEnum level, String message, String detail) {
|
|
||||||
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(), level, message, detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录节点开始日志
|
|
||||||
*/
|
|
||||||
protected void logNodeStart(NodeInstance nodeInstance) {
|
|
||||||
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(),
|
|
||||||
LogLevelEnum.INFO, String.format("节点开始执行: %s", nodeInstance.getName()), null);
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.RUNNING);
|
|
||||||
nodeInstance.setStartTime(LocalDateTime.now());
|
|
||||||
nodeInstanceRepository.save(nodeInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录节点完成日志
|
|
||||||
*/
|
|
||||||
protected void logNodeComplete(NodeInstance nodeInstance) {
|
|
||||||
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(),
|
|
||||||
LogLevelEnum.INFO, String.format("节点执行完成: %s", nodeInstance.getName()), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录节点错误日志
|
|
||||||
*/
|
|
||||||
protected void logNodeError(NodeInstance nodeInstance, String error) {
|
|
||||||
workflowLogService.log(nodeInstance.getWorkflowInstance(), nodeInstance.getNodeId(),
|
|
||||||
LogLevelEnum.ERROR, String.format("节点执行失败: %s", nodeInstance.getName()), error);
|
|
||||||
nodeInstance.setError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 结束节点执行器
|
|
||||||
* 负责完成工作流实例的执行
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component("endNodeExecutor")
|
|
||||||
public class EndNodeExecutor extends AbstractNodeExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private IWorkflowInstanceRepository workflowInstanceRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NodeTypeEnum getNodeType() {
|
|
||||||
return NodeTypeEnum.END;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validate(String config) {
|
|
||||||
// 结束节点不需要配置,无需验证
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doExecute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// 完成工作流实例
|
|
||||||
WorkflowInstance instance = nodeInstance.getWorkflowInstance();
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.COMPLETED);
|
|
||||||
instance.setEndTime(LocalDateTime.now());
|
|
||||||
workflowInstanceRepository.save(instance);
|
|
||||||
|
|
||||||
// 记录日志
|
|
||||||
context.log("工作流执行完成", LogLevelEnum.INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// 结束节点不需要终止逻辑
|
|
||||||
context.log("结束节点无需终止操作", LogLevelEnum.INFO);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点执行器接口
|
|
||||||
*/
|
|
||||||
public interface NodeExecutor {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取支持的节点类型
|
|
||||||
*/
|
|
||||||
NodeTypeEnum getNodeType();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行节点
|
|
||||||
*
|
|
||||||
* @param nodeInstance 节点实例
|
|
||||||
* @param context 工作流上下文
|
|
||||||
*/
|
|
||||||
void execute(NodeInstance nodeInstance, WorkflowContextOperations context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证节点配置
|
|
||||||
*/
|
|
||||||
void validate(String config);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 终止节点执行
|
|
||||||
*
|
|
||||||
* @param nodeInstance 节点实例
|
|
||||||
* @param context 工作流上下文
|
|
||||||
*/
|
|
||||||
void terminate(NodeInstance nodeInstance, WorkflowContextOperations context);
|
|
||||||
}
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeExecutorConfig;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
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等)
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class ScriptNodeExecutor extends AbstractNodeExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ScriptCommandRegistry commandRegistry;
|
|
||||||
|
|
||||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NodeTypeEnum getNodeType() {
|
|
||||||
return NodeTypeEnum.SCRIPT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validate(String config) {
|
|
||||||
try {
|
|
||||||
ScriptNodeExecutorConfig scriptConfig = objectMapper.readValue(config, ScriptNodeExecutorConfig.class);
|
|
||||||
// 验证脚本内容
|
|
||||||
if (scriptConfig.getScript() == null || scriptConfig.getScript().trim().isEmpty()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Script content cannot be empty");
|
|
||||||
}
|
|
||||||
// 验证脚本语言
|
|
||||||
if (scriptConfig.getLanguage() == null) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Script language must be specified");
|
|
||||||
}
|
|
||||||
// 验证其他参数
|
|
||||||
if (scriptConfig.getTimeout() != null && scriptConfig.getTimeout() <= 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Timeout must be positive");
|
|
||||||
}
|
|
||||||
if (scriptConfig.getRetryTimes() != null && scriptConfig.getRetryTimes() < 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Retry times cannot be negative");
|
|
||||||
}
|
|
||||||
if (scriptConfig.getRetryInterval() != null && scriptConfig.getRetryInterval() < 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Retry interval cannot be negative");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doExecute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
// String configJson = nodeInstance.getConfig();
|
|
||||||
// ScriptNodeExecutorConfig config = objectMapper.readValue(configJson, ScriptNodeExecutorConfig.class);
|
|
||||||
//
|
|
||||||
// // 设置重试次数和间隔
|
|
||||||
// int maxAttempts = config.getRetryTimes() != null ? config.getRetryTimes() : 1;
|
|
||||||
// long retryInterval = config.getRetryInterval() != null ? config.getRetryInterval() : 0;
|
|
||||||
//
|
|
||||||
// Exception lastException = null;
|
|
||||||
// for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
// try {
|
|
||||||
// // 执行脚本
|
|
||||||
ScriptNodeExecutorConfig config = new ScriptNodeExecutorConfig();
|
|
||||||
config.setLanguage(ScriptLanguageEnum.SHELL);
|
|
||||||
config.setInterpreter("/bin/bash");
|
|
||||||
config.setScript("ls -a");
|
|
||||||
executeScript(config, nodeInstance, context);
|
|
||||||
// return; // 执行成功,直接返回
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// lastException = e;
|
|
||||||
// if (attempt < maxAttempts) {
|
|
||||||
// context.log(String.format("Script execution failed (attempt %d/%d), retrying in %d seconds",
|
|
||||||
// attempt, maxAttempts, retryInterval), LogLevelEnum.WARN);
|
|
||||||
// Thread.sleep(retryInterval * 1000L);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // 如果所有重试都失败,抛出最后一个异常
|
|
||||||
// if (lastException != null) {
|
|
||||||
// throw lastException;
|
|
||||||
// }
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeScript(ScriptNodeExecutorConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) throws Exception {
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
||||||
|
|
||||||
// 获取命令实现并构建命令
|
|
||||||
ScriptCommand command = commandRegistry.getCommand(config.getLanguage());
|
|
||||||
List<String> commandList = command.buildCommand(config);
|
|
||||||
|
|
||||||
processBuilder.command(commandList);
|
|
||||||
|
|
||||||
// 设置工作目录
|
|
||||||
if (config.getWorkingDirectory() != null && !config.getWorkingDirectory().trim().isEmpty()) {
|
|
||||||
processBuilder.directory(new java.io.File(config.getWorkingDirectory()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置环境变量
|
|
||||||
if (config.getEnvironment() != null && !config.getEnvironment().isEmpty()) {
|
|
||||||
Map<String, String> env = processBuilder.environment();
|
|
||||||
env.putAll(config.getEnvironment());
|
|
||||||
}
|
|
||||||
|
|
||||||
Process process = processBuilder.start();
|
|
||||||
|
|
||||||
// 创建用于读取输出的Future
|
|
||||||
Future<List<String>> outputFuture = executorService.submit(() -> readOutput(process.getInputStream()));
|
|
||||||
Future<List<String>> errorFuture = executorService.submit(() -> readOutput(process.getErrorStream()));
|
|
||||||
|
|
||||||
// 等待进程执行完成或超时
|
|
||||||
boolean completed = true;
|
|
||||||
if (config.getTimeout() != null && config.getTimeout() > 0) {
|
|
||||||
completed = process.waitFor(config.getTimeout(), TimeUnit.SECONDS);
|
|
||||||
if (!completed) {
|
|
||||||
process.destroyForcibly();
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
|
|
||||||
String.format("Script execution timed out after %d seconds", config.getTimeout()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
process.waitFor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取输出结果
|
|
||||||
List<String> output = outputFuture.get(5, TimeUnit.SECONDS); // 给5秒时间读取输出
|
|
||||||
List<String> error = errorFuture.get(5, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
// 检查退出码
|
|
||||||
int exitCode = process.exitValue();
|
|
||||||
if (config.getSuccessExitCode() != null && exitCode != config.getSuccessExitCode()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
|
|
||||||
String.format("Script execution failed with exit code: %d%nError output: %s",
|
|
||||||
exitCode, String.join("\n", error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置输出变量
|
|
||||||
Map<String, Object> outputVariables = new HashMap<>();
|
|
||||||
outputVariables.put("scriptOutput", String.join("\n", output));
|
|
||||||
outputVariables.put("exitCode", exitCode);
|
|
||||||
variableOperations.setVariables(nodeInstance.getWorkflowInstance().getId(), outputVariables);
|
|
||||||
|
|
||||||
// 记录执行日志
|
|
||||||
context.log(String.format("Script executed successfully with exit code: %d", exitCode), LogLevelEnum.INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> readOutput(java.io.InputStream inputStream) throws Exception {
|
|
||||||
List<String> output = new ArrayList<>();
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
output.add(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现终止脚本进程的逻辑
|
|
||||||
context.log("Script node termination is not implemented yet", LogLevelEnum.WARN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ShellNodeExecutorConfig;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shell节点执行器
|
|
||||||
* @deprecated 请使用 {@link ScriptNodeExecutor} 替代
|
|
||||||
*/
|
|
||||||
@Deprecated(since = "1.0", forRemoval = true)
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class ShellNodeExecutor extends AbstractNodeExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NodeTypeEnum getNodeType() {
|
|
||||||
return NodeTypeEnum.SHELL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validate(String config) {
|
|
||||||
try {
|
|
||||||
ShellNodeExecutorConfig shellConfig = objectMapper.readValue(config, ShellNodeExecutorConfig.class);
|
|
||||||
// 验证执行器类型
|
|
||||||
if (!"SHELL".equals(shellConfig.getExecutor())) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
// 验证脚本内容
|
|
||||||
if (shellConfig.getScript() == null || shellConfig.getScript().trim().isEmpty()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
// 验证其他参数
|
|
||||||
if (shellConfig.getTimeout() != null && shellConfig.getTimeout() <= 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
if (shellConfig.getRetryTimes() != null && shellConfig.getRetryTimes() < 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
if (shellConfig.getRetryInterval() != null && shellConfig.getRetryInterval() < 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doExecute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
String configJson = nodeInstance.getConfig();
|
|
||||||
ShellNodeExecutorConfig config = objectMapper.readValue(configJson, ShellNodeExecutorConfig.class);
|
|
||||||
|
|
||||||
// 验证执行器类型
|
|
||||||
if (!"SHELL".equals(config.getExecutor())) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置重试次数和间隔
|
|
||||||
int maxAttempts = config.getRetryTimes() != null ? config.getRetryTimes() : 1;
|
|
||||||
long retryInterval = config.getRetryInterval() != null ? config.getRetryInterval() : 0;
|
|
||||||
|
|
||||||
Exception lastException = null;
|
|
||||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
try {
|
|
||||||
// 执行Shell命令
|
|
||||||
executeShellCommand(config, nodeInstance, context);
|
|
||||||
return; // 执行成功,直接返回
|
|
||||||
} catch (Exception e) {
|
|
||||||
lastException = e;
|
|
||||||
if (attempt < maxAttempts) {
|
|
||||||
context.log(String.format("Shell execution failed (attempt %d/%d), retrying in %d seconds", attempt, maxAttempts, retryInterval), LogLevelEnum.WARN);
|
|
||||||
Thread.sleep(retryInterval * 1000L);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果所有重试都失败,抛出最后一个异常
|
|
||||||
if (lastException != null) {
|
|
||||||
throw lastException;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeShellCommand(ShellNodeExecutorConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) throws Exception {
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
||||||
processBuilder.command("sh", "-c", config.getScript());
|
|
||||||
|
|
||||||
// 设置工作目录
|
|
||||||
if (config.getWorkingDirectory() != null && !config.getWorkingDirectory().trim().isEmpty()) {
|
|
||||||
processBuilder.directory(new java.io.File(config.getWorkingDirectory()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置环境变量
|
|
||||||
if (config.getEnvironment() != null && !config.getEnvironment().isEmpty()) {
|
|
||||||
Map<String, String> env = processBuilder.environment();
|
|
||||||
env.putAll(config.getEnvironment());
|
|
||||||
}
|
|
||||||
|
|
||||||
Process process = processBuilder.start();
|
|
||||||
|
|
||||||
// 创建用于读取输出的Future
|
|
||||||
Future<List<String>> outputFuture = executorService.submit(() -> readOutput(process.getInputStream()));
|
|
||||||
Future<List<String>> errorFuture = executorService.submit(() -> readOutput(process.getErrorStream()));
|
|
||||||
|
|
||||||
// 等待进程执行完成或超时
|
|
||||||
boolean completed = true;
|
|
||||||
if (config.getTimeout() != null && config.getTimeout() > 0) {
|
|
||||||
completed = process.waitFor(config.getTimeout(), TimeUnit.SECONDS);
|
|
||||||
if (!completed) {
|
|
||||||
process.destroyForcibly();
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
|
|
||||||
String.format("Shell execution timed out after %d seconds", config.getTimeout()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
process.waitFor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取输出结果
|
|
||||||
List<String> output = outputFuture.get(5, TimeUnit.SECONDS); // 给5秒时间读取输出
|
|
||||||
List<String> error = errorFuture.get(5, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
// 检查退出码
|
|
||||||
int exitCode = process.exitValue();
|
|
||||||
if (config.getSuccessExitCode() != null && exitCode != config.getSuccessExitCode()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
|
|
||||||
String.format("Shell execution failed with exit code: %d%nError output: %s",
|
|
||||||
exitCode, String.join("\n", error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置输出变量
|
|
||||||
Map<String, Object> outputVariables = new HashMap<>();
|
|
||||||
outputVariables.put("shellOutput", String.join("\n", output));
|
|
||||||
outputVariables.put("exitCode", exitCode);
|
|
||||||
variableOperations.setVariables(nodeInstance.getWorkflowInstance().getId(), outputVariables);
|
|
||||||
|
|
||||||
// 记录执行日志
|
|
||||||
context.log(String.format("Shell script executed successfully with exit code: %d", exitCode), LogLevelEnum.INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> readOutput(java.io.InputStream inputStream) throws Exception {
|
|
||||||
List<String> output = new ArrayList<>();
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
output.add(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现终止Shell进程的逻辑
|
|
||||||
context.log("Shell node termination is not implemented yet", LogLevelEnum.WARN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始节点执行器
|
|
||||||
* 负责工作流实例的初始化工作
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component("startNodeExecutor")
|
|
||||||
public class StartNodeExecutor extends AbstractNodeExecutor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NodeTypeEnum getNodeType() {
|
|
||||||
return NodeTypeEnum.START;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validate(String config) {
|
|
||||||
// 开始节点不需要配置,无需验证
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doExecute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
WorkflowInstance instance = context.getInstance();
|
|
||||||
|
|
||||||
// 记录启动日志
|
|
||||||
String message = String.format(
|
|
||||||
"工作流[%s]开始执行,实例ID: %d,业务键: %s",
|
|
||||||
instance.getWorkflowDefinition().getName(),
|
|
||||||
instance.getId(),
|
|
||||||
instance.getBusinessKey()
|
|
||||||
);
|
|
||||||
context.log(message, LogLevelEnum.INFO);
|
|
||||||
|
|
||||||
// 记录启动时间
|
|
||||||
log.info("Workflow instance {} started at {}", instance.getId(), instance.getCreateTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// 开始节点的终止意味着整个工作流的终止
|
|
||||||
WorkflowInstance instance = nodeInstance.getWorkflowInstance();
|
|
||||||
String message = String.format(
|
|
||||||
"工作流[%s]在启动阶段被终止,实例ID: %d",
|
|
||||||
instance.getWorkflowDefinition().getName(),
|
|
||||||
instance.getId()
|
|
||||||
);
|
|
||||||
context.log(message, LogLevelEnum.WARN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.task.TaskConfig;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class TaskNodeExecutor implements NodeExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NodeTypeEnum getNodeType() {
|
|
||||||
return NodeTypeEnum.TASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
// 1. 解析任务配置
|
|
||||||
TaskConfig config = parseConfig(nodeInstance.getConfig());
|
|
||||||
|
|
||||||
// 2. 执行具体任务
|
|
||||||
executeTask(config, nodeInstance, context);
|
|
||||||
|
|
||||||
// 3. 更新节点状态
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.COMPLETED);
|
|
||||||
context.log("任务节点执行完成", LogLevelEnum.INFO);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("任务节点执行失败", e);
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validate(String config) {
|
|
||||||
try {
|
|
||||||
TaskConfig taskConfig = parseConfig(config);
|
|
||||||
// 验证必填字段
|
|
||||||
if (taskConfig.getType() == null) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "任务类型不能为空");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// 终止任务执行
|
|
||||||
TaskConfig config = parseConfig(nodeInstance.getConfig());
|
|
||||||
terminateTask(config, nodeInstance, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TaskConfig parseConfig(String config) {
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(config, TaskConfig.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
switch (config.getType()) {
|
|
||||||
case HTTP:
|
|
||||||
executeHttpTask(config, nodeInstance, context);
|
|
||||||
break;
|
|
||||||
case JAVA:
|
|
||||||
executeJavaTask(config, nodeInstance, context);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_SUPPORTED, "不支持的任务类型: " + config.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void terminateTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// 根据任务类型执行终止操作
|
|
||||||
switch (config.getType()) {
|
|
||||||
case HTTP:
|
|
||||||
terminateHttpTask(config, nodeInstance, context);
|
|
||||||
break;
|
|
||||||
case JAVA:
|
|
||||||
terminateJavaTask(config, nodeInstance, context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现HTTP请求执行
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeJavaTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现Java方法调用
|
|
||||||
}
|
|
||||||
|
|
||||||
private void terminateHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现HTTP请求终止
|
|
||||||
}
|
|
||||||
|
|
||||||
private void terminateJavaTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现Java方法终止
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点配置基类
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略未知字段
|
|
||||||
@JsonSubTypes({
|
|
||||||
@JsonSubTypes.Type(value = ApprovalNodeExecutorConfig.class, name = "APPROVAL"),
|
|
||||||
@JsonSubTypes.Type(value = ScriptNodeExecutorConfig.class, name = "SCRIPT"),
|
|
||||||
@JsonSubTypes.Type(value = ShellNodeExecutorConfig.class, name = "SHELL"),
|
|
||||||
@JsonSubTypes.Type(value = JenkinsNodeExecutorConfig.class, name = "JENKINS"),
|
|
||||||
@JsonSubTypes.Type(value = GitNodeExecutorConfig.class, name = "GIT"),
|
|
||||||
@JsonSubTypes.Type(value = ConditionNodeExecutorConfig.class, name = "CONDITION"),
|
|
||||||
@JsonSubTypes.Type(value = ParallelNodeExecutorConfig.class, name = "PARALLEL"),
|
|
||||||
@JsonSubTypes.Type(value = NacosNodeExecutorConfig.class, name = "NACOS"),
|
|
||||||
@JsonSubTypes.Type(value = HttpNodeExecutorConfig.class, name = "HTTP"),
|
|
||||||
@JsonSubTypes.Type(value = NotifyNodeExecutorConfig.class, name = "NOTIFY")
|
|
||||||
})
|
|
||||||
public class NodeExecutorConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点ID
|
|
||||||
*/
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点名称
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点类型
|
|
||||||
*/
|
|
||||||
private NodeTypeEnum type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shell节点配置
|
|
||||||
* @deprecated 请使用 {@link ScriptNodeExecutorConfig} 替代,设置 language="shell"
|
|
||||||
*/
|
|
||||||
@Deprecated(since = "1.0", forRemoval = true)
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class ShellNodeExecutorConfig extends NodeExecutorConfig {
|
|
||||||
/**
|
|
||||||
* 执行器类型,固定为 SHELL
|
|
||||||
*/
|
|
||||||
private String executor = "SHELL";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shell脚本内容
|
|
||||||
*/
|
|
||||||
private String script;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作目录
|
|
||||||
*/
|
|
||||||
private String workingDirectory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 超时时间(秒)
|
|
||||||
*/
|
|
||||||
private Integer timeout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试次数
|
|
||||||
*/
|
|
||||||
private Integer retryTimes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试间隔(秒)
|
|
||||||
*/
|
|
||||||
private Integer retryInterval;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 环境变量
|
|
||||||
*/
|
|
||||||
private Map<String, String> environment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功退出码
|
|
||||||
*/
|
|
||||||
private Integer successExitCode = 0;
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.task;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.*;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class HttpTaskExecutor implements TaskExecutor {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(NodeInstance nodeInstance, WorkflowContextOperations context, Map<String, Object> parameters) {
|
|
||||||
String url = (String) parameters.get("url");
|
|
||||||
HttpMethod method = HttpMethod.valueOf((String) parameters.getOrDefault("method", "GET"));
|
|
||||||
Object body = parameters.get("body");
|
|
||||||
Map<String, String> headers = (Map<String, String>) parameters.get("headers");
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpHeaders httpHeaders = new HttpHeaders();
|
|
||||||
if (headers != null) {
|
|
||||||
headers.forEach(httpHeaders::add);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpEntity<?> requestEntity = new HttpEntity<>(body, httpHeaders);
|
|
||||||
ResponseEntity<String> response = restTemplate.exchange(url, method, requestEntity, String.class);
|
|
||||||
|
|
||||||
// 记录执行结果
|
|
||||||
nodeInstance.setOutput(objectMapper.createObjectNode()
|
|
||||||
.put("statusCode", response.getStatusCode().value())
|
|
||||||
.put("body", response.getBody())
|
|
||||||
.toString());
|
|
||||||
|
|
||||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED,
|
|
||||||
"HTTP request failed with status: " + response.getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
context.log("HTTP请求执行成功", response.getBody(), LogLevelEnum.INFO);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// HTTP请求无需终止操作
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.task;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class JavaTaskExecutor implements TaskExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ApplicationContext applicationContext;
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(NodeInstance nodeInstance, WorkflowContextOperations context, Map<String, Object> parameters) {
|
|
||||||
String className = parameters.get("className").toString();
|
|
||||||
String methodName = parameters.get("methodName").toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Class<?> clazz = Class.forName(className);
|
|
||||||
Object instance = applicationContext.getBean(clazz);
|
|
||||||
Method method = clazz.getMethod(methodName, NodeInstance.class, WorkflowContextOperations.class, Map.class);
|
|
||||||
method.invoke(instance, nodeInstance, context, parameters);
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Class not found: " + className);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, "Method not found: " + methodName);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// Java任务无法中断,记录日志
|
|
||||||
context.log("Java task cannot be terminated: " + nodeInstance.getNodeId(), LogLevelEnum.WARN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.task;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.exec.*;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class ShellTaskExecutor implements TaskExecutor {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(NodeInstance nodeInstance, WorkflowContextOperations context, Map<String, Object> parameters) {
|
|
||||||
String command = (String) parameters.get("command");
|
|
||||||
Integer timeout = (Integer) parameters.getOrDefault("timeout", 300);
|
|
||||||
|
|
||||||
CommandLine cmdLine = CommandLine.parse(command);
|
|
||||||
DefaultExecutor executor = new DefaultExecutor();
|
|
||||||
executor.setExitValues(null);
|
|
||||||
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
||||||
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
|
|
||||||
executor.setStreamHandler(new PumpStreamHandler(outputStream, errorStream));
|
|
||||||
|
|
||||||
ExecuteWatchdog watchdog = new ExecuteWatchdog(TimeUnit.SECONDS.toMillis(timeout));
|
|
||||||
executor.setWatchdog(watchdog);
|
|
||||||
|
|
||||||
try {
|
|
||||||
int exitValue = executor.execute(cmdLine);
|
|
||||||
String output = outputStream.toString();
|
|
||||||
String error = errorStream.toString();
|
|
||||||
|
|
||||||
// 记录执行结果
|
|
||||||
nodeInstance.setOutput(objectMapper.createObjectNode()
|
|
||||||
.put("exitValue", exitValue)
|
|
||||||
.put("output", output)
|
|
||||||
.put("error", error)
|
|
||||||
.toString());
|
|
||||||
|
|
||||||
if (exitValue != 0) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.log("Shell命令执行成功", output, LogLevelEnum.INFO);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate(NodeInstance nodeInstance, WorkflowContextOperations context) {
|
|
||||||
// TODO: 实现Shell命令终止逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.task;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.TaskTypeEnum;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class TaskConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务类型
|
|
||||||
*/
|
|
||||||
private TaskTypeEnum type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务名称
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务超时时间(秒)
|
|
||||||
*/
|
|
||||||
private Integer timeout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试次数
|
|
||||||
*/
|
|
||||||
private Integer retryCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试间隔(秒)
|
|
||||||
*/
|
|
||||||
private Integer retryInterval;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务参数
|
|
||||||
*/
|
|
||||||
private Map<String, Object> parameters;
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.task;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务执行器接口
|
|
||||||
*/
|
|
||||||
public interface TaskExecutor {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行任务
|
|
||||||
*/
|
|
||||||
void execute(NodeInstance nodeInstance, WorkflowContextOperations context, Map<String, Object> parameters);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 终止任务
|
|
||||||
*/
|
|
||||||
void terminate(NodeInstance nodeInstance, WorkflowContextOperations context);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.dto;
|
package com.qqchen.deploy.backend.workflow.engine.model;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -6,15 +6,15 @@ import lombok.Data;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点配置DTO
|
* 节点配置
|
||||||
* 用于前端传递节点配置信息
|
* 用于解析工作流定义中的节点配置JSON
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class NodeConfigDTO {
|
public class NodeConfig {
|
||||||
/**
|
/**
|
||||||
* 节点ID
|
* 节点ID
|
||||||
*/
|
*/
|
||||||
private String id;
|
private String nodeId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型
|
* 节点类型
|
||||||
@ -27,12 +27,7 @@ public class NodeConfigDTO {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点配置
|
* 节点配置,不同类型的节点有不同的配置项
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> config;
|
private Map<String, Object> config;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 节点描述
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流转配置
|
||||||
|
* 用于解析工作流定义中的流转配置JSON
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TransitionConfig {
|
||||||
|
/**
|
||||||
|
* 来源节点ID
|
||||||
|
*/
|
||||||
|
private String from;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标节点ID
|
||||||
|
*/
|
||||||
|
private String to;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流转条件
|
||||||
|
*/
|
||||||
|
private String condition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流转描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级,数字越小优先级越高
|
||||||
|
*/
|
||||||
|
private Integer priority;
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.engine.model;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class WorkflowGraph {
|
||||||
|
|
||||||
|
private List<NodeConfig> nodes;
|
||||||
|
|
||||||
|
private List<TransitionConfig> transitions;
|
||||||
|
|
||||||
|
public NodeConfig getStartNode() {
|
||||||
|
return nodes.stream()
|
||||||
|
.filter(node -> node.getType() == NodeTypeEnum.START)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NodeConfig> getNextNodes(String nodeId) {
|
||||||
|
return transitions.stream()
|
||||||
|
.filter(t -> t.getFrom().equals(nodeId))
|
||||||
|
.map(t -> findNodeById(t.getTo()))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeConfig findNodeById(String nodeId) {
|
||||||
|
return nodes.stream()
|
||||||
|
.filter(n -> n.getNodeId().equals(nodeId))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,15 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.parser;
|
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.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeConfig;
|
import com.qqchen.deploy.backend.workflow.engine.model.NodeConfig;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.TransitionConfig;
|
import com.qqchen.deploy.backend.workflow.engine.model.TransitionConfig;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.WorkflowGraph;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -20,149 +20,59 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* 工作流定义解析器
|
* 工作流定义解析器
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
@Component
|
@Component
|
||||||
|
@Slf4j
|
||||||
public class WorkflowDefinitionParser {
|
public class WorkflowDefinitionParser {
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Resource
|
public WorkflowDefinitionParser(ObjectMapper objectMapper) {
|
||||||
private ObjectMapper objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
public WorkflowGraph parse(WorkflowDefinition definition) {
|
||||||
* 解析节点配置
|
|
||||||
*
|
|
||||||
* @param nodeConfig 节点配置JSON字符串
|
|
||||||
* @return 节点配置列表
|
|
||||||
*/
|
|
||||||
public List<NodeConfig> parseNodeConfig(String nodeConfig) {
|
|
||||||
try {
|
try {
|
||||||
log.debug("Parsing node config: {}", nodeConfig);
|
// 解析节点配置
|
||||||
JsonNode rootNode = objectMapper.readTree(nodeConfig);
|
JsonNode rootNode = objectMapper.readTree(definition.getNodeConfig());
|
||||||
JsonNode nodesNode = rootNode.get("nodes");
|
List<NodeConfig> nodes = parseNodes(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) {
|
JsonNode transitionRoot = objectMapper.readTree(definition.getTransitionConfig());
|
||||||
NodeConfig config = new NodeConfig();
|
List<TransitionConfig> transitions = parseTransitions(transitionRoot.get("transitions"));
|
||||||
// 将前端的id映射到nodeId
|
|
||||||
config.setNodeId(node.get("id").asText());
|
return WorkflowGraph.builder()
|
||||||
config.setName(node.get("name").asText());
|
.nodes(nodes)
|
||||||
config.setType(NodeTypeEnum.valueOf(node.get("type").asText()));
|
.transitions(transitions)
|
||||||
|
.build();
|
||||||
// 解析节点配置
|
|
||||||
if (node.has("config")) {
|
} catch (Exception e) {
|
||||||
config.setConfig(objectMapper.convertValue(node.get("config"),
|
log.error("Failed to parse workflow definition", e);
|
||||||
new TypeReference<Map<String, Object>>() {}));
|
throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_ERROR);
|
||||||
}
|
|
||||||
|
|
||||||
// 可选字段
|
|
||||||
if (node.has("description")) {
|
|
||||||
config.setDescription(node.get("description").asText());
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.add(config);
|
|
||||||
log.debug("Parsed node: id={}, type={}", config.getNodeId(), config.getType());
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
log.error("Failed to parse node config: {}", e.getMessage(), e);
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_INVALID, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private List<NodeConfig> parseNodes(JsonNode nodesNode) {
|
||||||
* 解析流转配置
|
List<NodeConfig> nodes = new ArrayList<>();
|
||||||
*
|
for (JsonNode node : nodesNode) {
|
||||||
* @param transitionConfig 流转配置JSON字符串
|
NodeConfig config = new NodeConfig();
|
||||||
* @return 流转配置列表
|
config.setNodeId(node.get("id").asText());
|
||||||
*/
|
config.setType(NodeTypeEnum.valueOf(node.get("type").asText()));
|
||||||
public List<TransitionConfig> parseTransitionConfig(String transitionConfig) {
|
config.setName(node.get("name").asText());
|
||||||
try {
|
config.setConfig(objectMapper.convertValue(node.get("config"), new TypeReference<Map<String, Object>>() {}));
|
||||||
log.debug("Parsing transition config: {}", transitionConfig);
|
nodes.add(config);
|
||||||
JsonNode rootNode = objectMapper.readTree(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 = 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.getFrom(),
|
|
||||||
config.getTo(),
|
|
||||||
config.getPriority());
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
log.error("Failed to parse transition config: {}", e.getMessage(), e);
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID, e);
|
|
||||||
}
|
}
|
||||||
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private List<TransitionConfig> parseTransitions(JsonNode transitionsNode) {
|
||||||
* 验证节点配置的完整性
|
List<TransitionConfig> transitions = new ArrayList<>();
|
||||||
*
|
for (JsonNode transition : transitionsNode) {
|
||||||
* @param nodes 节点配置列表
|
TransitionConfig config = new TransitionConfig();
|
||||||
* @param transitions 流转配置列表
|
config.setFrom(transition.get("from").asText());
|
||||||
*/
|
config.setTo(transition.get("to").asText());
|
||||||
public void validateConfig(List<NodeConfig> nodes, List<TransitionConfig> transitions) {
|
config.setCondition(transition.get("condition").asText());
|
||||||
// 1. 检查是否有开始节点和结束节点
|
config.setPriority(transition.get("priority").asInt());
|
||||||
boolean hasStart = false;
|
transitions.add(config);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasStart || !hasEnd) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID,
|
|
||||||
"工作流必须包含开始节点和结束节点");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查流转配置的完整性
|
|
||||||
for (TransitionConfig transition : transitions) {
|
|
||||||
boolean sourceExists = false;
|
|
||||||
boolean targetExists = false;
|
|
||||||
|
|
||||||
for (NodeConfig node : nodes) {
|
|
||||||
if (node.getNodeId().equals(transition.getFrom())) {
|
|
||||||
sourceExists = true;
|
|
||||||
}
|
|
||||||
if (node.getNodeId().equals(transition.getTo())) {
|
|
||||||
targetExists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourceExists || !targetExists) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_CONFIG_INVALID,
|
|
||||||
String.format("流转配置中的节点不存在: %s -> %s",
|
|
||||||
transition.getFrom(),
|
|
||||||
transition.getTo()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return transitions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command;
|
package com.qqchen.deploy.backend.workflow.engine.script.command;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.executor.config.ScriptNodeExecutorConfig;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeExecutorConfig;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command;
|
package com.qqchen.deploy.backend.workflow.engine.script.command;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
||||||
import java.lang.annotation.*;
|
import java.lang.annotation.*;
|
||||||
@ -1,10 +1,10 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.impl;
|
package com.qqchen.deploy.backend.workflow.engine.script.command.impl;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeExecutorConfig;
|
import com.qqchen.deploy.backend.workflow.engine.executor.config.ScriptNodeExecutorConfig;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptCommand;
|
import com.qqchen.deploy.backend.workflow.engine.script.command.ScriptCommand;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptLanguageSupport;
|
import com.qqchen.deploy.backend.workflow.engine.script.command.ScriptLanguageSupport;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -1,10 +1,10 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.impl;
|
package com.qqchen.deploy.backend.workflow.engine.script.command.impl;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.config.ScriptNodeExecutorConfig;
|
import com.qqchen.deploy.backend.workflow.engine.executor.config.ScriptNodeExecutorConfig;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptCommand;
|
import com.qqchen.deploy.backend.workflow.engine.script.command.ScriptCommand;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptLanguageSupport;
|
import com.qqchen.deploy.backend.workflow.engine.script.command.ScriptLanguageSupport;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor.node.script.registry;
|
package com.qqchen.deploy.backend.workflow.engine.script.registry;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
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.script.command.ScriptCommand;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.script.command.ScriptLanguageSupport;
|
import com.qqchen.deploy.backend.workflow.engine.script.command.ScriptLanguageSupport;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
import com.qqchen.deploy.backend.workflow.enums.ScriptLanguageEnum;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.transition;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class TransitionExecutor {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private TransitionRuleEngine transitionRuleEngine;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private INodeInstanceRepository nodeInstanceRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行节点流转
|
|
||||||
*/
|
|
||||||
public void executeTransition(NodeInstance currentNode, WorkflowDefinition definition, WorkflowContextOperations context) {
|
|
||||||
// 1. 获取下一个节点ID列表
|
|
||||||
List<String> nextNodeIds = transitionRuleEngine.getNextNodeIds(currentNode, definition, context);
|
|
||||||
|
|
||||||
//这里应该从WorkflowDefinition类的transitionConfig获取
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.transition;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class TransitionRule {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 源节点ID
|
|
||||||
*/
|
|
||||||
private String from;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 目标节点ID
|
|
||||||
*/
|
|
||||||
private String to;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 条件表达式
|
|
||||||
*/
|
|
||||||
private String condition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 优先级
|
|
||||||
*/
|
|
||||||
private Integer priority;
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.transition;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContextOperations;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.expression.Expression;
|
|
||||||
import org.springframework.expression.ExpressionParser;
|
|
||||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
|
||||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class TransitionRuleEngine {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取下一个节点ID列表
|
|
||||||
*/
|
|
||||||
public List<String> getNextNodeIds(NodeInstance currentNode, WorkflowDefinition definition, WorkflowContextOperations context) {
|
|
||||||
try {
|
|
||||||
// 解析流转规则
|
|
||||||
List<TransitionRule> rules = parseTransitionRules(definition.getTransitionConfig());
|
|
||||||
|
|
||||||
// 过滤当前节点的规则并按优先级排序
|
|
||||||
List<TransitionRule> nodeRules = rules.stream()
|
|
||||||
.filter(rule -> rule.getFrom().equals(currentNode.getNodeId()))
|
|
||||||
.sorted(Comparator.comparing(TransitionRule::getPriority))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// 执行规则匹配
|
|
||||||
List<String> nextNodeIds = new ArrayList<>();
|
|
||||||
for (TransitionRule rule : nodeRules) {
|
|
||||||
if (evaluateCondition(rule.getCondition(), context)) {
|
|
||||||
nextNodeIds.add(rule.getTo());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextNodeIds;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_EXECUTION_FAILED, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<TransitionRule> parseTransitionRules(String config) {
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue(config, new TypeReference<List<TransitionRule>>() {});
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean evaluateCondition(String condition, WorkflowContextOperations context) {
|
|
||||||
if (condition == null || condition.trim().isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Expression expression = expressionParser.parseExpression(condition);
|
|
||||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
|
|
||||||
evaluationContext.setVariables(context.getVariables());
|
|
||||||
return expression.getValue(evaluationContext, Boolean.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NODE_CONFIG_ERROR, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
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<Long> {
|
|
||||||
/**
|
|
||||||
* 节点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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所属工作流定义ID
|
|
||||||
*/
|
|
||||||
@NotNull(message = "工作流定义ID不能为空")
|
|
||||||
@Column(name = "workflow_definition_id", nullable = false)
|
|
||||||
private Long workflowDefinitionId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点配置,不同类型的节点有不同的配置
|
|
||||||
* TASK节点:
|
|
||||||
* - type: SHELL/HTTP/JAVA
|
|
||||||
* - config: 具体的任务配置
|
|
||||||
* GATEWAY节点:
|
|
||||||
* - type: EXCLUSIVE/PARALLEL/INCLUSIVE
|
|
||||||
* - conditions: 分支条件配置
|
|
||||||
*/
|
|
||||||
@JdbcTypeCode(SqlTypes.JSON)
|
|
||||||
@Column(columnDefinition = "json")
|
|
||||||
private Map<String, Object> config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点描述,用于说明节点的用途
|
|
||||||
*/
|
|
||||||
@Column(columnDefinition = "text")
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查节点配置是否有效
|
|
||||||
*
|
|
||||||
* @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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,7 +6,7 @@ import com.qqchen.deploy.backend.framework.domain.Entity;
|
|||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.framework.utils.SpringUtils;
|
import com.qqchen.deploy.backend.framework.utils.SpringUtils;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.config.NodeExecutorConfig;
|
import com.qqchen.deploy.backend.workflow.engine.executor.config.NodeExecutorConfig;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
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;
|
|
||||||
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<Long> {
|
|
||||||
/**
|
|
||||||
* 源节点ID
|
|
||||||
*/
|
|
||||||
@NotBlank(message = "源节点ID不能为空")
|
|
||||||
@Column(name = "`from`", nullable = false)
|
|
||||||
private String from;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 目标节点ID
|
|
||||||
*/
|
|
||||||
@NotBlank(message = "目标节点ID不能为空")
|
|
||||||
@Column(name = "`to`", nullable = false)
|
|
||||||
private String to;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 流转条件,使用SpEL表达式
|
|
||||||
* 为空表示无条件流转
|
|
||||||
* 示例:
|
|
||||||
* - "${status == 'SUCCESS'}"
|
|
||||||
* - "${amount > 1000}"
|
|
||||||
* - "${result.code == 200 && result.data != null}"
|
|
||||||
*/
|
|
||||||
@Column(name = "`condition`", columnDefinition = "text")
|
|
||||||
private String condition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 优先级,数字越小优先级越高
|
|
||||||
* 用于控制多个出向流转的执行顺序
|
|
||||||
* 默认为0
|
|
||||||
*/
|
|
||||||
@NotNull(message = "优先级不能为空")
|
|
||||||
@Column(nullable = false)
|
|
||||||
private Integer priority = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所属工作流定义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 (from == null || from.trim().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (to == null || to.trim().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (from.equals(to)) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.monitor;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.Counter;
|
|
||||||
import io.micrometer.core.instrument.Gauge;
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
|
||||||
import io.micrometer.core.instrument.Timer;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.DoubleAdder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流上下文监控
|
|
||||||
* 负责收集和记录工作流变量操作的各项指标
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class WorkflowContextMonitor {
|
|
||||||
|
|
||||||
private final MeterRegistry meterRegistry;
|
|
||||||
private final Counter variableSetCounter;
|
|
||||||
private final Counter variableGetCounter;
|
|
||||||
private final Counter cacheHitCounter;
|
|
||||||
private final Counter cacheMissCounter;
|
|
||||||
private final Timer variableOperationTimer;
|
|
||||||
private final Timer transactionTimer;
|
|
||||||
private final Gauge contextCacheSize;
|
|
||||||
private final DoubleAdder cacheSize = new DoubleAdder();
|
|
||||||
|
|
||||||
public WorkflowContextMonitor(MeterRegistry registry) {
|
|
||||||
this.meterRegistry = registry;
|
|
||||||
// 变量操作计数器
|
|
||||||
this.variableSetCounter = Counter.builder("workflow.variable.operations")
|
|
||||||
.tag("type", "set")
|
|
||||||
.description("Number of variable set operations")
|
|
||||||
.register(registry);
|
|
||||||
|
|
||||||
this.variableGetCounter = Counter.builder("workflow.variable.operations")
|
|
||||||
.tag("type", "get")
|
|
||||||
.description("Number of variable get operations")
|
|
||||||
.register(registry);
|
|
||||||
|
|
||||||
// 缓存命中计数器
|
|
||||||
this.cacheHitCounter = Counter.builder("workflow.context.cache")
|
|
||||||
.tag("result", "hit")
|
|
||||||
.description("Number of context cache hits")
|
|
||||||
.register(registry);
|
|
||||||
|
|
||||||
this.cacheMissCounter = Counter.builder("workflow.context.cache")
|
|
||||||
.tag("result", "miss")
|
|
||||||
.description("Number of context cache misses")
|
|
||||||
.register(registry);
|
|
||||||
|
|
||||||
// 操作耗时计时器
|
|
||||||
this.variableOperationTimer = Timer.builder("workflow.variable.operation.duration")
|
|
||||||
.description("Time taken for variable operations")
|
|
||||||
.publishPercentiles(0.5, 0.95, 0.99)
|
|
||||||
.register(registry);
|
|
||||||
|
|
||||||
this.transactionTimer = Timer.builder("workflow.variable.transaction.duration")
|
|
||||||
.description("Time taken for variable transactions")
|
|
||||||
.publishPercentiles(0.5, 0.95, 0.99)
|
|
||||||
.register(registry);
|
|
||||||
|
|
||||||
// 缓存大小测量
|
|
||||||
this.contextCacheSize = Gauge.builder("workflow.context.cache.size",
|
|
||||||
cacheSize::doubleValue) // 使用 DoubleAdder 来存储和获取值
|
|
||||||
.description("Current size of workflow context cache")
|
|
||||||
.register(registry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录变量设置操作
|
|
||||||
*/
|
|
||||||
public void recordVariableSet() {
|
|
||||||
variableSetCounter.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录变量获取操作
|
|
||||||
*/
|
|
||||||
public void recordVariableGet() {
|
|
||||||
variableGetCounter.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录缓存命中
|
|
||||||
*/
|
|
||||||
public void recordCacheHit() {
|
|
||||||
cacheHitCounter.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录缓存未命中
|
|
||||||
*/
|
|
||||||
public void recordCacheMiss() {
|
|
||||||
cacheMissCounter.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录操作耗时
|
|
||||||
*/
|
|
||||||
public Timer.Sample startOperation() {
|
|
||||||
return Timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止操作计时
|
|
||||||
*/
|
|
||||||
public void stopOperation(Timer.Sample sample) {
|
|
||||||
sample.stop(variableOperationTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录事务耗时
|
|
||||||
*/
|
|
||||||
public Timer.Sample startTransaction() {
|
|
||||||
return Timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止事务计时
|
|
||||||
*/
|
|
||||||
public void stopTransaction(Timer.Sample sample) {
|
|
||||||
sample.stop(transactionTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新缓存大小
|
|
||||||
*/
|
|
||||||
public void updateCacheSize(long size) {
|
|
||||||
cacheSize.reset();
|
|
||||||
cacheSize.add(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录错误
|
|
||||||
*/
|
|
||||||
public void recordError(String operation, Exception e) {
|
|
||||||
log.error("Workflow variable operation error. Operation: {}, Error: {}",
|
|
||||||
operation, e.getMessage(), e);
|
|
||||||
Counter.builder("workflow.variable.errors")
|
|
||||||
.tag("operation", operation)
|
|
||||||
.tag("error", e.getClass().getSimpleName())
|
|
||||||
.register(meterRegistry)
|
|
||||||
.increment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -2,11 +2,8 @@ package com.qqchen.deploy.backend.workflow.service;
|
|||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
import com.qqchen.deploy.backend.framework.service.IBaseService;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.NodeTypeDTO;
|
import com.qqchen.deploy.backend.workflow.dto.NodeTypeDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.definition.TaskExecutorDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeType;
|
import com.qqchen.deploy.backend.workflow.entity.NodeType;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型服务接口
|
* 节点类型服务接口
|
||||||
*/
|
*/
|
||||||
@ -20,22 +17,6 @@ public interface INodeTypeService extends IBaseService<NodeType, NodeTypeDTO, Lo
|
|||||||
*/
|
*/
|
||||||
NodeTypeDTO findByCode(String code);
|
NodeTypeDTO findByCode(String code);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定节点类型支持的执行器列表
|
|
||||||
* 说明:
|
|
||||||
* 1. 只有TASK类型的节点才有执行器列表
|
|
||||||
* 2. 一个任务节点可以支持多种执行器
|
|
||||||
* 3. 每个执行器都有自己的配置模式
|
|
||||||
*
|
|
||||||
* 使用场景:
|
|
||||||
* 1. 流程设计时,选择节点类型后,需要展示该节点支持的执行器列表
|
|
||||||
* 2. 根据选择的执行器,动态渲染配置表单
|
|
||||||
* 3. 保存节点配置时,验证执行器参数是否符合配置模式
|
|
||||||
*
|
|
||||||
* @param code 节点类型编码
|
|
||||||
* @return 执行器列表
|
|
||||||
*/
|
|
||||||
List<TaskExecutorDefinition> getExecutors(String code);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启用节点类型
|
* 启用节点类型
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.request.WorkflowStartRequest;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流执行引擎接口
|
||||||
|
*/
|
||||||
|
public interface IWorkflowEngineService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动工作流实例
|
||||||
|
*/
|
||||||
|
WorkflowInstance startWorkflow(WorkflowStartRequest request);
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.service;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作流变量操作接口
|
|
||||||
*/
|
|
||||||
public interface WorkflowVariableOperations {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取工作流实例的所有变量
|
|
||||||
*
|
|
||||||
* @param workflowInstanceId 工作流实例ID
|
|
||||||
* @return 变量Map
|
|
||||||
*/
|
|
||||||
Map<String, Object> getVariables(Long workflowInstanceId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置工作流实例的变量
|
|
||||||
*
|
|
||||||
* @param workflowInstanceId 工作流实例ID
|
|
||||||
* @param variables 变量Map
|
|
||||||
*/
|
|
||||||
void setVariables(Long workflowInstanceId, Map<String, Object> variables);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取工作流实例的指定变量
|
|
||||||
*
|
|
||||||
* @param workflowInstanceId 工作流实例ID
|
|
||||||
* @param key 变量键
|
|
||||||
* @return 变量值
|
|
||||||
*/
|
|
||||||
Object getVariable(Long workflowInstanceId, String key);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置工作流实例的指定变量
|
|
||||||
*
|
|
||||||
* @param workflowInstanceId 工作流实例ID
|
|
||||||
* @param key 变量键
|
|
||||||
* @param value 变量值
|
|
||||||
*/
|
|
||||||
void setVariable(Long workflowInstanceId, String key, Object value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除实例的所有变量
|
|
||||||
*
|
|
||||||
* @param workflowInstanceId 工作流实例ID
|
|
||||||
*/
|
|
||||||
void clearVariables(Long workflowInstanceId);
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.service.impl;
|
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 并发安全的工作流变量操作实现
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class ConcurrentWorkflowVariableOperations implements WorkflowVariableOperations {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private IWorkflowVariableService variableService;
|
|
||||||
|
|
||||||
private final Cache<Long, Map<String, Object>> variableCache;
|
|
||||||
|
|
||||||
public ConcurrentWorkflowVariableOperations() {
|
|
||||||
this.variableCache = Caffeine.newBuilder()
|
|
||||||
.expireAfterWrite(30, TimeUnit.MINUTES)
|
|
||||||
.maximumSize(10000)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> getVariables(Long workflowInstanceId) {
|
|
||||||
return variableCache.get(workflowInstanceId,
|
|
||||||
id -> variableService.getVariables(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setVariables(Long workflowInstanceId, Map<String, Object> variables) {
|
|
||||||
if (variables == null || variables.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
variableService.setVariables(workflowInstanceId, variables);
|
|
||||||
variableCache.put(workflowInstanceId, variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getVariable(Long workflowInstanceId, String key) {
|
|
||||||
Map<String, Object> variables = getVariables(workflowInstanceId);
|
|
||||||
return variables != null ? variables.get(key) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setVariable(Long workflowInstanceId, String key, Object value) {
|
|
||||||
Map<String, Object> variables = getVariables(workflowInstanceId);
|
|
||||||
if (variables != null) {
|
|
||||||
variables.put(key, value);
|
|
||||||
setVariables(workflowInstanceId, variables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearVariables(Long workflowInstanceId) {
|
|
||||||
variableService.clearVariables(workflowInstanceId);
|
|
||||||
variableCache.invalidate(workflowInstanceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,18 +6,13 @@ import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
|||||||
import com.qqchen.deploy.backend.workflow.converter.JsonConverter;
|
import com.qqchen.deploy.backend.workflow.converter.JsonConverter;
|
||||||
import com.qqchen.deploy.backend.workflow.converter.NodeTypeConverter;
|
import com.qqchen.deploy.backend.workflow.converter.NodeTypeConverter;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.NodeTypeDTO;
|
import com.qqchen.deploy.backend.workflow.dto.NodeTypeDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.engine.definition.TaskExecutorDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeType;
|
import com.qqchen.deploy.backend.workflow.entity.NodeType;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeCategoryEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.INodeTypeRepository;
|
import com.qqchen.deploy.backend.workflow.repository.INodeTypeRepository;
|
||||||
import com.qqchen.deploy.backend.workflow.service.INodeTypeService;
|
import com.qqchen.deploy.backend.workflow.service.INodeTypeService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点类型服务实现类
|
* 节点类型服务实现类
|
||||||
*/
|
*/
|
||||||
@ -40,27 +35,6 @@ public class NodeTypeServiceImpl extends BaseServiceImpl<NodeType, NodeTypeDTO,
|
|||||||
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<TaskExecutorDefinition> getExecutors(String code) {
|
|
||||||
// 1. 查询节点类型
|
|
||||||
NodeType nodeType = nodeTypeRepository.findByCodeAndDeletedFalse(code)
|
|
||||||
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NODE_TYPE_NOT_FOUND));
|
|
||||||
|
|
||||||
// 2. 检查节点类型是否为任务节点
|
|
||||||
if (!NodeCategoryEnum.TASK.equals(nodeType.getCategory())) {
|
|
||||||
// 非任务节点没有执行器列表
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 检查节点类型是否启用
|
|
||||||
if (!nodeType.getEnabled()) {
|
|
||||||
throw new BusinessException(ResponseCode.WORKFLOW_NODE_TYPE_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 将JSON字符串转换为执行器列表
|
|
||||||
return jsonConverter.toExecutorList(nodeType.getExecutors());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void enable(Long id) {
|
public void enable(Long id) {
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.qqchen.deploy.backend.workflow.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.request.WorkflowStartRequest;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.executor.INodeExecutor;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.NodeConfig;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.model.WorkflowGraph;
|
||||||
|
import com.qqchen.deploy.backend.workflow.engine.parser.WorkflowDefinitionParser;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
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.INodeInstanceRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowEngineService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class WorkflowEngineServiceImpl implements IWorkflowEngineService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowInstanceRepository workflowInstanceRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private INodeInstanceRepository nodeInstanceRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
@Lazy
|
||||||
|
private Map<NodeTypeEnum, INodeExecutor> nodeExecutors;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WorkflowDefinitionParser workflowDefinitionParser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public WorkflowInstance startWorkflow(WorkflowStartRequest request) {
|
||||||
|
// 1. 获取工作流定义
|
||||||
|
WorkflowDefinition definition = workflowDefinitionRepository.findByCodeAndDeletedFalse(request.getWorkflowCode());
|
||||||
|
if (definition == null) {
|
||||||
|
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查工作流定义状态
|
||||||
|
if (definition.getStatus() != WorkflowDefinitionStatusEnum.PUBLISHED) {
|
||||||
|
throw new WorkflowEngineException(ResponseCode.WORKFLOW_NOT_PUBLISHED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建工作流实例
|
||||||
|
WorkflowInstance workflowInstance = new WorkflowInstance();
|
||||||
|
workflowInstance.setWorkflowDefinition(definition);
|
||||||
|
workflowInstance.setBusinessKey(request.getBusinessKey());
|
||||||
|
workflowInstance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
||||||
|
workflowInstance.setCreateTime(LocalDateTime.now());
|
||||||
|
workflowInstanceRepository.save(workflowInstance);
|
||||||
|
|
||||||
|
// 5. 解析工作流配置
|
||||||
|
WorkflowGraph graph = workflowDefinitionParser.parse(definition);
|
||||||
|
|
||||||
|
// 6. 获取并执行开始节点
|
||||||
|
NodeConfig startNode = graph.getStartNode();
|
||||||
|
INodeExecutor executor = nodeExecutors.get(startNode.getType());
|
||||||
|
executor.execute(workflowInstance, graph, startNode);
|
||||||
|
return workflowInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -11,7 +11,6 @@ import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnum;
|
|||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
|
||||||
import com.qqchen.deploy.backend.workflow.service.WorkflowVariableOperations;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -33,8 +32,6 @@ public class WorkflowInstanceServiceImpl extends BaseServiceImpl<WorkflowInstanc
|
|||||||
@Resource
|
@Resource
|
||||||
private IWorkflowInstanceRepository workflowInstanceRepository;
|
private IWorkflowInstanceRepository workflowInstanceRepository;
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowVariableOperations variableOperations;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@ -58,11 +55,6 @@ public class WorkflowInstanceServiceImpl extends BaseServiceImpl<WorkflowInstanc
|
|||||||
instance.setStatus(WorkflowInstanceStatusEnum.CREATED);
|
instance.setStatus(WorkflowInstanceStatusEnum.CREATED);
|
||||||
final WorkflowInstance savedInstance = workflowInstanceRepository.save(instance);
|
final WorkflowInstance savedInstance = workflowInstanceRepository.save(instance);
|
||||||
|
|
||||||
// 4. 设置初始变量
|
|
||||||
if (variables != null && !variables.isEmpty()) {
|
|
||||||
variableOperations.setVariables(savedInstance.getId(), variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
return converter.toDto(savedInstance);
|
return converter.toDto(savedInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://localhost:3306/deploy-ease-platform?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
|
url: jdbc:mysql://localhost:3306/deploy-ease-platform?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
|
||||||
username: root
|
username: root
|
||||||
password: ServBay.dev
|
password: root
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
|
|||||||
@ -405,26 +405,6 @@ CREATE TABLE wf_workflow_definition (
|
|||||||
CONSTRAINT UK_workflow_definition_code_version UNIQUE (code, version_no)
|
CONSTRAINT UK_workflow_definition_code_version UNIQUE (code, version_no)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流定义表';
|
) 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 (
|
CREATE TABLE wf_workflow_instance (
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.api;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.service.IExternalSystemService;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
@AutoConfigureMockMvc
|
|
||||||
@WithMockUser(username = "admin", roles = {"ADMIN"})
|
|
||||||
class ExternalSystemApiControllerTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private IExternalSystemService externalSystemService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConnection_WhenSuccess_ShouldReturnTrue() throws Exception {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemService.testConnection(1L)).thenReturn(true);
|
|
||||||
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(get("/api/v1/external-system/1/test-connection"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200))
|
|
||||||
.andExpect(jsonPath("$.data").value(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConnection_WhenFailed_ShouldReturnFalse() throws Exception {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemService.testConnection(1L)).thenReturn(false);
|
|
||||||
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(get("/api/v1/external-system/1/test-connection"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200))
|
|
||||||
.andExpect(jsonPath("$.data").value(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void syncData_ShouldReturnSuccess() throws Exception {
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(post("/api/v1/external-system/1/sync"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateStatus_ShouldReturnSuccess() throws Exception {
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(put("/api/v1/external-system/1/status")
|
|
||||||
.param("enabled", "false"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.api;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.service.ITenantService;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
|
|
||||||
import static org.mockito.Mockito.doNothing;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 租户API控制器测试类
|
|
||||||
*
|
|
||||||
* @author QQChen
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
@AutoConfigureMockMvc
|
|
||||||
@WithMockUser(username = "admin", roles = {"ADMIN"})
|
|
||||||
class TenantApiControllerTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private ITenantService tenantService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getStatus_WhenEnabled_ShouldReturnTrue() throws Exception {
|
|
||||||
// Mock
|
|
||||||
when(tenantService.getStatus(1L)).thenReturn(true);
|
|
||||||
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(get("/api/v1/tenant/1/enabled"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200))
|
|
||||||
.andExpect(jsonPath("$.success").value(true))
|
|
||||||
.andExpect(jsonPath("$.data").value(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getStatus_WhenDisabled_ShouldReturnFalse() throws Exception {
|
|
||||||
// Mock
|
|
||||||
when(tenantService.getStatus(1L)).thenReturn(false);
|
|
||||||
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(get("/api/v1/tenant/1/enabled"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200))
|
|
||||||
.andExpect(jsonPath("$.success").value(true))
|
|
||||||
.andExpect(jsonPath("$.data").value(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateStatus_WhenEnabled_ShouldReturnSuccess() throws Exception {
|
|
||||||
// Mock
|
|
||||||
doNothing().when(tenantService).updateStatus(1L, true);
|
|
||||||
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(put("/api/v1/tenant/1/enabled")
|
|
||||||
.param("enabled", "true"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200))
|
|
||||||
.andExpect(jsonPath("$.success").value(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateStatus_WhenDisabled_ShouldReturnSuccess() throws Exception {
|
|
||||||
// Mock
|
|
||||||
doNothing().when(tenantService).updateStatus(1L, false);
|
|
||||||
|
|
||||||
// 执行并验证
|
|
||||||
mockMvc.perform(put("/api/v1/tenant/1/enabled")
|
|
||||||
.param("enabled", "false"))
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.code").value(200))
|
|
||||||
.andExpect(jsonPath("$.success").value(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.service.impl;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.entity.ExternalSystem;
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
|
||||||
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.ExternalSystemAuthTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.ExternalSystemSyncStatusEnum;
|
|
||||||
import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum;
|
|
||||||
import com.qqchen.deploy.backend.system.model.ExternalSystemDTO;
|
|
||||||
import com.qqchen.deploy.backend.system.repository.IExternalSystemRepository;
|
|
||||||
import com.qqchen.deploy.backend.system.service.impl.ExternalSystemServiceImpl;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class ExternalSystemServiceImplTest {
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private IExternalSystemRepository externalSystemRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ExternalSystemServiceImpl externalSystemService;
|
|
||||||
|
|
||||||
private ExternalSystem system;
|
|
||||||
private ExternalSystemDTO systemDTO;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// 准备测试数据
|
|
||||||
system = new ExternalSystem();
|
|
||||||
system.setId(1L);
|
|
||||||
system.setName("测试Jenkins");
|
|
||||||
system.setType(ExternalSystemTypeEnum.JENKINS);
|
|
||||||
system.setUrl("http://jenkins.test.com");
|
|
||||||
system.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
|
|
||||||
system.setUsername("admin");
|
|
||||||
system.setPassword("password");
|
|
||||||
system.setEnabled(true);
|
|
||||||
system.setSort(1);
|
|
||||||
|
|
||||||
systemDTO = new ExternalSystemDTO();
|
|
||||||
systemDTO.setName("测试Jenkins");
|
|
||||||
systemDTO.setType(ExternalSystemTypeEnum.JENKINS);
|
|
||||||
systemDTO.setUrl("http://jenkins.test.com");
|
|
||||||
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
|
|
||||||
systemDTO.setUsername("admin");
|
|
||||||
systemDTO.setPassword("password");
|
|
||||||
systemDTO.setEnabled(true);
|
|
||||||
systemDTO.setSort(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void validateUniqueConstraints_WhenNameExists_ShouldThrowException() {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(true);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.validateUniqueConstraints(systemDTO))
|
|
||||||
.isInstanceOf(UniqueConstraintException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_NAME_EXISTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void validateUniqueConstraints_WhenTypeAndUrlExists_ShouldThrowException() {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(false);
|
|
||||||
when(externalSystemRepository.existsByTypeAndUrlAndDeletedFalse(systemDTO.getType(), systemDTO.getUrl()))
|
|
||||||
.thenReturn(true);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.validateUniqueConstraints(systemDTO))
|
|
||||||
.isInstanceOf(UniqueConstraintException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_TYPE_URL_EXISTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConnection_WhenSystemDisabled_ShouldThrowException() {
|
|
||||||
// 准备数据
|
|
||||||
system.setEnabled(false);
|
|
||||||
when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system));
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.testConnection(1L))
|
|
||||||
.isInstanceOf(BusinessException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConnection_WhenSystemEnabled_ShouldReturnTrue() {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system));
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
boolean result = externalSystemService.testConnection(1L);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(result).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void syncData_WhenSystemDisabled_ShouldThrowException() {
|
|
||||||
// 准备数据
|
|
||||||
system.setEnabled(false);
|
|
||||||
when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system));
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.syncData(1L))
|
|
||||||
.isInstanceOf(BusinessException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void syncData_WhenSuccessful_ShouldUpdateStatus() {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system));
|
|
||||||
when(externalSystemRepository.save(any(ExternalSystem.class))).thenReturn(system);
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
externalSystemService.syncData(1L);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(system.getSyncStatus()).isEqualTo(ExternalSystemSyncStatusEnum.SUCCESS);
|
|
||||||
assertThat(system.getLastSyncTime()).isNotNull();
|
|
||||||
verify(externalSystemRepository, times(2)).save(any(ExternalSystem.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateStatus_ShouldUpdateSystemStatus() {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system));
|
|
||||||
when(externalSystemRepository.save(any(ExternalSystem.class))).thenReturn(system);
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
externalSystemService.updateStatus(1L, false);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(system.getEnabled()).isFalse();
|
|
||||||
verify(externalSystemRepository).save(system);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void validateUniqueConstraints_WhenGitWithoutToken_ShouldThrowException() {
|
|
||||||
// 准备数据
|
|
||||||
systemDTO.setType(ExternalSystemTypeEnum.GIT);
|
|
||||||
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.TOKEN);
|
|
||||||
systemDTO.setToken(null);
|
|
||||||
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(false);
|
|
||||||
when(externalSystemRepository.existsByTypeAndUrlAndDeletedFalse(systemDTO.getType(), systemDTO.getUrl()))
|
|
||||||
.thenReturn(false);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.validateUniqueConstraints(systemDTO))
|
|
||||||
.isInstanceOf(BusinessException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_GIT_TOKEN_REQUIRED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void validateUniqueConstraints_WhenGitWithWrongAuthType_ShouldThrowException() {
|
|
||||||
// 准备数据
|
|
||||||
systemDTO.setType(ExternalSystemTypeEnum.GIT);
|
|
||||||
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
|
|
||||||
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(false);
|
|
||||||
when(externalSystemRepository.existsByTypeAndUrlAndDeletedFalse(systemDTO.getType(), systemDTO.getUrl()))
|
|
||||||
.thenReturn(false);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.validateUniqueConstraints(systemDTO))
|
|
||||||
.isInstanceOf(BusinessException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_GIT_AUTH_TYPE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void update_WhenGitWithoutToken_ShouldThrowException() {
|
|
||||||
// 准备数据
|
|
||||||
systemDTO.setType(ExternalSystemTypeEnum.GIT);
|
|
||||||
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.TOKEN);
|
|
||||||
systemDTO.setToken(null);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.update(1L, systemDTO))
|
|
||||||
.isInstanceOf(BusinessException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_GIT_TOKEN_REQUIRED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void update_WhenGitWithWrongAuthType_ShouldThrowException() {
|
|
||||||
// 准备数据
|
|
||||||
systemDTO.setType(ExternalSystemTypeEnum.GIT);
|
|
||||||
systemDTO.setAuthType(ExternalSystemAuthTypeEnum.BASIC);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThatThrownBy(() -> externalSystemService.update(1L, systemDTO))
|
|
||||||
.isInstanceOf(BusinessException.class)
|
|
||||||
.hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_GIT_AUTH_TYPE_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConnection_WhenSuccess_ShouldUpdateLastConnectTime() {
|
|
||||||
// Mock
|
|
||||||
when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system));
|
|
||||||
when(externalSystemRepository.save(any(ExternalSystem.class))).thenReturn(system);
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
boolean result = externalSystemService.testConnection(1L);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(result).isTrue();
|
|
||||||
assertThat(system.getLastConnectTime()).isNotNull();
|
|
||||||
verify(externalSystemRepository).save(system);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,236 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.service.impl;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.converter.MenuConverter;
|
|
||||||
import com.qqchen.deploy.backend.system.entity.Menu;
|
|
||||||
import com.qqchen.deploy.backend.system.entity.Permission;
|
|
||||||
import com.qqchen.deploy.backend.system.model.MenuDTO;
|
|
||||||
import com.qqchen.deploy.backend.system.model.response.MenuPermissionTreeResponse;
|
|
||||||
import com.qqchen.deploy.backend.system.model.response.MenuResponse;
|
|
||||||
import com.qqchen.deploy.backend.system.model.response.PermissionResponse;
|
|
||||||
import com.qqchen.deploy.backend.system.repository.IMenuRepository;
|
|
||||||
import com.qqchen.deploy.backend.system.repository.IPermissionRepository;
|
|
||||||
import com.qqchen.deploy.backend.system.service.impl.MenuServiceImpl;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class MenuServiceImplTest {
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private IMenuRepository menuRepository;
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private IPermissionRepository permissionRepository;
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private MenuConverter menuConverter;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MenuServiceImpl menuService;
|
|
||||||
|
|
||||||
private Menu rootMenu;
|
|
||||||
private Menu childMenu;
|
|
||||||
private Permission permission1;
|
|
||||||
private Permission permission2;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// 准备测试数据
|
|
||||||
rootMenu = new Menu();
|
|
||||||
rootMenu.setId(1L);
|
|
||||||
rootMenu.setName("根菜单");
|
|
||||||
rootMenu.setSort(1);
|
|
||||||
rootMenu.setType(1);
|
|
||||||
rootMenu.setParentId(0L);
|
|
||||||
|
|
||||||
childMenu = new Menu();
|
|
||||||
childMenu.setId(2L);
|
|
||||||
childMenu.setName("子菜单");
|
|
||||||
childMenu.setSort(1);
|
|
||||||
childMenu.setType(2);
|
|
||||||
childMenu.setParentId(1L);
|
|
||||||
|
|
||||||
permission1 = new Permission();
|
|
||||||
permission1.setId(1L);
|
|
||||||
permission1.setMenuId(2L);
|
|
||||||
permission1.setName("查看");
|
|
||||||
permission1.setCode("VIEW");
|
|
||||||
permission1.setSort(1);
|
|
||||||
|
|
||||||
permission2 = new Permission();
|
|
||||||
permission2.setId(2L);
|
|
||||||
permission2.setMenuId(2L);
|
|
||||||
permission2.setName("编辑");
|
|
||||||
permission2.setCode("EDIT");
|
|
||||||
permission2.setSort(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPermissionTree_ShouldReturnCorrectStructure() {
|
|
||||||
// 准备测试数据
|
|
||||||
List<Menu> menus = Arrays.asList(rootMenu, childMenu);
|
|
||||||
List<Permission> permissions = Arrays.asList(permission1, permission2);
|
|
||||||
|
|
||||||
// Mock Repository方法
|
|
||||||
when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(menus);
|
|
||||||
when(permissionRepository.findAllByDeletedFalseOrderBySort()).thenReturn(permissions);
|
|
||||||
|
|
||||||
// Mock Converter方法
|
|
||||||
MenuPermissionTreeResponse rootResponse = createMenuPermissionResponse(rootMenu);
|
|
||||||
MenuPermissionTreeResponse childResponse = createMenuPermissionResponse(childMenu);
|
|
||||||
when(menuConverter.toMenuPermissionResponse(rootMenu)).thenReturn(rootResponse);
|
|
||||||
when(menuConverter.toMenuPermissionResponse(childMenu)).thenReturn(childResponse);
|
|
||||||
when(menuConverter.toPermissionResponseList(Arrays.asList(permission1, permission2)))
|
|
||||||
.thenReturn(Arrays.asList(
|
|
||||||
createPermissionResponse(permission1),
|
|
||||||
createPermissionResponse(permission2)
|
|
||||||
));
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
List<MenuPermissionTreeResponse> result = menuService.getPermissionTree();
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result).hasSize(1); // 只有一个根节点
|
|
||||||
|
|
||||||
MenuPermissionTreeResponse root = result.get(0);
|
|
||||||
assertThat(root.getId()).isEqualTo(1L);
|
|
||||||
assertThat(root.getName()).isEqualTo("根菜单");
|
|
||||||
assertThat(root.getPermissionChildren()).hasSize(1); // 有一个子节点
|
|
||||||
|
|
||||||
MenuPermissionTreeResponse child = root.getPermissionChildren().get(0);
|
|
||||||
assertThat(child.getId()).isEqualTo(2L);
|
|
||||||
assertThat(child.getName()).isEqualTo("子菜单");
|
|
||||||
assertThat(child.getPermissions()).hasSize(2); // 有两个权限
|
|
||||||
|
|
||||||
PermissionResponse firstPermission = child.getPermissions().get(0);
|
|
||||||
assertThat(firstPermission.getCode()).isEqualTo("VIEW");
|
|
||||||
assertThat(firstPermission.getName()).isEqualTo("查看");
|
|
||||||
}
|
|
||||||
|
|
||||||
private MenuPermissionTreeResponse createMenuPermissionResponse(Menu menu) {
|
|
||||||
MenuPermissionTreeResponse response = new MenuPermissionTreeResponse();
|
|
||||||
response.setId(menu.getId());
|
|
||||||
response.setName(menu.getName());
|
|
||||||
response.setParentId(menu.getParentId());
|
|
||||||
response.setType(menu.getType());
|
|
||||||
response.setSort(menu.getSort());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PermissionResponse createPermissionResponse(Permission permission) {
|
|
||||||
PermissionResponse response = new PermissionResponse();
|
|
||||||
response.setId(permission.getId());
|
|
||||||
response.setCode(permission.getCode());
|
|
||||||
response.setName(permission.getName());
|
|
||||||
response.setType(permission.getType());
|
|
||||||
response.setSort(permission.getSort());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPermissionTree_WithEmptyData_ShouldReturnEmptyList() {
|
|
||||||
// Mock空数据
|
|
||||||
when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(List.of());
|
|
||||||
when(permissionRepository.findAllByDeletedFalseOrderBySort()).thenReturn(List.of());
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
List<MenuPermissionTreeResponse> result = menuService.getPermissionTree();
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMenuTree_ShouldReturnCorrectStructure() {
|
|
||||||
// 准备测试数据
|
|
||||||
List<Menu> menus = Arrays.asList(rootMenu, childMenu);
|
|
||||||
|
|
||||||
// 准备DTO对象
|
|
||||||
MenuDTO rootDto = convertToDto(rootMenu);
|
|
||||||
MenuDTO childDto = convertToDto(childMenu);
|
|
||||||
rootDto.setChildren(new ArrayList<>(Arrays.asList(childDto)));
|
|
||||||
List<MenuDTO> dtoList = new ArrayList<>(Arrays.asList(rootDto, childDto));
|
|
||||||
|
|
||||||
// 准备Response对象
|
|
||||||
MenuResponse rootResponse = createMenuResponse(rootMenu);
|
|
||||||
MenuResponse childResponse = createMenuResponse(childMenu);
|
|
||||||
rootResponse.setChildren(new ArrayList<>(Arrays.asList(childResponse)));
|
|
||||||
List<MenuResponse> responseList = new ArrayList<>(Arrays.asList(rootResponse));
|
|
||||||
|
|
||||||
// Mock Repository方法
|
|
||||||
when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(menus);
|
|
||||||
|
|
||||||
// Mock Converter方法
|
|
||||||
when(menuConverter.toDtoList(menus)).thenReturn(dtoList);
|
|
||||||
when(menuConverter.toResponseList(List.of(rootDto))).thenReturn(responseList);
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
List<MenuResponse> result = menuService.getMenuTree();
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result).hasSize(1); // 只有一个根节点
|
|
||||||
|
|
||||||
MenuResponse root = result.get(0);
|
|
||||||
assertThat(root.getId()).isEqualTo(1L);
|
|
||||||
assertThat(root.getName()).isEqualTo("根菜单");
|
|
||||||
assertThat(root.getType()).isEqualTo(1);
|
|
||||||
assertThat(root.getParentId()).isEqualTo(0L);
|
|
||||||
assertThat(root.getChildren()).hasSize(1); // 有一个子节点
|
|
||||||
|
|
||||||
MenuResponse child = root.getChildren().get(0);
|
|
||||||
assertThat(child.getId()).isEqualTo(2L);
|
|
||||||
assertThat(child.getName()).isEqualTo("子菜单");
|
|
||||||
assertThat(child.getType()).isEqualTo(2);
|
|
||||||
assertThat(child.getParentId()).isEqualTo(1L);
|
|
||||||
assertThat(child.getChildren()).isEmpty(); // 没有子节点
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMenuTree_WithEmptyData_ShouldReturnEmptyList() {
|
|
||||||
// Mock空数据
|
|
||||||
when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(List.of());
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
List<MenuResponse> result = menuService.getMenuTree();
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertThat(result).isNotNull();
|
|
||||||
assertThat(result).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private MenuResponse createMenuResponse(Menu menu) {
|
|
||||||
MenuResponse response = new MenuResponse();
|
|
||||||
response.setId(menu.getId());
|
|
||||||
response.setName(menu.getName());
|
|
||||||
response.setParentId(menu.getParentId());
|
|
||||||
response.setType(menu.getType());
|
|
||||||
response.setSort(menu.getSort());
|
|
||||||
response.setChildren(new ArrayList<>());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MenuDTO convertToDto(Menu menu) {
|
|
||||||
MenuDTO dto = new MenuDTO();
|
|
||||||
dto.setId(menu.getId());
|
|
||||||
dto.setName(menu.getName());
|
|
||||||
dto.setParentId(menu.getParentId());
|
|
||||||
dto.setType(menu.getType());
|
|
||||||
dto.setSort(menu.getSort());
|
|
||||||
dto.setChildren(new ArrayList<>());
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.service.impl;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.entity.Tenant;
|
|
||||||
import com.qqchen.deploy.backend.system.repository.ITenantRepository;
|
|
||||||
import com.qqchen.deploy.backend.system.service.impl.TenantServiceImpl;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 租户服务测试类
|
|
||||||
*
|
|
||||||
* @author QQChen
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
class TenantServiceImplTest {
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private ITenantRepository tenantRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private TenantServiceImpl tenantService;
|
|
||||||
|
|
||||||
private Tenant tenant;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// 准备测试数据
|
|
||||||
tenant = new Tenant();
|
|
||||||
tenant.setId(1L);
|
|
||||||
tenant.setName("测试租户");
|
|
||||||
tenant.setCode("TEST");
|
|
||||||
tenant.setEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getStatus_WhenEnabled_ShouldReturnTrue() {
|
|
||||||
// Mock
|
|
||||||
when(tenantRepository.findById(1L)).thenReturn(Optional.of(tenant));
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
boolean result = tenantService.getStatus(1L);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(result).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getStatus_WhenDisabled_ShouldReturnFalse() {
|
|
||||||
// 准备数据
|
|
||||||
tenant.setEnabled(false);
|
|
||||||
|
|
||||||
// Mock
|
|
||||||
when(tenantRepository.findById(1L)).thenReturn(Optional.of(tenant));
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
boolean result = tenantService.getStatus(1L);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(result).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateStatus_WhenDisabled_ShouldUpdateTenantStatus() {
|
|
||||||
// Mock
|
|
||||||
when(tenantRepository.findById(1L)).thenReturn(Optional.of(tenant));
|
|
||||||
when(tenantRepository.save(any(Tenant.class))).thenReturn(tenant);
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
tenantService.updateStatus(1L, false);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(tenant.getEnabled()).isFalse();
|
|
||||||
verify(tenantRepository).save(tenant);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateStatus_WhenEnabled_ShouldUpdateTenantStatus() {
|
|
||||||
// 准备数据
|
|
||||||
tenant.setEnabled(false);
|
|
||||||
|
|
||||||
// Mock
|
|
||||||
when(tenantRepository.findById(1L)).thenReturn(Optional.of(tenant));
|
|
||||||
when(tenantRepository.save(any(Tenant.class))).thenReturn(tenant);
|
|
||||||
|
|
||||||
// 执行
|
|
||||||
tenantService.updateStatus(1L, true);
|
|
||||||
|
|
||||||
// 验证
|
|
||||||
assertThat(tenant.getEnabled()).isTrue();
|
|
||||||
verify(tenantRepository).save(tenant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,305 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.DefaultWorkflowContext;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.NodeExecutor;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
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.INodeInstanceRepository;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
class DefaultWorkflowEngineTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private IWorkflowInstanceRepository workflowInstanceRepository;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private INodeInstanceRepository nodeInstanceRepository;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private Map<NodeTypeEnum, NodeExecutor> nodeExecutors;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private DefaultWorkflowContext.Factory workflowContextFactory;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private NodeExecutor startNodeExecutor;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private NodeExecutor taskNodeExecutor;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private DefaultWorkflowContext workflowContext;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private DefaultWorkflowEngine workflowEngine;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
MockitoAnnotations.openMocks(this);
|
|
||||||
|
|
||||||
// 配置基本的Mock行为
|
|
||||||
when(nodeExecutors.get(NodeTypeEnum.START)).thenReturn(startNodeExecutor);
|
|
||||||
when(nodeExecutors.get(NodeTypeEnum.TASK)).thenReturn(taskNodeExecutor);
|
|
||||||
when(workflowContextFactory.create(any())).thenReturn(workflowContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void startWorkflow_Success() {
|
|
||||||
// 准备测试数据
|
|
||||||
String workflowCode = "test-workflow";
|
|
||||||
String businessKey = "test-key";
|
|
||||||
Map<String, Object> variables = new HashMap<>();
|
|
||||||
variables.put("key1", "value1");
|
|
||||||
|
|
||||||
WorkflowDefinition definition = new WorkflowDefinition();
|
|
||||||
definition.setId(1L);
|
|
||||||
definition.setCode(workflowCode);
|
|
||||||
definition.setStatus(WorkflowDefinitionStatusEnum.PUBLISHED);
|
|
||||||
|
|
||||||
NodeInstance startNode = new NodeInstance();
|
|
||||||
startNode.setId(1L);
|
|
||||||
startNode.setNodeType(NodeTypeEnum.START);
|
|
||||||
startNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode)).thenReturn(definition);
|
|
||||||
when(workflowInstanceRepository.save(any())).thenAnswer(i -> i.getArgument(0));
|
|
||||||
when(nodeInstanceRepository.save(any())).thenReturn(startNode);
|
|
||||||
when(nodeInstanceRepository.findById(startNode.getId())).thenReturn(Optional.of(startNode));
|
|
||||||
doNothing().when(startNodeExecutor).execute(any(), any());
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
WorkflowInstance result = workflowEngine.startWorkflow(workflowCode, businessKey, variables);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertNotNull(result);
|
|
||||||
assertEquals(WorkflowInstanceStatusEnum.RUNNING, result.getStatus());
|
|
||||||
assertNotNull(result.getStartTime());
|
|
||||||
|
|
||||||
verify(workflowDefinitionRepository).findByCodeAndDeletedFalse(workflowCode);
|
|
||||||
verify(workflowInstanceRepository).save(any());
|
|
||||||
verify(nodeInstanceRepository).save(any());
|
|
||||||
verify(workflowContextFactory).create(any());
|
|
||||||
verify(workflowContext, times(variables.size())).setVariable(anyString(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void startWorkflow_WorkflowNotFound() {
|
|
||||||
// 准备测试数据
|
|
||||||
String workflowCode = "non-existent";
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode)).thenReturn(null);
|
|
||||||
|
|
||||||
// 执行测试并验证异常
|
|
||||||
WorkflowEngineException exception = assertThrows(WorkflowEngineException.class,
|
|
||||||
() -> workflowEngine.startWorkflow(workflowCode, "test", null));
|
|
||||||
assertTrue(exception.getMessage().contains(ResponseCode.WORKFLOW_NOT_FOUND.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void startWorkflow_WorkflowNotPublished() {
|
|
||||||
// 准备测试数据
|
|
||||||
String workflowCode = "draft-workflow";
|
|
||||||
WorkflowDefinition definition = new WorkflowDefinition();
|
|
||||||
definition.setCode(workflowCode);
|
|
||||||
definition.setStatus(WorkflowDefinitionStatusEnum.DRAFT);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(workflowDefinitionRepository.findByCodeAndDeletedFalse(workflowCode)).thenReturn(definition);
|
|
||||||
|
|
||||||
// 执行测试并验证异常
|
|
||||||
WorkflowEngineException exception = assertThrows(WorkflowEngineException.class,
|
|
||||||
() -> workflowEngine.startWorkflow(workflowCode, "test", null));
|
|
||||||
assertTrue(exception.getMessage().contains(ResponseCode.WORKFLOW_NOT_PUBLISHED.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void executeNode_Success() {
|
|
||||||
// 准备测试数据
|
|
||||||
Long nodeInstanceId = 1L;
|
|
||||||
NodeInstance nodeInstance = new NodeInstance();
|
|
||||||
nodeInstance.setId(nodeInstanceId);
|
|
||||||
nodeInstance.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
|
|
||||||
WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
nodeInstance.setWorkflowInstance(instance);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(nodeInstanceRepository.findById(nodeInstanceId)).thenReturn(Optional.of(nodeInstance));
|
|
||||||
doNothing().when(taskNodeExecutor).execute(any(), any());
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
workflowEngine.executeNode(nodeInstanceId);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertEquals(NodeStatusEnum.COMPLETED, nodeInstance.getStatus());
|
|
||||||
assertNotNull(nodeInstance.getEndTime());
|
|
||||||
|
|
||||||
verify(nodeInstanceRepository).findById(nodeInstanceId);
|
|
||||||
verify(taskNodeExecutor).execute(any(), any());
|
|
||||||
verify(nodeInstanceRepository).save(nodeInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void executeNode_NodeNotFound() {
|
|
||||||
// 准备测试数据
|
|
||||||
Long nodeInstanceId = 999L;
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(nodeInstanceRepository.findById(nodeInstanceId)).thenReturn(Optional.empty());
|
|
||||||
|
|
||||||
// 执行测试并验证异常
|
|
||||||
WorkflowEngineException exception = assertThrows(WorkflowEngineException.class,
|
|
||||||
() -> workflowEngine.executeNode(nodeInstanceId));
|
|
||||||
assertTrue(exception.getMessage().contains(ResponseCode.WORKFLOW_NODE_NOT_FOUND.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void executeNode_WorkflowNotRunning() {
|
|
||||||
// 准备测试数据
|
|
||||||
Long nodeInstanceId = 1L;
|
|
||||||
NodeInstance nodeInstance = new NodeInstance();
|
|
||||||
nodeInstance.setId(nodeInstanceId);
|
|
||||||
nodeInstance.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
|
|
||||||
WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.COMPLETED);
|
|
||||||
nodeInstance.setWorkflowInstance(instance);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(nodeInstanceRepository.findById(nodeInstanceId)).thenReturn(Optional.of(nodeInstance));
|
|
||||||
|
|
||||||
// 执行测试并验证异常
|
|
||||||
WorkflowEngineException exception = assertThrows(WorkflowEngineException.class,
|
|
||||||
() -> workflowEngine.executeNode(nodeInstanceId));
|
|
||||||
assertTrue(exception.getMessage().contains(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void terminateWorkflow_Success() {
|
|
||||||
// 准备测试数据
|
|
||||||
Long instanceId = 1L;
|
|
||||||
String reason = "Test termination";
|
|
||||||
|
|
||||||
WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
instance.setId(instanceId);
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
|
|
||||||
NodeInstance runningNode = new NodeInstance();
|
|
||||||
runningNode.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
runningNode.setStatus(NodeStatusEnum.RUNNING);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(workflowInstanceRepository.findById(instanceId)).thenReturn(Optional.of(instance));
|
|
||||||
when(nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING))
|
|
||||||
.thenReturn(Arrays.asList(runningNode));
|
|
||||||
doNothing().when(taskNodeExecutor).terminate(any(), any());
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
workflowEngine.terminateWorkflow(instanceId, reason);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertEquals(WorkflowInstanceStatusEnum.TERMINATED, instance.getStatus());
|
|
||||||
assertEquals(reason, instance.getError());
|
|
||||||
assertNotNull(instance.getEndTime());
|
|
||||||
assertEquals(NodeStatusEnum.TERMINATED, runningNode.getStatus());
|
|
||||||
assertNotNull(runningNode.getEndTime());
|
|
||||||
|
|
||||||
verify(workflowInstanceRepository).findById(instanceId);
|
|
||||||
verify(nodeInstanceRepository).findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING);
|
|
||||||
verify(taskNodeExecutor).terminate(any(), any());
|
|
||||||
verify(nodeInstanceRepository).save(runningNode);
|
|
||||||
verify(workflowInstanceRepository).save(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void pauseWorkflow_Success() {
|
|
||||||
// 准备测试数据
|
|
||||||
Long instanceId = 1L;
|
|
||||||
WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
instance.setId(instanceId);
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
|
|
||||||
NodeInstance runningNode = new NodeInstance();
|
|
||||||
runningNode.setStatus(NodeStatusEnum.RUNNING);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(workflowInstanceRepository.findById(instanceId)).thenReturn(Optional.of(instance));
|
|
||||||
when(nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING))
|
|
||||||
.thenReturn(Arrays.asList(runningNode));
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
workflowEngine.pauseWorkflow(instanceId);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertEquals(WorkflowInstanceStatusEnum.PAUSED, instance.getStatus());
|
|
||||||
assertEquals(NodeStatusEnum.PAUSED, runningNode.getStatus());
|
|
||||||
|
|
||||||
verify(workflowInstanceRepository).findById(instanceId);
|
|
||||||
verify(nodeInstanceRepository).findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.RUNNING);
|
|
||||||
verify(nodeInstanceRepository).save(runningNode);
|
|
||||||
verify(workflowInstanceRepository).save(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resumeWorkflow_Success() {
|
|
||||||
// 准备测试数据
|
|
||||||
Long instanceId = 1L;
|
|
||||||
WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
instance.setId(instanceId);
|
|
||||||
instance.setStatus(WorkflowInstanceStatusEnum.PAUSED);
|
|
||||||
|
|
||||||
NodeInstance pausedNode = new NodeInstance();
|
|
||||||
pausedNode.setId(2L);
|
|
||||||
pausedNode.setStatus(NodeStatusEnum.PAUSED);
|
|
||||||
pausedNode.setWorkflowInstance(instance);
|
|
||||||
|
|
||||||
// 配置Mock行为
|
|
||||||
when(workflowInstanceRepository.findById(instanceId)).thenReturn(Optional.of(instance));
|
|
||||||
when(nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.PAUSED))
|
|
||||||
.thenReturn(Arrays.asList(pausedNode));
|
|
||||||
when(nodeInstanceRepository.findById(pausedNode.getId())).thenReturn(Optional.of(pausedNode));
|
|
||||||
doNothing().when(taskNodeExecutor).execute(any(), any());
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
workflowEngine.resumeWorkflow(instanceId);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertEquals(WorkflowInstanceStatusEnum.RUNNING, instance.getStatus());
|
|
||||||
assertEquals(NodeStatusEnum.RUNNING, pausedNode.getStatus());
|
|
||||||
|
|
||||||
verify(workflowInstanceRepository).findById(instanceId);
|
|
||||||
verify(nodeInstanceRepository).findByWorkflowInstanceIdAndStatus(instanceId, NodeStatusEnum.PAUSED);
|
|
||||||
verify(nodeInstanceRepository).save(pausedNode);
|
|
||||||
verify(workflowInstanceRepository).save(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
public class WorkflowShellTest {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private WorkflowEngine workflowEngine;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testShellWorkflow() throws Exception {
|
|
||||||
// 1. 加载工作流定义
|
|
||||||
String json = Files.readString(new ClassPathResource("workflow-shell-test.json").getFile().toPath());
|
|
||||||
WorkflowDefinition definition = objectMapper.readValue(json, WorkflowDefinition.class);
|
|
||||||
workflowDefinitionRepository.save(definition);
|
|
||||||
|
|
||||||
// 2. 启动工作流实例
|
|
||||||
workflowEngine.startWorkflow(definition.getId(), "TEST-" + System.currentTimeMillis(), new HashMap<>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.engine.executor;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.system.enums.LogLevelEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.executor.node.StartNodeExecutor;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
|
|
||||||
class StartNodeExecutorTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private WorkflowContext workflowContext;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private StartNodeExecutor startNodeExecutor;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
MockitoAnnotations.openMocks(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getNodeType_ShouldReturnStartType() {
|
|
||||||
assertEquals(NodeTypeEnum.START, startNodeExecutor.getNodeType());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void execute_ShouldCompleteNodeAndLog() {
|
|
||||||
// 准备测试数据
|
|
||||||
NodeInstance nodeInstance = new NodeInstance();
|
|
||||||
nodeInstance.setNodeType(NodeTypeEnum.START);
|
|
||||||
nodeInstance.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
|
|
||||||
// 执行测试
|
|
||||||
startNodeExecutor.execute(nodeInstance, workflowContext);
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
assertEquals(NodeStatusEnum.COMPLETED, nodeInstance.getStatus());
|
|
||||||
verify(workflowContext).log("开始节点执行完成", LogLevelEnum.INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void validate_ShouldDoNothing() {
|
|
||||||
// 开始节点无需配置,验证方法不会抛出异常
|
|
||||||
startNodeExecutor.validate(null);
|
|
||||||
startNodeExecutor.validate("");
|
|
||||||
startNodeExecutor.validate("{}");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void terminate_ShouldDoNothing() {
|
|
||||||
// 开始节点无需终止操作,验证方法不会抛出异常
|
|
||||||
NodeInstance nodeInstance = new NodeInstance();
|
|
||||||
startNodeExecutor.terminate(nodeInstance, workflowContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
//package com.qqchen.deploy.backend.workflow.engine.executor;
|
|
||||||
//
|
|
||||||
//import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.engine.executor.task.TaskExecutor;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
|
|
||||||
//import org.junit.jupiter.api.BeforeEach;
|
|
||||||
//import org.junit.jupiter.api.Test;
|
|
||||||
//import org.mockito.InjectMocks;
|
|
||||||
//import org.mockito.Mock;
|
|
||||||
//import org.mockito.MockitoAnnotations;
|
|
||||||
//
|
|
||||||
//import java.util.HashMap;
|
|
||||||
//import java.util.Map;
|
|
||||||
//
|
|
||||||
//import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
//import static org.mockito.ArgumentMatchers.any;
|
|
||||||
//import static org.mockito.Mockito.*;
|
|
||||||
//
|
|
||||||
//class TaskNodeExecutorTest {
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private INodeInstanceRepository nodeInstanceRepository;
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private Map<String, TaskExecutor> taskExecutors;
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private TaskExecutor shellTaskExecutor;
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private WorkflowContext context;
|
|
||||||
//
|
|
||||||
// @InjectMocks
|
|
||||||
// private TaskNodeExecutor taskNodeExecutor;
|
|
||||||
//
|
|
||||||
// @BeforeEach
|
|
||||||
// void setUp() {
|
|
||||||
// MockitoAnnotations.openMocks(this);
|
|
||||||
//
|
|
||||||
// // 设置任务执行器映射
|
|
||||||
// when(taskExecutors.get("SHELL")).thenReturn(shellTaskExecutor);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testExecute() {
|
|
||||||
// // 准备测试数据
|
|
||||||
// WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
// instance.setId(1L);
|
|
||||||
//
|
|
||||||
// NodeInstance taskNode = new NodeInstance();
|
|
||||||
// taskNode.setId(1L);
|
|
||||||
// taskNode.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
// taskNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
// taskNode.setConfig("{\"type\":\"SHELL\",\"script\":\"echo hello\"}");
|
|
||||||
//
|
|
||||||
// Map<String, Object> input = new HashMap<>();
|
|
||||||
// input.put("param1", "value1");
|
|
||||||
//
|
|
||||||
// // 设置Mock行为
|
|
||||||
// when(context.getWorkflowInstance()).thenReturn(instance);
|
|
||||||
// when(context.getVariables()).thenReturn(input);
|
|
||||||
// when(shellTaskExecutor.execute(any(), any())).thenReturn(true);
|
|
||||||
//
|
|
||||||
// // 执行测试
|
|
||||||
// boolean result = taskNodeExecutor.execute(context, taskNode);
|
|
||||||
//
|
|
||||||
// // 验证结果
|
|
||||||
// assertTrue(result);
|
|
||||||
// assertEquals(NodeStatusEnum.RUNNING, taskNode.getStatus());
|
|
||||||
// assertNotNull(taskNode.getStartTime());
|
|
||||||
// verify(nodeInstanceRepository, times(1)).save(taskNode);
|
|
||||||
// verify(shellTaskExecutor, times(1)).execute(any(), any());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testExecuteWithError() {
|
|
||||||
// // 准备测试数据
|
|
||||||
// WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
// instance.setId(1L);
|
|
||||||
//
|
|
||||||
// NodeInstance taskNode = new NodeInstance();
|
|
||||||
// taskNode.setId(1L);
|
|
||||||
// taskNode.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
// taskNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
// taskNode.setConfig("{\"type\":\"SHELL\",\"script\":\"echo hello\"}");
|
|
||||||
//
|
|
||||||
// // 设置Mock行为
|
|
||||||
// when(context.getWorkflowInstance()).thenReturn(instance);
|
|
||||||
// when(shellTaskExecutor.execute(any(), any())).thenReturn(false);
|
|
||||||
//
|
|
||||||
// // 执行测试
|
|
||||||
// boolean result = taskNodeExecutor.execute(context, taskNode);
|
|
||||||
//
|
|
||||||
// // 验证结果
|
|
||||||
// assertFalse(result);
|
|
||||||
// assertEquals(NodeStatusEnum.FAILED, taskNode.getStatus());
|
|
||||||
// assertNotNull(taskNode.getStartTime());
|
|
||||||
// assertNotNull(taskNode.getEndTime());
|
|
||||||
// verify(nodeInstanceRepository, times(1)).save(taskNode);
|
|
||||||
// verify(shellTaskExecutor, times(1)).execute(any(), any());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testCanExecute() {
|
|
||||||
// // 准备测试数据
|
|
||||||
// NodeInstance taskNode = new NodeInstance();
|
|
||||||
// taskNode.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
// taskNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
//
|
|
||||||
// // 执行测试
|
|
||||||
// boolean result = taskNodeExecutor.canExecute(taskNode);
|
|
||||||
//
|
|
||||||
// // 验证结果
|
|
||||||
// assertTrue(result);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testCannotExecuteWrongType() {
|
|
||||||
// // 准备测试数据
|
|
||||||
// NodeInstance startNode = new NodeInstance();
|
|
||||||
// startNode.setNodeType(NodeTypeEnum.START);
|
|
||||||
// startNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
//
|
|
||||||
// // 执行测试
|
|
||||||
// boolean result = taskNodeExecutor.canExecute(startNode);
|
|
||||||
//
|
|
||||||
// // 验证结果
|
|
||||||
// assertFalse(result);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testCannotExecuteWrongStatus() {
|
|
||||||
// // 准备测试数据
|
|
||||||
// NodeInstance taskNode = new NodeInstance();
|
|
||||||
// taskNode.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
// taskNode.setStatus(NodeStatusEnum.RUNNING);
|
|
||||||
//
|
|
||||||
// // 执行测试
|
|
||||||
// boolean result = taskNodeExecutor.canExecute(taskNode);
|
|
||||||
//
|
|
||||||
// // 验证结果
|
|
||||||
// assertFalse(result);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void testExecuteWithInvalidConfig() {
|
|
||||||
// // 准备测试数据
|
|
||||||
// WorkflowInstance instance = new WorkflowInstance();
|
|
||||||
// instance.setId(1L);
|
|
||||||
//
|
|
||||||
// NodeInstance taskNode = new NodeInstance();
|
|
||||||
// taskNode.setId(1L);
|
|
||||||
// taskNode.setNodeType(NodeTypeEnum.TASK);
|
|
||||||
// taskNode.setStatus(NodeStatusEnum.PENDING);
|
|
||||||
// taskNode.setConfig("invalid json");
|
|
||||||
//
|
|
||||||
// // 设置Mock行为
|
|
||||||
// when(context.getWorkflowInstance()).thenReturn(instance);
|
|
||||||
//
|
|
||||||
// // 执行测试
|
|
||||||
// boolean result = taskNodeExecutor.execute(context, taskNode);
|
|
||||||
//
|
|
||||||
// // 验证结果
|
|
||||||
// assertFalse(result);
|
|
||||||
// assertEquals(NodeStatusEnum.FAILED, taskNode.getStatus());
|
|
||||||
// assertNotNull(taskNode.getStartTime());
|
|
||||||
// assertNotNull(taskNode.getEndTime());
|
|
||||||
// assertNotNull(taskNode.getError());
|
|
||||||
// verify(nodeInstanceRepository, times(1)).save(taskNode);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.service;
|
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.engine.context.WorkflowContext;
|
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
import com.qqchen.deploy.backend.workflow.service.impl.ConcurrentWorkflowVariableOperations;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.*;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class ConcurrentWorkflowVariableOperationsTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private IWorkflowVariableService variableService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private TransactionTemplate transactionTemplate;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private WorkflowContext.Factory contextFactory;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private ConcurrentWorkflowVariableOperations operations;
|
|
||||||
|
|
||||||
private WorkflowInstance instance;
|
|
||||||
private WorkflowContext context;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
instance = new WorkflowInstance();
|
|
||||||
instance.setId(1L);
|
|
||||||
|
|
||||||
context = mock(WorkflowContext.class);
|
|
||||||
|
|
||||||
when(contextFactory.create(any(), anyMap())).thenReturn(context);
|
|
||||||
when(transactionTemplate.execute(any())).thenAnswer(invocation -> {
|
|
||||||
return invocation.getArgument(0, TransactionCallback.class).doInTransaction(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void setVariables_ShouldHandleConcurrentAccess() throws InterruptedException {
|
|
||||||
// 准备测试数据
|
|
||||||
int threadCount = 10;
|
|
||||||
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
|
|
||||||
CountDownLatch latch = new CountDownLatch(threadCount);
|
|
||||||
|
|
||||||
// 模拟多线程并发访问
|
|
||||||
for (int i = 0; i < threadCount; i++) {
|
|
||||||
final int index = i;
|
|
||||||
executorService.submit(() -> {
|
|
||||||
try {
|
|
||||||
Map<String, Object> variables = new HashMap<>();
|
|
||||||
variables.put("key" + index, "value" + index);
|
|
||||||
operations.setVariables(instance, variables);
|
|
||||||
} finally {
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待所有线程完成
|
|
||||||
latch.await();
|
|
||||||
executorService.shutdown();
|
|
||||||
|
|
||||||
// 验证结果
|
|
||||||
verify(variableService, times(threadCount)).setVariable(eq(instance), anyString(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getVariables_ShouldReturnCachedValues() {
|
|
||||||
// 准备测试数据
|
|
||||||
Map<String, Object> expectedVariables = new HashMap<>();
|
|
||||||
expectedVariables.put("key1", "value1");
|
|
||||||
when(context.getVariables()).thenReturn(expectedVariables);
|
|
||||||
|
|
||||||
// 第一次调用
|
|
||||||
Map<String, Object> result1 = operations.getVariables(instance);
|
|
||||||
assertEquals(expectedVariables, result1);
|
|
||||||
|
|
||||||
// 第二次调用应该返回缓存的结果
|
|
||||||
Map<String, Object> result2 = operations.getVariables(instance);
|
|
||||||
assertEquals(expectedVariables, result2);
|
|
||||||
|
|
||||||
// 验证 variableService 只被调用一次
|
|
||||||
verify(variableService, times(1)).getVariables(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void clearContext_ShouldRemoveFromCache() {
|
|
||||||
// 首先获取变量以确保上下文被缓存
|
|
||||||
operations.getVariables(instance);
|
|
||||||
|
|
||||||
// 清除上下文
|
|
||||||
operations.clearContext(instance);
|
|
||||||
|
|
||||||
// 再次获取变量,应该重新从服务加载
|
|
||||||
operations.getVariables(instance);
|
|
||||||
|
|
||||||
// 验证 variableService 被调用了两次
|
|
||||||
verify(variableService, times(2)).getVariables(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
//package com.qqchen.deploy.backend.workflow.service;
|
|
||||||
//
|
|
||||||
//import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|
||||||
//import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceDTO;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnum;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.enums.WorkflowDefinitionStatusEnum;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
|
||||||
//import com.qqchen.deploy.backend.workflow.service.impl.WorkflowInstanceServiceImpl;
|
|
||||||
//import org.junit.jupiter.api.BeforeEach;
|
|
||||||
//import org.junit.jupiter.api.Test;
|
|
||||||
//import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
//import org.mockito.InjectMocks;
|
|
||||||
//import org.mockito.Mock;
|
|
||||||
//import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
//
|
|
||||||
//import java.util.HashMap;
|
|
||||||
//import java.util.Map;
|
|
||||||
//import java.util.Optional;
|
|
||||||
//
|
|
||||||
//import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
//import static org.mockito.ArgumentMatchers.any;
|
|
||||||
//import static org.mockito.ArgumentMatchers.anyLong;
|
|
||||||
//import static org.mockito.Mockito.*;
|
|
||||||
//
|
|
||||||
//@ExtendWith(MockitoExtension.class)
|
|
||||||
//class WorkflowInstanceServiceTest {
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private IWorkflowInstanceRepository workflowInstanceRepository;
|
|
||||||
//
|
|
||||||
// @Mock
|
|
||||||
// private IWorkflowVariableService workflowVariableService;
|
|
||||||
//
|
|
||||||
// @InjectMocks
|
|
||||||
// private WorkflowInstanceServiceImpl workflowInstanceService;
|
|
||||||
//
|
|
||||||
// private WorkflowDefinition definition;
|
|
||||||
// private WorkflowInstance instance;
|
|
||||||
// private Map<String, Object> variables;
|
|
||||||
//
|
|
||||||
// @BeforeEach
|
|
||||||
// void setUp() {
|
|
||||||
// definition = new WorkflowDefinition();
|
|
||||||
// definition.setId(1L);
|
|
||||||
// definition.setCode("TEST-WF");
|
|
||||||
// definition.setName("Test Workflow");
|
|
||||||
// definition.setStatus(WorkflowDefinitionStatusEnum.PUBLISHED);
|
|
||||||
// definition.setEnabled(true);
|
|
||||||
//
|
|
||||||
// instance = new WorkflowInstance();
|
|
||||||
// instance.setId(1L);
|
|
||||||
// instance.setDefinition(definition);
|
|
||||||
// instance.setBusinessKey("TEST-KEY");
|
|
||||||
// instance.setStatus(WorkflowInstanceStatusEnum.CREATED);
|
|
||||||
//
|
|
||||||
// variables = new HashMap<>();
|
|
||||||
// variables.put("key1", "value1");
|
|
||||||
// variables.put("key2", "value2");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void createInstance_Success() {
|
|
||||||
// when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(definition));
|
|
||||||
// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance);
|
|
||||||
//
|
|
||||||
// WorkflowInstanceDTO result = workflowInstanceService.createInstance(1L, "TEST-KEY", variables);
|
|
||||||
//
|
|
||||||
// assertNotNull(result);
|
|
||||||
// assertEquals(WorkflowInstanceStatusEnum.CREATED, result.getStatus());
|
|
||||||
// assertEquals("TEST-KEY", result.getBusinessKey());
|
|
||||||
// verify(workflowDefinitionRepository).findById(1L);
|
|
||||||
// verify(workflowInstanceRepository).save(any(WorkflowInstance.class));
|
|
||||||
// verify(workflowVariableService, times(2)).setVariable(anyLong(), anyString(), any());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void createInstance_WorkflowNotFound_ThrowsException() {
|
|
||||||
// when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.empty());
|
|
||||||
//
|
|
||||||
// BusinessException exception = assertThrows(BusinessException.class,
|
|
||||||
// () -> workflowInstanceService.createInstance(1L, "TEST-KEY", variables));
|
|
||||||
// assertEquals(ResponseCode.WORKFLOW_NOT_FOUND, exception.getCode());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void createInstance_WorkflowNotPublished_ThrowsException() {
|
|
||||||
// definition.setStatus(WorkflowDefinitionStatusEnum.DRAFT);
|
|
||||||
// when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(definition));
|
|
||||||
//
|
|
||||||
// BusinessException exception = assertThrows(BusinessException.class,
|
|
||||||
// () -> workflowInstanceService.createInstance(1L, "TEST-KEY", variables));
|
|
||||||
// assertEquals(ResponseCode.WORKFLOW_NOT_PUBLISHED, exception.getCode());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void startInstance_Success() {
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance));
|
|
||||||
// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance);
|
|
||||||
//
|
|
||||||
// WorkflowInstanceDTO result = workflowInstanceService.startInstance(1L);
|
|
||||||
//
|
|
||||||
// assertNotNull(result);
|
|
||||||
// assertEquals(WorkflowInstanceStatusEnum.RUNNING, result.getStatus());
|
|
||||||
// assertNotNull(result.getStartTime());
|
|
||||||
// verify(workflowInstanceRepository).findById(1L);
|
|
||||||
// verify(workflowInstanceRepository).save(any(WorkflowInstance.class));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void startInstance_NotFound_ThrowsException() {
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.empty());
|
|
||||||
//
|
|
||||||
// BusinessException exception = assertThrows(BusinessException.class,
|
|
||||||
// () -> workflowInstanceService.startInstance(1L));
|
|
||||||
// assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_FOUND, exception.getCode());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void startInstance_NotCreated_ThrowsException() {
|
|
||||||
// instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance));
|
|
||||||
//
|
|
||||||
// BusinessException exception = assertThrows(BusinessException.class,
|
|
||||||
// () -> workflowInstanceService.startInstance(1L));
|
|
||||||
// assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING, exception.getCode());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void pauseInstance_Success() {
|
|
||||||
// instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance));
|
|
||||||
// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance);
|
|
||||||
//
|
|
||||||
// WorkflowInstanceDTO result = workflowInstanceService.pauseInstance(1L);
|
|
||||||
//
|
|
||||||
// assertNotNull(result);
|
|
||||||
// assertEquals(WorkflowInstanceStatusEnum.PAUSED, result.getStatus());
|
|
||||||
// verify(workflowInstanceRepository).findById(1L);
|
|
||||||
// verify(workflowInstanceRepository).save(any(WorkflowInstance.class));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void resumeInstance_Success() {
|
|
||||||
// instance.setStatus(WorkflowInstanceStatusEnum.PAUSED);
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance));
|
|
||||||
// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance);
|
|
||||||
//
|
|
||||||
// WorkflowInstanceDTO result = workflowInstanceService.resumeInstance(1L);
|
|
||||||
//
|
|
||||||
// assertNotNull(result);
|
|
||||||
// assertEquals(WorkflowInstanceStatusEnum.RUNNING, result.getStatus());
|
|
||||||
// verify(workflowInstanceRepository).findById(1L);
|
|
||||||
// verify(workflowInstanceRepository).save(any(WorkflowInstance.class));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void terminateInstance_Success() {
|
|
||||||
// instance.setStatus(WorkflowInstanceStatusEnum.RUNNING);
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance));
|
|
||||||
// when(workflowInstanceRepository.save(any(WorkflowInstance.class))).thenReturn(instance);
|
|
||||||
//
|
|
||||||
// WorkflowInstanceDTO result = workflowInstanceService.terminateInstance(1L, "Test reason");
|
|
||||||
//
|
|
||||||
// assertNotNull(result);
|
|
||||||
// assertEquals(WorkflowInstanceStatusEnum.TERMINATED, result.getStatus());
|
|
||||||
// assertEquals("Test reason", result.getError());
|
|
||||||
// assertNotNull(result.getEndTime());
|
|
||||||
// verify(workflowInstanceRepository).findById(1L);
|
|
||||||
// verify(workflowInstanceRepository).save(any(WorkflowInstance.class));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Test
|
|
||||||
// void terminateInstance_AlreadyTerminated_ThrowsException() {
|
|
||||||
// instance.setStatus(WorkflowInstanceStatusEnum.TERMINATED);
|
|
||||||
// when(workflowInstanceRepository.findById(anyLong())).thenReturn(Optional.of(instance));
|
|
||||||
//
|
|
||||||
// BusinessException exception = assertThrows(BusinessException.class,
|
|
||||||
// () -> workflowInstanceService.terminateInstance(1L, "Test reason"));
|
|
||||||
// assertEquals(ResponseCode.WORKFLOW_INSTANCE_NOT_RUNNING, exception.getCode());
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
Loading…
Reference in New Issue
Block a user