修改工作流数据结构。

This commit is contained in:
dengqichen 2024-12-11 17:15:20 +08:00
parent 1d2b3eea5d
commit 40e2aba12c
19 changed files with 1267 additions and 669 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,7 +92,6 @@ public class WorkflowNodeDefinition extends Entity<Long> {
/**
* 排序号
*/
@Type(JsonType.class)
@Column(nullable = false)
private Integer orderNum = 0;

View File

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

View File

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

View File

@ -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);
// // 创建工作流定义

View File

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

View File

@ -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 '流程标识',
-- 基础信息
name VARCHAR(255) NOT NULL COMMENT '流程名称',
`key` VARCHAR(255) 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',
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 (

View File

@ -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配置
-- 复杂流程:开始 -> 服务任务 -> 用户任务 -> 脚本任务 -> 结束
(
'复杂业务流程', '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": "shellTask",
"delegateExpression": "$${shellTaskDelegate}",
"listeners": [
{
"event": "start",
"delegateExpression": "$${shellTaskStartListener}"
"type": "object",
"required": ["type", "implementation", "fields"],
"properties": {
"type": {
"type": "string",
"enum": ["shell"],
"description": "任务类型"
},
{
"event": "end",
"delegateExpression": "$${shellTaskEndListener}"
"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"
}
}
}
}
}
]
}',
-- 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"
"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"
},
"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"
"type": {
"type": "string",
"enum": ["input", "textarea", "select"]
},
"label": {
"text": "Shell脚本",
"fill": "#ffffff",
"fontSize": 12,
"textAnchor": "middle"
"type": "string"
},
"required": {
"type": "boolean"
}
}
}
}
}
}',
-- 表单配置
'{
"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
);

View File

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

View File

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