修改工作流数据结构。
This commit is contained in:
parent
1d2b3eea5d
commit
40e2aba12c
@ -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)
|
||||
@ -33,12 +36,19 @@ public class WorkflowDefinitionDTO extends BaseDTO {
|
||||
*/
|
||||
private String bpmnXml;
|
||||
|
||||
private JsonNode graphConfig;
|
||||
|
||||
private JsonNode flowableConfig;
|
||||
/**
|
||||
* 图形数据
|
||||
*/
|
||||
private WorkflowGraph graph;
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
private JsonNode formConfig;
|
||||
|
||||
/**
|
||||
* 流程状态
|
||||
*/
|
||||
private WorkflowStatusEnums status;
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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<String, Object> fields;
|
||||
|
||||
/**
|
||||
* 任务处理人
|
||||
*/
|
||||
private String assignee;
|
||||
|
||||
/**
|
||||
* 候选用户列表
|
||||
*/
|
||||
private List<String> candidateUsers;
|
||||
|
||||
/**
|
||||
* 候选组列表
|
||||
*/
|
||||
private List<String> candidateGroups;
|
||||
|
||||
/**
|
||||
* 到期时间
|
||||
*/
|
||||
private String dueDate;
|
||||
|
||||
/**
|
||||
* 优先级
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 表单标识
|
||||
*/
|
||||
private String formKey;
|
||||
|
||||
/**
|
||||
* 跳过表达式
|
||||
*/
|
||||
private String skipExpression;
|
||||
|
||||
/**
|
||||
* 是否异步
|
||||
*/
|
||||
private Boolean isAsync;
|
||||
|
||||
/**
|
||||
* 是否排他
|
||||
*/
|
||||
private Boolean exclusive;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<String, Object> properties;
|
||||
}
|
||||
@ -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<WorkflowNode> nodes;
|
||||
|
||||
/**
|
||||
* 边列表
|
||||
*/
|
||||
private List<WorkflowEdge> edges;
|
||||
|
||||
/**
|
||||
* 工作流属性
|
||||
*/
|
||||
private Map<String, Object> properties;
|
||||
}
|
||||
@ -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<String, Object> properties;
|
||||
|
||||
/**
|
||||
* 节点大小
|
||||
*/
|
||||
private Size size;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -48,24 +48,22 @@ public class WorkflowDefinition extends Entity<Long> {
|
||||
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<Long> {
|
||||
/**
|
||||
* 流程描述
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
@ -92,7 +92,6 @@ public class WorkflowNodeDefinition extends Entity<Long> {
|
||||
/**
|
||||
* 排序号
|
||||
*/
|
||||
@Type(JsonType.class)
|
||||
@Column(nullable = false)
|
||||
private Integer orderNum = 0;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<WorkflowDefin
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public WorkflowDefinitionDTO saveWorkflowDesign(WorkflowDefinitionDTO dto) throws Exception {
|
||||
// 转换图形JSON为BPMN XML
|
||||
String bpmnXml = bpmnConverter.convertToBpmnXml(dto.getGraphConfig().toString(), dto.getKey());
|
||||
|
||||
String bpmnXml = bpmnConverter.convertToXml(dto.getGraph(), dto.getKey());
|
||||
dto.setFlowVersion(1);
|
||||
dto.setBpmnXml(bpmnXml);
|
||||
// // 创建工作流定义
|
||||
|
||||
@ -1,294 +1,305 @@
|
||||
package com.qqchen.deploy.backend.workflow.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qqchen.deploy.backend.workflow.constants.BpmnConstants;
|
||||
import com.qqchen.deploy.backend.workflow.enums.BpmnNodeTypeEnums;
|
||||
import com.qqchen.deploy.backend.workflow.handler.BpmnNodeHandler;
|
||||
import com.qqchen.deploy.backend.workflow.model.BpmnNodeConfig;
|
||||
import com.qqchen.deploy.backend.workflow.parser.BpmnNodeConfigParser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.flowable.bpmn.BpmnAutoLayout;
|
||||
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.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* BPMN转换工具类
|
||||
* BPMN 模型转换工具
|
||||
* @author cascade
|
||||
* @date 2024-12-11
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BpmnConverter {
|
||||
|
||||
private final Map<BpmnNodeTypeEnums, BpmnNodeHandler<?>> handlers;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final BpmnNodeConfigParser configParser;
|
||||
private final BpmnXMLConverter bpmnXMLConverter;
|
||||
private final BpmnXMLConverter bpmnXmlConverter = new BpmnXMLConverter();
|
||||
|
||||
public BpmnConverter(ObjectMapper objectMapper,
|
||||
List<BpmnNodeHandler<?>> 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<String, FlowElement> 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<String, Object> 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<String, FlowElement> 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<String, FlowElement> 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<FlowElement> 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<FlowElement> 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<String, FlowElement> 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<String, FlowElement> elementMap) {
|
||||
cells.forEach(cell -> {
|
||||
if (isEdge(cell)) {
|
||||
handleSequenceFlow(cell, process, elementMap);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ensureStartAndEndEvents(Process process, Map<String, FlowElement> 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 extends FlowElement> T createAndHandleElement(
|
||||
BpmnNodeHandler<T> 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<String, FlowElement> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 '乐观锁版本号',
|
||||
CREATE TABLE workflow_definition
|
||||
(
|
||||
-- 主键
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||
|
||||
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',
|
||||
-- 基础信息
|
||||
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 '流程分类',
|
||||
|
||||
CONSTRAINT UK_workflow_definition_key_version UNIQUE (`key`, flow_version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 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 (
|
||||
|
||||
@ -156,305 +156,141 @@ INSERT INTO sys_external_system (
|
||||
-- 初始化工作流相关数据
|
||||
-- --------------------------------------------------------------------------------------
|
||||
|
||||
-- 初始化工作流节点定义数据
|
||||
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
|
||||
-- 工作流定义测试数据
|
||||
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
|
||||
-- 开始节点
|
||||
(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
|
||||
-- 简单流程:开始 -> 服务任务 -> 结束
|
||||
(
|
||||
'简单服务任务流程', 'simple_service_flow', 1, '一个包含服务任务的简单流程', 'test',
|
||||
'<?xml version="1.0" encoding="UTF-8"?><definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/test"><process id="simple_service_flow" name="简单服务任务流程" isExecutable="true"><startEvent id="start1" name="开始"></startEvent><serviceTask id="service1" name="服务任务"><extensionElements><flowable:field name="script"><flowable:string><![CDATA[echo "Hello World"]]></flowable:string></flowable:field></extensionElements></serviceTask><endEvent id="end1" name="结束"></endEvent><sequenceFlow id="flow1" sourceRef="start1" targetRef="service1"></sequenceFlow><sequenceFlow id="flow2" sourceRef="service1" targetRef="end1"></sequenceFlow></process></definitions>',
|
||||
'{"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
|
||||
),
|
||||
|
||||
-- 结束节点
|
||||
(2, 'endEvent', '结束节点', '流程的结束节点', 'EVENT',
|
||||
-- Flowable配置
|
||||
'{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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
|
||||
-- 用户任务流程:开始 -> 用户任务 -> 结束
|
||||
(
|
||||
'用户审批流程', 'user_approval_flow', 1, '一个简单的用户审批流程', 'approval',
|
||||
'<?xml version="1.0" encoding="UTF-8"?><definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/test"><process id="user_approval_flow" name="用户审批流程"
|
||||
isExecutable="true"><startEvent id="start1" name="开始"></startEvent><userTask id="user1" name="审批任务" flowable:assignee="$${initiator}" flowable:candidateUsers="user1,user2" flowable:candidateGroups="group1,group2" flowable:dueDate="$${dueDate}" flowable:formKey="form1"
|
||||
flowable:priority="1"></userTask><endEvent id="end1" name="结束"></endEvent><sequenceFlow id="flow1" name="提交审批" sourceRef="start1" targetRef="user1"></sequenceFlow><sequenceFlow id="flow2" name="审批完成" sourceRef="user1" targetRef="end1"></sequenceFlow></process></definitions>',
|
||||
'{"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
|
||||
),
|
||||
|
||||
-- Shell脚本节点
|
||||
(3, 'shellTask', 'Shell脚本', '执行Shell命令的节点', 'TASK',
|
||||
-- Flowable配置
|
||||
'{
|
||||
"type": "shellTask",
|
||||
"delegateExpression": "$${shellTaskDelegate}",
|
||||
"listeners": [
|
||||
{
|
||||
"event": "start",
|
||||
"delegateExpression": "$${shellTaskStartListener}"
|
||||
},
|
||||
{
|
||||
"event": "end",
|
||||
"delegateExpression": "$${shellTaskEndListener}"
|
||||
}
|
||||
]
|
||||
}',
|
||||
-- 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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
|
||||
-- 复杂流程:开始 -> 服务任务 -> 用户任务 -> 脚本任务 -> 结束
|
||||
(
|
||||
'复杂业务流程', 'complex_business_flow', 1, '包含多种任务节点的复杂业务流程', 'business',
|
||||
'<?xml version="1.0" encoding="UTF-8"?><definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/test"><process id="complex_business_flow"
|
||||
name="复杂业务流程" isExecutable="true"><startEvent id="start1" name="开始"></startEvent><serviceTask id="service1" name="服务任务"><extensionElements><flowable:field name="implementation"><flowable:string><![CDATA[com.example.ServiceTask]]></flowable:string></flowable:field></extensionElements></serviceTask><userTask id="user1" name="用户任务" flowable:assignee="user1"></userTask><scriptTask id="script1" name="脚本任务" scriptFormat="groovy"><script><![CDATA[println "Hello"]]></script></scriptTask><endEvent id="end1" name="结束"></endEvent><sequenceFlow id="flow1" sourceRef="start1" targetRef="service1"></sequenceFlow><sequenceFlow id="flow2" sourceRef="service1" targetRef="user1"><conditionExpression xsi:type="tFormalExpression">$${condition}</conditionExpression></sequenceFlow><sequenceFlow id="flow3" sourceRef="user1" targetRef="script1"></sequenceFlow><sequenceFlow id="flow4" sourceRef="script1" targetRef="end1"></sequenceFlow></process></definitions>',
|
||||
'{"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
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------------------------
|
||||
-- 初始化工作流节点定义数据
|
||||
-- --------------------------------------------------------------------------------------
|
||||
|
||||
-- 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": "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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}',
|
||||
'{
|
||||
"type": "object",
|
||||
"required": ["shape"],
|
||||
"properties": {
|
||||
"shape": {
|
||||
"type": "string",
|
||||
"const": "serviceTask",
|
||||
"description": "节点形状"
|
||||
}
|
||||
}
|
||||
}',
|
||||
'{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}',
|
||||
10,
|
||||
TRUE
|
||||
);
|
||||
@ -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<WorkflowNode> 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<String, Object> 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<WorkflowEdge> 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<FieldExtension> fieldExtensions = serviceTask.getFieldExtensions();
|
||||
assertNotNull(fieldExtensions);
|
||||
assertEquals(3, fieldExtensions.size());
|
||||
|
||||
Map<String, String> 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<WorkflowNode> 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<WorkflowEdge> 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("<flowable:field name=\"script\">"));
|
||||
assertTrue(xml.contains("<flowable:field name=\"workDir\">"));
|
||||
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<String, Object> properties = new HashMap<>();
|
||||
properties.put("name", "测试流程");
|
||||
properties.put("key", "test_process");
|
||||
properties.put("description", "这是一个测试流程");
|
||||
graph.setProperties(properties);
|
||||
|
||||
// 创建节点
|
||||
List<WorkflowNode> 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<String, Object> 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<String> candidateUsers = Arrays.asList("user1", "user2");
|
||||
List<String> 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<String, Object> 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<WorkflowEdge> 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("<process id=\"complex_process\""));
|
||||
// 验证节点
|
||||
assertTrue(xml.contains("<startEvent"));
|
||||
assertTrue(xml.contains("<serviceTask"));
|
||||
assertTrue(xml.contains("<endEvent"));
|
||||
assertNotNull(bpmnModel);
|
||||
Process process = bpmnModel.getMainProcess();
|
||||
assertNotNull(process);
|
||||
|
||||
// 验证流程属性
|
||||
assertEquals("测试流程", process.getName());
|
||||
assertEquals("test_process", process.getId());
|
||||
assertEquals("这是一个测试流程", process.getDocumentation());
|
||||
|
||||
// 验证节点数量
|
||||
assertEquals(9, process.getFlowElements().size()); // 5个节点 + 4个连线
|
||||
|
||||
// 验证开始节点
|
||||
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());
|
||||
|
||||
// 验证用户任务
|
||||
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());
|
||||
|
||||
// 验证脚本任务
|
||||
ScriptTask scriptTask = (ScriptTask) process.getFlowElement("script1");
|
||||
assertNotNull(scriptTask);
|
||||
assertEquals("脚本任务", scriptTask.getName());
|
||||
assertEquals("groovy", scriptTask.getScriptFormat());
|
||||
|
||||
// 验证结束节点
|
||||
EndEvent endEvent = (EndEvent) process.getFlowElement("end");
|
||||
assertNotNull(endEvent);
|
||||
assertEquals("结束", endEvent.getName());
|
||||
|
||||
// 验证连线
|
||||
assertTrue(xml.contains("<sequenceFlow"));
|
||||
|
||||
// 验证节点配置
|
||||
assertTrue(nodeManager.hasNodeType("shellTask"));
|
||||
assertNotNull(nodeManager.getNodeConfig("shellTask"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNodeRegistration() {
|
||||
// 验证节点注册
|
||||
assertTrue(nodeManager.hasNodeType("shellTask"));
|
||||
|
||||
// 验证Shell任务节点配置
|
||||
var config = nodeManager.getNodeConfig("shellTask");
|
||||
assertNotNull(config);
|
||||
assertEquals("Shell脚本", config.getName());
|
||||
assertTrue(config.isAsync());
|
||||
assertTrue(config.getRequiredFields().contains("script"));
|
||||
assertTrue(config.getRequiredFields().contains("workDir"));
|
||||
assertEquals("${shellTaskDelegate}", config.getDelegateExpression());
|
||||
SequenceFlow flow2 = (SequenceFlow) process.getFlowElement("flow2");
|
||||
assertNotNull(flow2);
|
||||
assertEquals("${approved}", flow2.getConditionExpression());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import { Graph } from '@antv/x6';
|
||||
import { NodeData } from '../types';
|
||||
|
||||
export interface GraphJsonCell {
|
||||
id: string;
|
||||
shape: string;
|
||||
data?: {
|
||||
label: string;
|
||||
serviceTask?: {
|
||||
type: string;
|
||||
implementation: string;
|
||||
fields: any;
|
||||
};
|
||||
};
|
||||
position?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export interface GraphJson {
|
||||
cells: GraphJsonCell[];
|
||||
}
|
||||
|
||||
export const loadGraphData = (graph: Graph, graphJson: GraphJson) => {
|
||||
// 清空现有图形
|
||||
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 });
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user