diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java index f476f6ff..a3f268bd 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java @@ -2,12 +2,15 @@ package com.qqchen.deploy.backend.workflow.dto; import com.fasterxml.jackson.databind.JsonNode; import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import com.qqchen.deploy.backend.workflow.dto.graph.WorkflowGraph; import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnums; import lombok.Data; import lombok.EqualsAndHashCode; /** * 工作流定义DTO + * @author cascade + * @date 2024-12-11 */ @Data @EqualsAndHashCode(callSuper = true) @@ -32,13 +35,20 @@ public class WorkflowDefinitionDTO extends BaseDTO { * BPMN XML内容 */ private String bpmnXml; - - private JsonNode graphConfig; - - private JsonNode flowableConfig; - + + /** + * 图形数据 + */ + private WorkflowGraph graph; + + /** + * 表单配置 + */ private JsonNode formConfig; - + + /** + * 流程状态 + */ private WorkflowStatusEnums status; /** diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/EdgeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/EdgeConfig.java new file mode 100644 index 00000000..ad1a90f2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/EdgeConfig.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; + +/** + * 边配置 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class EdgeConfig { + /** + * 条件 + */ + private String condition; + + /** + * 表达式 + */ + private String expression; + + /** + * 类型(sequence/message/association) + */ + private String type; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java new file mode 100644 index 00000000..c334700c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java @@ -0,0 +1,73 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; +import java.util.List; +import java.util.Map; + +/** + * 节点配置 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class NodeConfig { + /** + * 配置类型 + */ + private String type; + + /** + * 实现类 + */ + private String implementation; + + /** + * 字段配置 + */ + private Map fields; + + /** + * 任务处理人 + */ + private String assignee; + + /** + * 候选用户列表 + */ + private List candidateUsers; + + /** + * 候选组列表 + */ + private List candidateGroups; + + /** + * 到期时间 + */ + private String dueDate; + + /** + * 优先级 + */ + private Integer priority; + + /** + * 表单标识 + */ + private String formKey; + + /** + * 跳过表达式 + */ + private String skipExpression; + + /** + * 是否异步 + */ + private Boolean isAsync; + + /** + * 是否排他 + */ + private Boolean exclusive; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/Position.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/Position.java new file mode 100644 index 00000000..69965605 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/Position.java @@ -0,0 +1,25 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 位置数据传输对象 + * @author cascade + * @date 2024-12-11 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Position { + /** + * X坐标 + */ + private Double x; + + /** + * Y坐标 + */ + private Double y; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/Size.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/Size.java new file mode 100644 index 00000000..b840eac2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/Size.java @@ -0,0 +1,21 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; + +/** + * 节点大小 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class Size { + /** + * 宽度 + */ + private Integer width; + + /** + * 高度 + */ + private Integer height; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowEdge.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowEdge.java new file mode 100644 index 00000000..6c953336 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowEdge.java @@ -0,0 +1,42 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; +import java.util.Map; + +/** + * 工作流边 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class WorkflowEdge { + /** + * 边ID + */ + private String id; + + /** + * 源节点ID + */ + private String source; + + /** + * 目标节点ID + */ + private String target; + + /** + * 边名称 + */ + private String name; + + /** + * 边配置 + */ + private EdgeConfig config; + + /** + * 边属性 + */ + private Map properties; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java new file mode 100644 index 00000000..bd8b19eb --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java @@ -0,0 +1,28 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; +import java.util.List; +import java.util.Map; + +/** + * 工作流图形数据传输对象 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class WorkflowGraph { + /** + * 节点列表 + */ + private List nodes; + + /** + * 边列表 + */ + private List edges; + + /** + * 工作流属性 + */ + private Map properties; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowNode.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowNode.java new file mode 100644 index 00000000..1b124bc5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowNode.java @@ -0,0 +1,48 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import com.qqchen.deploy.backend.workflow.enums.NodeType; +import lombok.Data; +import java.util.Map; + +/** + * 工作流节点数据传输对象 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class WorkflowNode { + /** + * 节点ID + */ + private String id; + + /** + * 节点类型 + */ + private NodeType type; + + /** + * 节点名称 + */ + private String name; + + /** + * 节点位置 + */ + private Position position; + + /** + * 节点配置 + */ + private NodeConfig config; + + /** + * 节点属性 + */ + private Map properties; + + /** + * 节点大小 + */ + private Size size; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowProperties.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowProperties.java new file mode 100644 index 00000000..88ec74de --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowProperties.java @@ -0,0 +1,36 @@ +package com.qqchen.deploy.backend.workflow.dto.graph; + +import lombok.Data; + +/** + * 工作流属性 + * @author cascade + * @date 2024-12-11 + */ +@Data +public class WorkflowProperties { + /** + * 工作流名称 + */ + private String name; + + /** + * 工作流标识 + */ + private String key; + + /** + * 工作流描述 + */ + private String description; + + /** + * 工作流版本 + */ + private Integer version; + + /** + * 工作流类别 + */ + private String category; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java index 05549518..b1d7dde1 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java @@ -48,24 +48,22 @@ public class WorkflowDefinition extends Entity { private String bpmnXml; /** - * 流程图JSON数据 + * 流程图数据,包含节点和连线的位置、样式等信息 */ @Type(JsonType.class) - @Column(name = "graph_json", columnDefinition = "json") - private JsonNode graphJson; + @Column(name = "graph_data", columnDefinition = "json") + private JsonNode graphData; /** - * 表单配置JSON - * 包含: - * - 流程级别的表单配置 - * - 流程变量定义 - * - 表单验证规则 - * - 表单布局配置 + * 表单配置 */ @Type(JsonType.class) @Column(name = "form_config", columnDefinition = "json") private JsonNode formConfig; + /** + * 流程状态 + */ @Column(nullable = false) @Enumerated(EnumType.STRING) private WorkflowStatusEnums status; @@ -73,5 +71,31 @@ public class WorkflowDefinition extends Entity { /** * 流程描述 */ + @Column(columnDefinition = "TEXT") private String description; + + /** + * 是否可执行 + */ + @Column(name = "is_executable", nullable = false) + private Boolean isExecutable = true; + + /** + * 目标命名空间 + */ + @Column(name = "target_namespace") + private String targetNamespace = "http://www.flowable.org/test"; + + /** + * 流程分类 + */ + @Column(name = "category") + private String category; + + /** + * 流程标签,用于分组和过滤 + */ + @Type(JsonType.class) + @Column(name = "tags", columnDefinition = "json") + private JsonNode tags; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeDefinition.java index 4db64f56..50a6d141 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowNodeDefinition.java @@ -92,7 +92,6 @@ public class WorkflowNodeDefinition extends Entity { /** * 排序号 */ - @Type(JsonType.class) @Column(nullable = false) private Integer orderNum = 0; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeType.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeType.java new file mode 100644 index 00000000..2fd15980 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeType.java @@ -0,0 +1,28 @@ +package com.qqchen.deploy.backend.workflow.enums; + +/** + * 节点类型枚举 + * @author cascade + * @date 2024-12-11 + */ +public enum NodeType { + START("start"), + END("end"), + USER_TASK("userTask"), + SERVICE_TASK("serviceTask"), + SCRIPT_TASK("scriptTask"), + EXCLUSIVE_GATEWAY("exclusiveGateway"), + PARALLEL_GATEWAY("parallelGateway"), + SUBPROCESS("subProcess"), + CALL_ACTIVITY("callActivity"); + + private final String value; + + NodeType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java new file mode 100644 index 00000000..3bd31c51 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java @@ -0,0 +1,12 @@ +package com.qqchen.deploy.backend.workflow.exception; + +public class WorkflowValidationException extends RuntimeException { + + public WorkflowValidationException(String message) { + super(message); + } + + public WorkflowValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java index 69c5c824..cbcc30e6 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java @@ -2,7 +2,6 @@ package com.qqchen.deploy.backend.workflow.service.impl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; -import com.qqchen.deploy.backend.workflow.dto.WorkflowDesignDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowExecutionDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowInstanceCreateDTO; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; @@ -71,8 +70,7 @@ public class WorkflowDefinitionServiceImpl extends BaseServiceImpl> handlers; - private final ObjectMapper objectMapper; - private final BpmnNodeConfigParser configParser; - private final BpmnXMLConverter bpmnXMLConverter; + private final BpmnXMLConverter bpmnXmlConverter = new BpmnXMLConverter(); - public BpmnConverter(ObjectMapper objectMapper, - List> nodeHandlers, - BpmnNodeConfigParser configParser) { - this.objectMapper = objectMapper; - this.configParser = configParser; - this.bpmnXMLConverter = new BpmnXMLConverter(); - this.handlers = nodeHandlers.stream() - .collect(Collectors.toMap(BpmnNodeHandler::getType, Function.identity())); - } - - public String convertToBpmnXml(String x6Json, String processId) throws Exception { - JsonNode jsonNode = objectMapper.readTree(x6Json); - BpmnModel bpmnModel = createBpmnModel(processId); - Process process = bpmnModel.getMainProcess(); - - // 处理节点 - Map elementMap = new HashMap<>(); - JsonNode cells = jsonNode.path(BpmnConstants.ProcessAttribute.CELLS); - - if (!cells.isMissingNode()) { - // 处理所有节点 - processNodes(cells, process, elementMap); - - // 确保存在开始事件和结束事件(如果没有显式定义) - ensureStartAndEndEvents(process, elementMap); - - // 处理所有连线 - processSequenceFlows(cells, process, elementMap); - } - - // 自动布局 - new BpmnAutoLayout(bpmnModel).execute(); - - // 转换为XML - return new String(bpmnXMLConverter.convertToXML(bpmnModel)); + /** + * 将工作流图转换为BPMN XML + * @param graph 工作流图数据 + * @param processKey 流程标识 + * @return BPMN XML字符串 + */ + public String convertToXml(WorkflowGraph graph, String processKey) { + BpmnModel bpmnModel = convertToBpmnModel(graph, processKey); + byte[] xmlBytes = bpmnXmlConverter.convertToXML(bpmnModel); + return new String(xmlBytes, StandardCharsets.UTF_8); } /** - * 创建BPMN模型 + * 将工作流图转换为BPMN模型 + * @param graph 工作流图数据 + * @param processKey 流程标识 + * @return BPMN模型 */ - private BpmnModel createBpmnModel(String processId) { + public BpmnModel convertToBpmnModel(WorkflowGraph graph, String processKey) { BpmnModel bpmnModel = new BpmnModel(); Process process = new Process(); - process.setId(processId); - process.setName(processId); - process.setExecutable(Boolean.parseBoolean(BpmnConstants.ProcessAttribute.EXECUTABLE)); + + // 设置流程属性 + process.setId(processKey); + if (graph.getProperties() != null) { + Map properties = graph.getProperties(); + process.setName(properties.get("name") != null ? properties.get("name").toString() : null); + process.setDocumentation(properties.get("description") != null ? properties.get("description").toString() : null); + } + + // 转换节点 + for (WorkflowNode node : graph.getNodes()) { + FlowElement element = convertNode(node); + if (element != null) { + process.addFlowElement(element); + } + } + + // 转换边 + for (WorkflowEdge edge : graph.getEdges()) { + SequenceFlow flow = convertEdge(edge); + if (flow != null) { + process.addFlowElement(flow); + } + } + bpmnModel.addProcess(process); return bpmnModel; } /** - * 处理所有节点 + * 将工作流图转换为BPMN模型 + * @param graph 工作流图数据 + * @return BPMN模型 */ - private void processNodes(JsonNode cells, Process process, Map elementMap) { - cells.forEach(cell -> { - if (isNode(cell)) { - processNode(cell, process, elementMap); - } - }); + public BpmnModel convertToBpmnModel(WorkflowGraph graph) { + String processKey = null; + if (graph.getProperties() != null) { + Object key = graph.getProperties().get("key"); + processKey = key != null ? key.toString() : null; + } + if (processKey == null) { + processKey = "process_" + System.currentTimeMillis(); + } + return convertToBpmnModel(graph, processKey); } /** - * 处理单个节点 + * 转换节点 + * @param node 工作流节点 + * @return 流程元素 */ - private void processNode(JsonNode cell, Process process, Map elementMap) { - String shape = cell.path(BpmnConstants.NodeAttribute.SHAPE).asText(); - String id = cell.path(BpmnConstants.NodeAttribute.ID).asText(); - String label = cell.path(BpmnConstants.NodeAttribute.DATA) - .path(BpmnConstants.NodeAttribute.LABEL).asText(""); - - // 处理特殊节点类型 - Optional specialElement = createSpecialElement(shape, id, label); - if (specialElement.isPresent()) { - process.addFlowElement(specialElement.get()); - elementMap.put(id, specialElement.get()); - return; + private FlowElement convertNode(WorkflowNode node) { + if (node.getType() == null) { + return null; } - // 处理普通节点 - processRegularNode(cell, process, elementMap); - } - - /** - * 创建特殊元素(开始/结束事件) - */ - private Optional createSpecialElement(String shape, String id, String label) { - FlowElement element = null; - - if (BpmnConstants.NodeShape.START.equals(shape)) { - StartEvent startEvent = new StartEvent(); - startEvent.setId(id); - startEvent.setName(label); - element = startEvent; - } else if (BpmnConstants.NodeShape.END.equals(shape)) { - EndEvent endEvent = new EndEvent(); - endEvent.setId(id); - endEvent.setName(label); - element = endEvent; - } - - return Optional.ofNullable(element); - } - - /** - * 处理普通节点 - */ - private void processRegularNode(JsonNode cell, Process process, Map elementMap) { - BpmnNodeConfig config = configParser.parse(cell); - if (config.getType() != null) { - BpmnNodeHandler handler = handlers.get(config.getType()); - if (handler != null) { - FlowElement element = createAndHandleElement(handler, cell, config); - process.addFlowElement(element); - elementMap.put(config.getId(), element); - } - } - } - - /** - * 处理所有连线 - */ - private void processSequenceFlows(JsonNode cells, Process process, Map elementMap) { - cells.forEach(cell -> { - if (isEdge(cell)) { - handleSequenceFlow(cell, process, elementMap); - } - }); - } - - private void ensureStartAndEndEvents(Process process, Map elementMap) { - // 查找现有的开始和结束事件 - StartEvent startEvent = null; - EndEvent endEvent = null; - FlowElement firstElement = null; - FlowElement lastElement = null; - - // 遍历所有节点,找到开始和结束事件,以及第一个和最后一个任务节点 - for (FlowElement element : process.getFlowElements()) { - if (element instanceof StartEvent) { - startEvent = (StartEvent) element; - } else if (element instanceof EndEvent) { - endEvent = (EndEvent) element; - } else if (!(element instanceof SequenceFlow)) { - if (firstElement == null) { - firstElement = element; - } - lastElement = element; - } - } - - // 如果没有开始事件,创建一个并连接到第一个任务 - if (startEvent == null && firstElement != null) { - startEvent = createStartEvent(); - process.addFlowElement(startEvent); - elementMap.put(startEvent.getId(), startEvent); - - // 创建从开始事件到第一个任务的连线 - SequenceFlow startFlow = createSequenceFlow( - BpmnConstants.DefaultNodeId.START_FLOW, - startEvent.getId(), - firstElement.getId() - ); - process.addFlowElement(startFlow); - } - - // 如果没有结束事件,创建一个并从最后一个任务连接到它 - if (endEvent == null && lastElement != null) { - endEvent = createEndEvent(); - process.addFlowElement(endEvent); - elementMap.put(endEvent.getId(), endEvent); - - // 创建从最后一个任务到结束事件的连线 - SequenceFlow endFlow = createSequenceFlow( - BpmnConstants.DefaultNodeId.END_FLOW, - lastElement.getId(), - endEvent.getId() - ); - process.addFlowElement(endFlow); + switch (node.getType()) { + case START: + return createStartEvent(node); + case END: + return createEndEvent(node); + case SERVICE_TASK: + return createServiceTask(node); + case USER_TASK: + return createUserTask(node); + case SCRIPT_TASK: + return createScriptTask(node); + case EXCLUSIVE_GATEWAY: + return createExclusiveGateway(node); + case PARALLEL_GATEWAY: + return createParallelGateway(node); + case SUBPROCESS: + return createSubProcess(node); + case CALL_ACTIVITY: + return createCallActivity(node); + default: + return null; } } /** * 创建开始事件 + * @param node 节点数据 + * @return 开始事件 */ - private StartEvent createStartEvent() { + private StartEvent createStartEvent(WorkflowNode node) { StartEvent startEvent = new StartEvent(); - startEvent.setId(BpmnConstants.DefaultNodeId.START_EVENT); - startEvent.setName(BpmnConstants.DefaultNodeName.START_EVENT); + startEvent.setId(node.getId()); + startEvent.setName(node.getName()); return startEvent; } /** * 创建结束事件 + * @param node 节点数据 + * @return 结束事件 */ - private EndEvent createEndEvent() { + private EndEvent createEndEvent(WorkflowNode node) { EndEvent endEvent = new EndEvent(); - endEvent.setId(BpmnConstants.DefaultNodeId.END_EVENT); - endEvent.setName(BpmnConstants.DefaultNodeName.END_EVENT); + endEvent.setId(node.getId()); + endEvent.setName(node.getName()); return endEvent; } /** - * 创建连线 + * 创建服务任务 + * @param node 节点数据 + * @return 服务任务 */ - private SequenceFlow createSequenceFlow(String id, String sourceRef, String targetRef) { + private ServiceTask createServiceTask(WorkflowNode node) { + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(node.getId()); + serviceTask.setName(node.getName()); + + if (node.getConfig() != null) { + NodeConfig config = node.getConfig(); + serviceTask.setImplementation(config.getImplementation()); + if (config.getFields() != null) { + config.getFields().forEach((key, value) -> { + FieldExtension field = new FieldExtension(); + field.setFieldName(key); + field.setStringValue(value != null ? value.toString() : null); + serviceTask.getFieldExtensions().add(field); + }); + } + } + + return serviceTask; + } + + /** + * 创建用户任务 + * @param node 节点数据 + * @return 用户任务 + */ + private UserTask createUserTask(WorkflowNode node) { + UserTask userTask = new UserTask(); + userTask.setId(node.getId()); + userTask.setName(node.getName()); + + if (node.getConfig() != null) { + NodeConfig config = node.getConfig(); + userTask.setAssignee(config.getAssignee()); + userTask.setCandidateUsers(config.getCandidateUsers()); + userTask.setCandidateGroups(config.getCandidateGroups()); + userTask.setDueDate(config.getDueDate()); + userTask.setPriority(config.getPriority() != null ? config.getPriority().toString() : null); + userTask.setFormKey(config.getFormKey()); + userTask.setSkipExpression(config.getSkipExpression()); + } + + return userTask; + } + + /** + * 创建脚本任务 + * @param node 节点数据 + * @return 脚本任务 + */ + private ScriptTask createScriptTask(WorkflowNode node) { + ScriptTask scriptTask = new ScriptTask(); + scriptTask.setId(node.getId()); + scriptTask.setName(node.getName()); + + if (node.getConfig() != null) { + NodeConfig config = node.getConfig(); + scriptTask.setScriptFormat(config.getImplementation()); + if (config.getFields() != null && config.getFields().containsKey("script")) { + scriptTask.setScript(config.getFields().get("script").toString()); + } + } + + return scriptTask; + } + + /** + * 创建排他网关 + * @param node 节点数据 + * @return 排他网关 + */ + private ExclusiveGateway createExclusiveGateway(WorkflowNode node) { + ExclusiveGateway gateway = new ExclusiveGateway(); + gateway.setId(node.getId()); + gateway.setName(node.getName()); + return gateway; + } + + /** + * 创建并行网关 + * @param node 节点数据 + * @return 并行网关 + */ + private ParallelGateway createParallelGateway(WorkflowNode node) { + ParallelGateway gateway = new ParallelGateway(); + gateway.setId(node.getId()); + gateway.setName(node.getName()); + return gateway; + } + + /** + * 创建子流程 + * @param node 节点数据 + * @return 子流程 + */ + private SubProcess createSubProcess(WorkflowNode node) { + SubProcess subProcess = new SubProcess(); + subProcess.setId(node.getId()); + subProcess.setName(node.getName()); + return subProcess; + } + + /** + * 创建调用活动 + * @param node 节点数据 + * @return 调用活动 + */ + private CallActivity createCallActivity(WorkflowNode node) { + CallActivity callActivity = new CallActivity(); + callActivity.setId(node.getId()); + callActivity.setName(node.getName()); + + if (node.getConfig() != null) { + NodeConfig config = node.getConfig(); + callActivity.setCalledElement(config.getImplementation()); + if (config.getFields() != null) { + config.getFields().forEach((key, value) -> { + IOParameter parameter = new IOParameter(); + parameter.setSource(key); + parameter.setTarget(value != null ? value.toString() : null); + callActivity.getInParameters().add(parameter); + }); + } + } + + return callActivity; + } + + /** + * 转换边 + * @param edge 工作流边 + * @return 序列流 + */ + private SequenceFlow convertEdge(WorkflowEdge edge) { SequenceFlow flow = new SequenceFlow(); - flow.setId(id); - flow.setSourceRef(sourceRef); - flow.setTargetRef(targetRef); + flow.setId(edge.getId()); + flow.setName(edge.getName()); + flow.setSourceRef(edge.getSource()); + flow.setTargetRef(edge.getTarget()); + + if (edge.getConfig() != null) { + EdgeConfig config = edge.getConfig(); + if ("sequence".equals(config.getType())) { + if (config.getCondition() != null) { + ((SequenceFlow) flow).setConditionExpression(config.getCondition()); + } + } + } + return flow; } - - private boolean isNode(JsonNode cell) { - return cell.has(BpmnConstants.NodeAttribute.SHAPE) && - !BpmnConstants.NodeShape.EDGE.equals(cell.path(BpmnConstants.NodeAttribute.SHAPE).asText()); - } - - private boolean isEdge(JsonNode cell) { - return cell.has(BpmnConstants.NodeAttribute.SHAPE) && - BpmnConstants.NodeShape.EDGE.equals(cell.path(BpmnConstants.NodeAttribute.SHAPE).asText()); - } - - @SuppressWarnings("unchecked") - private T createAndHandleElement( - BpmnNodeHandler handler, JsonNode nodeData, BpmnNodeConfig config) { - T element = handler.createElement(config); - handler.handle(nodeData, element, config); - return element; - } - - private void handleSequenceFlow(JsonNode cell, Process process, Map elementMap) { - String sourceId = cell.path(BpmnConstants.NodeAttribute.SOURCE).asText(); - String targetId = cell.path(BpmnConstants.NodeAttribute.TARGET).asText(); - String id = cell.path(BpmnConstants.NodeAttribute.ID).asText(); - - FlowElement sourceElement = elementMap.get(sourceId); - FlowElement targetElement = elementMap.get(targetId); - - if (sourceElement != null && targetElement != null) { - SequenceFlow sequenceFlow = createSequenceFlow(id, sourceId, targetId); - - // 设置连线名称 - JsonNode label = cell.path(BpmnConstants.NodeAttribute.DATA) - .path(BpmnConstants.NodeAttribute.LABEL); - if (!label.isMissingNode()) { - sequenceFlow.setName(label.asText()); - } - - // 设置条件表达式 - JsonNode condition = cell.path(BpmnConstants.NodeAttribute.DATA) - .path(BpmnConstants.NodeAttribute.CONDITION); - if (!condition.isMissingNode()) { - sequenceFlow.setConditionExpression(condition.asText()); - } - - process.addFlowElement(sequenceFlow); - } - } } diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index ea3df333..ef621a91 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -383,26 +383,41 @@ CREATE TABLE deploy_repo_branch ( -- -------------------------------------------------------------------------------------- -- 工作流定义表 -CREATE TABLE workflow_definition ( - 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 '乐观锁版本号', - - name VARCHAR(100) NOT NULL COMMENT '流程名称', - `key` VARCHAR(50) NOT NULL COMMENT '流程标识', - flow_version INT NOT NULL COMMENT '流程版本', - bpmn_xml TEXT COMMENT 'BPMN XML内容', - graph_json JSON COMMENT 'x6 JSON内容', - status VARCHAR(32) NOT NULL COMMENT '状态', - description VARCHAR(255) NULL COMMENT '流程描述', - form_config JSON COMMENT '表单配置JSON', - - CONSTRAINT UK_workflow_definition_key_version UNIQUE (`key`, flow_version) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流定义表'; +CREATE TABLE workflow_definition +( + -- 主键 + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + + -- 基础信息 + name VARCHAR(255) NOT NULL COMMENT '流程名称', + `key` VARCHAR(255) NOT NULL COMMENT '流程标识', + flow_version INT NOT NULL COMMENT '流程版本', + description TEXT COMMENT '流程描述', + category VARCHAR(100) COMMENT '流程分类', + + -- 流程配置 + bpmn_xml TEXT COMMENT 'BPMN XML内容', + graph_data JSON COMMENT '流程图数据,包含节点和连线的位置、样式等信息', + form_config JSON COMMENT '表单配置', + tags JSON COMMENT '流程标签', + + -- 流程属性 + status VARCHAR(50) NOT NULL COMMENT '流程状态(DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用)', + is_executable BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否可执行', + target_namespace VARCHAR(255) DEFAULT 'http://www.flowable.org/test' COMMENT '目标命名空间', + + -- 审计字段 + created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', + updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', + created_by BIGINT COMMENT '创建人', + updated_by BIGINT COMMENT '更新人', + is_deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否删除', + + -- 约束 + UNIQUE KEY uk_key_version (`key`, flow_version) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci COMMENT ='工作流定义表'; -- 工作流实例表 CREATE TABLE workflow_instance ( diff --git a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql index 737dc854..5f2046ee 100644 --- a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql +++ b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql @@ -156,305 +156,141 @@ INSERT INTO sys_external_system ( -- 初始化工作流相关数据 -- -------------------------------------------------------------------------------------- +-- 工作流定义测试数据 +INSERT INTO workflow_definition ( + name, `key`, flow_version, description, category, + bpmn_xml, graph_data, form_config, tags, + status, is_executable, target_namespace, + created_at, updated_at, created_by, updated_by, is_deleted +) VALUES +-- 简单流程:开始 -> 服务任务 -> 结束 +( + '简单服务任务流程', 'simple_service_flow', 1, '一个包含服务任务的简单流程', 'test', + '', + '{"nodes":[{"id":"start1","type":"start","position":{"x":100,"y":100}},{"id":"service1","type":"serviceTask","position":{"x":300,"y":100}},{"id":"end1","type":"end","position":{"x":500,"y":100}}],"edges":[{"id":"flow1","source":"start1","target":"service1"},{"id":"flow2","source":"service1","target":"end1"}]}', + '{"formItems":[]}', + '["simple","test","service"]', + 'PUBLISHED', TRUE, 'http://www.flowable.org/test', + NOW(), NOW(), 1, 1, FALSE +), + +-- 用户任务流程:开始 -> 用户任务 -> 结束 +( + '用户审批流程', 'user_approval_flow', 1, '一个简单的用户审批流程', 'approval', + '', + '{"nodes":[{"id":"start1","type":"start","position":{"x":100,"y":100}},{"id":"user1","type":"userTask","position":{"x":300,"y":100}},{"id":"end1","type":"end","position":{"x":500,"y":100}}],"edges":[{"id":"flow1","source":"start1","target":"user1","label":"提交审批"},{"id":"flow2","source":"user1","target":"end1","label":"审批完成"}]}', + '{"formItems":[{"type":"input","label":"意见","name":"comment","required":true}]}', + '["approval","user-task"]', + 'PUBLISHED', TRUE, 'http://www.flowable.org/test', + NOW(), NOW(), 1, 1, FALSE +), + +-- 复杂流程:开始 -> 服务任务 -> 用户任务 -> 脚本任务 -> 结束 +( + '复杂业务流程', 'complex_business_flow', 1, '包含多种任务节点的复杂业务流程', 'business', + '$${condition}', + '{"nodes":[{"id":"start1","type":"start","position":{"x":100,"y":100}},{"id":"service1","type":"serviceTask","position":{"x":300,"y":100}},{"id":"user1","type":"userTask","position":{"x":500,"y":100}},{"id":"script1","type":"scriptTask","position":{"x":700,"y":100}},{"id":"end1","type":"end","position":{"x":900,"y":100}}],"edges":[{"id":"flow1","source":"start1","target":"service1"},{"id":"flow2","source":"service1","target":"user1","label":"条件分支"},{"id":"flow3","source":"user1","target":"script1"},{"id":"flow4","source":"script1","target":"end1"}]}', + '{"formItems":[{"type":"input","label":"业务参数","name":"businessParam","required":true},{"type":"select","label":"审批结果","name":"approvalResult","options":[{"label":"同意","value":"approve"},{"label":"拒绝","value":"reject"}]}]}', + '["complex","business","multi-task"]', + 'PUBLISHED', TRUE, 'http://www.flowable.org/test', + NOW(), NOW(), 1, 1, FALSE +); + +-- -------------------------------------------------------------------------------------- -- 初始化工作流节点定义数据 -INSERT INTO workflow_node_definition ( - id, type, name, description, category, - flowable_config, graph_config, form_config, - order_num, enabled, - create_time, create_by, update_time, update_by, version, deleted -) VALUES --- 开始节点 -(1, 'startEvent', '开始节点', '流程的开始节点', 'EVENT', - -- Flowable配置 - '{ - "type": "startEvent" - }', - -- X6图形配置 - '{ - "shape": "flow-circle", - "width": 40, - "height": 40, - "ports": { - "groups": { - "bottom": { - "position": "bottom", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" - } - } - } - }, - "items": [ - { "group": "bottom", "id": "bottom" } - ] - }, - "attrs": { - "body": { - "fill": "#67C23A", - "stroke": "#5F95FF", - "strokeWidth": 1 - }, - "icon": { - "xlinkHref": "path/to/start-icon.svg" - } - } - }', - -- 表单配置(开始节点一般不需要配置) - NULL, - 10, true, - NOW(), 'system', NOW(), 'system', 1, false -), +-- -------------------------------------------------------------------------------------- --- 结束节点 -(2, 'endEvent', '结束节点', '流程的结束节点', 'EVENT', - -- Flowable配置 +-- Shell任务节点定义 +INSERT INTO workflow_node_definition (id, create_time, create_by, update_time, update_by, type, name, description, category, flowable_config, graph_config, form_config, order_num, enabled) +VALUES ( + 1, + NOW(), + 'system', + NOW(), + 'system', + 'shell', + 'Shell脚本', + 'Shell脚本执行节点,用于执行Shell命令或脚本', + 'TASK', '{ - "type": "endEvent" - }', - -- X6图形配置 - '{ - "shape": "flow-circle", - "width": 40, - "height": 40, - "ports": { - "groups": { - "top": { - "position": "top", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" + "type": "object", + "required": ["type", "implementation", "fields"], + "properties": { + "type": { + "type": "string", + "enum": ["shell"], + "description": "任务类型" + }, + "implementation": { + "type": "string", + "const": "$${shellTaskDelegate}", + "description": "任务实现类" + }, + "fields": { + "type": "object", + "required": ["script", "workDir"], + "properties": { + "script": { + "type": "string", + "description": "要执行的Shell脚本内容" + }, + "workDir": { + "type": "string", + "description": "脚本执行的工作目录" + }, + "env": { + "type": "object", + "description": "环境变量", + "additionalProperties": { + "type": "string" } } } - }, - "items": [ - { "group": "top", "id": "top" } - ] - }, - "attrs": { - "body": { - "fill": "#F56C6C", - "stroke": "#5F95FF", - "strokeWidth": 2 - }, - "icon": { - "xlinkHref": "path/to/end-icon.svg" } } }', - -- 表单配置(结束节点一般不需要配置) - NULL, - 90, true, - NOW(), 'system', NOW(), 'system', 1, false -), - --- Shell脚本节点 -(3, 'shellTask', 'Shell脚本', '执行Shell命令的节点', 'TASK', - -- Flowable配置 '{ - "type": "shellTask", - "delegateExpression": "$${shellTaskDelegate}", - "listeners": [ - { - "event": "start", - "delegateExpression": "$${shellTaskStartListener}" - }, - { - "event": "end", - "delegateExpression": "$${shellTaskEndListener}" + "type": "object", + "required": ["shape"], + "properties": { + "shape": { + "type": "string", + "const": "serviceTask", + "description": "节点形状" } - ] + } }', - -- X6图形配置 '{ - "shape": "flow-rect", - "width": 120, - "height": 60, - "ports": { - "groups": { - "top": { - "position": "top", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" - } - } - }, - "bottom": { - "position": "bottom", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "type", "label"], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["input", "textarea", "select"] + }, + "label": { + "type": "string" + }, + "required": { + "type": "boolean" } } } - }, - "items": [ - { "group": "top", "id": "top" }, - { "group": "bottom", "id": "bottom" } - ] - }, - "attrs": { - "body": { - "fill": "#E6A23C", - "stroke": "#5F95FF", - "strokeWidth": 1 - }, - "icon": { - "xlinkHref": "path/to/shell-icon.svg" - }, - "label": { - "text": "Shell脚本", - "fill": "#ffffff", - "fontSize": 12, - "textAnchor": "middle" } } }', - -- 表单配置 - '{ - "fields": [ - { - "name": "script", - "label": "脚本内容", - "type": "textarea", - "required": true, - "placeholder": "请输入shell脚本内容" - }, - { - "name": "workDir", - "label": "工作目录", - "type": "input", - "required": false, - "placeholder": "请输入工作目录路径" - }, - { - "name": "env", - "label": "环境变量", - "type": "keyValue", - "required": false, - "placeholder": "请配置环境变量" - } - ], - "rules": { - "script": [ - { "required": true, "message": "请输入脚本内容" } - ] - } - }', - 20, true, - NOW(), 'system', NOW(), 'system', 1, false -), - --- 排他网关 -(4, 'exclusiveGateway', '排他网关', '基于条件的分支网关', 'GATEWAY', - -- Flowable配置 - '{ - "type": "exclusiveGateway" - }', - -- X6图形配置 - '{ - "shape": "flow-rhombus", - "width": 60, - "height": 60, - "ports": { - "groups": { - "top": { - "position": "top", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" - } - } - }, - "bottom": { - "position": "bottom", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" - } - } - }, - "left": { - "position": "left", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" - } - } - }, - "right": { - "position": "right", - "attrs": { - "circle": { - "r": 4, - "magnet": true, - "stroke": "#5F95FF", - "strokeWidth": 1, - "fill": "#fff" - } - } - } - }, - "items": [ - { "group": "top", "id": "top" }, - { "group": "bottom", "id": "bottom" }, - { "group": "left", "id": "left" }, - { "group": "right", "id": "right" } - ] - }, - "attrs": { - "body": { - "fill": "#9B59B6", - "stroke": "#5F95FF", - "strokeWidth": 1 - }, - "icon": { - "xlinkHref": "path/to/exclusive-gateway-icon.svg" - } - } - }', - -- 表单配置 - '{ - "fields": [ - { - "name": "defaultFlow", - "label": "默认流转路径", - "type": "select", - "required": false, - "placeholder": "请选择默认流转路径" - } - ] - }', - 30, true, - NOW(), 'system', NOW(), 'system', 1, false + 10, + TRUE ); \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/BpmnConverterTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/BpmnConverterTest.java index bc9d84f8..53b36459 100644 --- a/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/BpmnConverterTest.java +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/BpmnConverterTest.java @@ -1,144 +1,439 @@ package com.qqchen.deploy.backend.workflow.util; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.qqchen.deploy.backend.workflow.config.BpmnNodeManager; -import com.qqchen.deploy.backend.workflow.handler.impl.ShellTaskHandler; -import com.qqchen.deploy.backend.workflow.parser.BpmnNodeConfigParser; -import org.flowable.bpmn.model.BpmnModel; +import com.qqchen.deploy.backend.workflow.dto.graph.*; +import com.qqchen.deploy.backend.workflow.enums.NodeType; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.*; import org.flowable.bpmn.model.Process; -import org.flowable.bpmn.model.ServiceTask; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; -import java.nio.charset.StandardCharsets; -import java.util.Collections; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; /** * BPMN转换器测试 */ -@ContextConfiguration(classes = {BpmnNodeManager.class}) +@SpringBootTest(properties = { + "spring.flyway.enabled=false" +}) class BpmnConverterTest { + @Autowired private BpmnConverter bpmnConverter; - private ObjectMapper objectMapper; - private BpmnNodeManager nodeManager; - @BeforeEach - void setUp() { - // 创建Spring上下文 - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.register(BpmnNodeManager.class, ShellTaskHandler.class); - context.refresh(); + @Test + void testConvertSimpleWorkflow() { + // 创建一个简单的工作流图:开始 -> 服务任务 -> 结束 + WorkflowGraph graph = new WorkflowGraph(); - // 获取必要的Bean - objectMapper = new ObjectMapper(); - nodeManager = context.getBean(BpmnNodeManager.class); + // 创建节点 + List nodes = new ArrayList<>(); - // 创建BpmnConverter - bpmnConverter = new BpmnConverter( - objectMapper, - Collections.singletonList(context.getBean(ShellTaskHandler.class)), - new BpmnNodeConfigParser() - ); + // 开始节点 + WorkflowNode startNode = new WorkflowNode(); + startNode.setId("start"); + startNode.setType(NodeType.START); + startNode.setName("开始"); + startNode.setPosition(new Position(100.0, 100.0)); + nodes.add(startNode); + + // 服务任务节点 + WorkflowNode serviceNode = new WorkflowNode(); + serviceNode.setId("service1"); + serviceNode.setType(NodeType.SERVICE_TASK); + serviceNode.setName("服务任务"); + serviceNode.setPosition(new Position(300.0, 100.0)); + + NodeConfig serviceConfig = new NodeConfig(); + serviceConfig.setImplementation("${shellTaskDelegate}"); + Map fields = new HashMap<>(); + fields.put("script", "echo 'Hello World'"); + fields.put("workDir", "/tmp"); + fields.put("timeout", "30"); + serviceConfig.setFields(fields); + serviceNode.setConfig(serviceConfig); + nodes.add(serviceNode); + + // 结束节点 + WorkflowNode endNode = new WorkflowNode(); + endNode.setId("end"); + endNode.setType(NodeType.END); + endNode.setName("结束"); + endNode.setPosition(new Position(500.0, 100.0)); + nodes.add(endNode); + + // 创建边 + List edges = new ArrayList<>(); + + // 开始 -> 服务任务 + WorkflowEdge edge1 = new WorkflowEdge(); + edge1.setId("flow1"); + edge1.setSource("start"); + edge1.setTarget("service1"); + edge1.setName("流转到服务任务"); + edges.add(edge1); + + // 服务任务 -> 结束 + WorkflowEdge edge2 = new WorkflowEdge(); + edge2.setId("flow2"); + edge2.setSource("service1"); + edge2.setTarget("end"); + edge2.setName("流转到结束"); + edges.add(edge2); + + graph.setNodes(nodes); + graph.setEdges(edges); + + // 执行转换 + BpmnModel bpmnModel = bpmnConverter.convertToBpmnModel(graph); + + // 打印XML + try { + System.out.println("\n=== Simple Workflow XML ==="); + System.out.println(new String(new BpmnXMLConverter().convertToXML(bpmnModel))); + } catch (Exception e) { + e.printStackTrace(); + } + + // 验证结果 + assertNotNull(bpmnModel); + Process process = bpmnModel.getMainProcess(); + assertNotNull(process); + + // 验证节点数量 + assertEquals(5, process.getFlowElements().size()); + + // 验证开始节点 + StartEvent startEvent = (StartEvent) process.getFlowElement("start"); + assertNotNull(startEvent); + assertEquals("开始", startEvent.getName()); + + // 验证服务任务节点 + ServiceTask serviceTask = (ServiceTask) process.getFlowElement("service1"); + assertNotNull(serviceTask); + assertEquals("服务任务", serviceTask.getName()); + assertEquals("${shellTaskDelegate}", serviceTask.getImplementation()); + + // 验证服务任务字段 + List fieldExtensions = serviceTask.getFieldExtensions(); + assertNotNull(fieldExtensions); + assertEquals(3, fieldExtensions.size()); + + Map fieldMap = new HashMap<>(); + for (FieldExtension field : fieldExtensions) { + fieldMap.put(field.getFieldName(), field.getStringValue()); + } + + assertEquals("echo 'Hello World'", fieldMap.get("script")); + assertEquals("/tmp", fieldMap.get("workDir")); + assertEquals("30", fieldMap.get("timeout")); + + // 验证结束节点 + EndEvent endEvent = (EndEvent) process.getFlowElement("end"); + assertNotNull(endEvent); + assertEquals("结束", endEvent.getName()); + + // 验证边 + SequenceFlow flow1 = (SequenceFlow) process.getFlowElement("flow1"); + assertNotNull(flow1); + assertEquals("流转到服务任务", flow1.getName()); + assertEquals("start", flow1.getSourceRef()); + assertEquals("service1", flow1.getTargetRef()); + + SequenceFlow flow2 = (SequenceFlow) process.getFlowElement("flow2"); + assertNotNull(flow2); + assertEquals("流转到结束", flow2.getName()); + assertEquals("service1", flow2.getSourceRef()); + assertEquals("end", flow2.getTargetRef()); } @Test - void testConvertShellTask() throws Exception { - // 准备测试数据 - String json = """ - { - "cells": [ - { - "id": "start", - "shape": "start", - "data": { - "label": "开始" - } - }, - { - "id": "shell1", - "shape": "shellTask", - "data": { - "label": "Shell脚本", - "serviceTask": { - "fields": { - "script": "echo 'Hello World'", - "workDir": "/tmp" - } - } - } - }, - { - "id": "flow1", - "shape": "edge", - "source": "start", - "target": "shell1", - "data": { - "label": "流转到Shell" - } - } - ] - } - """; - + void testConvertUserTask() { + // 创建一个包含用户任务的工作流图 + WorkflowGraph graph = new WorkflowGraph(); + + // 创建节点 + List nodes = new ArrayList<>(); + + // 开始节点 + WorkflowNode startNode = new WorkflowNode(); + startNode.setId("start"); + startNode.setType(NodeType.START); + startNode.setName("开始"); + startNode.setPosition(new Position(100.0, 100.0)); + nodes.add(startNode); + + // 用户任务节点 + WorkflowNode userNode = new WorkflowNode(); + userNode.setId("user1"); + userNode.setType(NodeType.USER_TASK); + userNode.setName("审批任务"); + userNode.setPosition(new Position(300.0, 100.0)); + + NodeConfig userConfig = new NodeConfig(); + userConfig.setAssignee("${initiator}"); + userConfig.setCandidateUsers(Arrays.asList("user1", "user2")); + userConfig.setCandidateGroups(Arrays.asList("group1", "group2")); + userConfig.setDueDate("${dueDate}"); + userConfig.setPriority(1); + userConfig.setFormKey("form1"); + userNode.setConfig(userConfig); + nodes.add(userNode); + + // 结束节点 + WorkflowNode endNode = new WorkflowNode(); + endNode.setId("end"); + endNode.setType(NodeType.END); + endNode.setName("结束"); + endNode.setPosition(new Position(500.0, 100.0)); + nodes.add(endNode); + + // 创建边 + List edges = new ArrayList<>(); + + // 开始 -> 用户任务 + WorkflowEdge edge1 = new WorkflowEdge(); + edge1.setId("flow1"); + edge1.setSource("start"); + edge1.setTarget("user1"); + edge1.setName("提交审批"); + edges.add(edge1); + + // 用户任务 -> 结束 + WorkflowEdge edge2 = new WorkflowEdge(); + edge2.setId("flow2"); + edge2.setSource("user1"); + edge2.setTarget("end"); + edge2.setName("审批完成"); + edges.add(edge2); + + graph.setNodes(nodes); + graph.setEdges(edges); + // 执行转换 - String xml = bpmnConverter.convertToBpmnXml(json, "test_process"); + BpmnModel bpmnModel = bpmnConverter.convertToBpmnModel(graph); + + // 打印XML + try { + System.out.println("\n=== User Task Workflow XML ==="); + System.out.println(new String(new BpmnXMLConverter().convertToXML(bpmnModel))); + } catch (Exception e) { + e.printStackTrace(); + } // 验证结果 - assertNotNull(xml); - assertTrue(xml.contains("flowable:delegateExpression=\"${shellTaskDelegate}\"")); - assertTrue(xml.contains("")); - assertTrue(xml.contains("")); + assertNotNull(bpmnModel); + Process process = bpmnModel.getMainProcess(); + assertNotNull(process); - // 验证节点配置 - assertTrue(nodeManager.hasNodeType("shellTask")); - assertNotNull(nodeManager.getNodeConfig("shellTask")); - assertEquals("Shell脚本", nodeManager.getNodeConfig("shellTask").getName()); - assertTrue(nodeManager.getNodeConfig("shellTask").isAsync()); + // 验证用户任务节点 + UserTask userTask = (UserTask) process.getFlowElement("user1"); + assertNotNull(userTask); + assertEquals("审批任务", userTask.getName()); + assertEquals("${initiator}", userTask.getAssignee()); + assertEquals(Arrays.asList("user1", "user2"), userTask.getCandidateUsers()); + assertEquals(Arrays.asList("group1", "group2"), userTask.getCandidateGroups()); + assertEquals("${dueDate}", userTask.getDueDate()); + assertEquals("1", userTask.getPriority()); + assertEquals("form1", userTask.getFormKey()); } @Test - void testConvertComplexProcess() throws Exception { - // 从测试资源文件加载复杂流程JSON - ClassPathResource resource = new ClassPathResource("test-process.json"); - String json = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + void testConvertComplexWorkflow() { + // 创建一个复杂的工作流图:开始 -> 服务任务 -> 用户任务 -> 脚本任务 -> 结束 + WorkflowGraph graph = new WorkflowGraph(); + + // 设置流程属性 + Map properties = new HashMap<>(); + properties.put("name", "测试流程"); + properties.put("key", "test_process"); + properties.put("description", "这是一个测试流程"); + graph.setProperties(properties); + + // 创建节点 + List nodes = new ArrayList<>(); + + // 开始节点 + WorkflowNode startNode = new WorkflowNode(); + startNode.setId("start"); + startNode.setType(NodeType.START); + startNode.setName("开始"); + startNode.setPosition(new Position(100.0, 100.0)); + nodes.add(startNode); + + // 服务任务节点 + WorkflowNode serviceNode = new WorkflowNode(); + serviceNode.setId("service1"); + serviceNode.setType(NodeType.SERVICE_TASK); + serviceNode.setName("服务任务"); + serviceNode.setPosition(new Position(250.0, 100.0)); + + NodeConfig serviceConfig = new NodeConfig(); + serviceConfig.setImplementation("${shellTaskDelegate}"); + Map serviceFields = new HashMap<>(); + serviceFields.put("script", "echo 'Hello World'"); + serviceFields.put("workDir", "/tmp"); + serviceConfig.setFields(serviceFields); + serviceNode.setConfig(serviceConfig); + nodes.add(serviceNode); + + // 用户任务节点 + WorkflowNode userNode = new WorkflowNode(); + userNode.setId("user1"); + userNode.setType(NodeType.USER_TASK); + userNode.setName("用户任务"); + userNode.setPosition(new Position(400.0, 100.0)); + + NodeConfig userConfig = new NodeConfig(); + userConfig.setAssignee("${initiator}"); + List candidateUsers = Arrays.asList("user1", "user2"); + List candidateGroups = Arrays.asList("group1", "group2"); + userConfig.setCandidateUsers(candidateUsers); + userConfig.setCandidateGroups(candidateGroups); + userConfig.setPriority(1); + userConfig.setFormKey("form1"); + userNode.setConfig(userConfig); + nodes.add(userNode); + + // 脚本任务节点 + WorkflowNode scriptNode = new WorkflowNode(); + scriptNode.setId("script1"); + scriptNode.setType(NodeType.SCRIPT_TASK); + scriptNode.setName("脚本任务"); + scriptNode.setPosition(new Position(550.0, 100.0)); + + NodeConfig scriptConfig = new NodeConfig(); + scriptConfig.setImplementation("groovy"); + Map scriptFields = new HashMap<>(); + scriptFields.put("script", "println 'Hello from Groovy'"); + scriptConfig.setFields(scriptFields); + scriptNode.setConfig(scriptConfig); + nodes.add(scriptNode); + + // 结束节点 + WorkflowNode endNode = new WorkflowNode(); + endNode.setId("end"); + endNode.setType(NodeType.END); + endNode.setName("结束"); + endNode.setPosition(new Position(700.0, 100.0)); + nodes.add(endNode); + + // 创建边 + List edges = new ArrayList<>(); + + // 开始 -> 服务任务 + WorkflowEdge edge1 = new WorkflowEdge(); + edge1.setId("flow1"); + edge1.setSource("start"); + edge1.setTarget("service1"); + edge1.setName("流转到服务任务"); + EdgeConfig edgeConfig1 = new EdgeConfig(); + edgeConfig1.setType("sequence"); + edge1.setConfig(edgeConfig1); + edges.add(edge1); + + // 服务任务 -> 用户任务 + WorkflowEdge edge2 = new WorkflowEdge(); + edge2.setId("flow2"); + edge2.setSource("service1"); + edge2.setTarget("user1"); + edge2.setName("流转到用户任务"); + EdgeConfig edgeConfig2 = new EdgeConfig(); + edgeConfig2.setType("sequence"); + edgeConfig2.setCondition("${approved}"); + edge2.setConfig(edgeConfig2); + edges.add(edge2); + + // 用户任务 -> 脚本任务 + WorkflowEdge edge3 = new WorkflowEdge(); + edge3.setId("flow3"); + edge3.setSource("user1"); + edge3.setTarget("script1"); + edge3.setName("流转到脚本任务"); + EdgeConfig edgeConfig3 = new EdgeConfig(); + edgeConfig3.setType("sequence"); + edge3.setConfig(edgeConfig3); + edges.add(edge3); + + // 脚本任务 -> 结束 + WorkflowEdge edge4 = new WorkflowEdge(); + edge4.setId("flow4"); + edge4.setSource("script1"); + edge4.setTarget("end"); + edge4.setName("流转到结束"); + EdgeConfig edgeConfig4 = new EdgeConfig(); + edgeConfig4.setType("sequence"); + edge4.setConfig(edgeConfig4); + edges.add(edge4); + + graph.setNodes(nodes); + graph.setEdges(edges); + + // 执行转换并获取XML + String xml = bpmnConverter.convertToXml(graph, "test_process"); + System.out.println("Generated BPMN XML:"); + System.out.println(xml); // 执行转换 - String xml = bpmnConverter.convertToBpmnXml(json, "complex_process"); + BpmnModel bpmnModel = bpmnConverter.convertToBpmnModel(graph); + + // 打印XML + try { + System.out.println("\n=== Complex Workflow XML ==="); + System.out.println(new String(new BpmnXMLConverter().convertToXML(bpmnModel))); + } catch (Exception e) { + e.printStackTrace(); + } // 验证结果 - assertNotNull(xml); - // 验证基本结构 - assertTrue(xml.contains(" { + // 清空现有图形 + graph.clearCells(); + + // 先添加所有节点 + const nodes = graphJson.cells.filter(cell => cell.shape !== 'edge'); + nodes.forEach(node => { + graph.addNode({ + id: node.id, + shape: node.shape, + x: node.position?.x || 0, + y: node.position?.y || 0, + label: node.data?.label || '', + data: { + ...node.data, + type: node.shape, + }, + }); + }); + + // 再添加所有边 + const edges = graphJson.cells.filter(cell => cell.shape === 'edge'); + edges.forEach(edge => { + graph.addEdge({ + id: edge.id, + source: edge.source, + target: edge.target, + label: edge.data?.label || '', + attrs: { + line: { + stroke: '#5F95FF', + strokeWidth: 1, + targetMarker: { + name: 'classic', + size: 8, + }, + }, + }, + }); + }); + + // 自动布局 + graph.centerContent(); + graph.zoomToFit({ padding: 20 }); +};