From 6dbfb2c878ad4daae5e870cb55691e7bfa2869ea Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 12 Dec 2024 11:10:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/workflow/config/NodeUiConfig.java | 123 +++++++ .../workflow/dto/graph/NodeConfig.java | 66 ---- .../workflow/dto/graph/WorkflowGraph.java | 4 - .../dto/nodeConfig/BaseTaskNodeConfig.java | 9 - .../backend/workflow/enums/NodeTypeEnums.java | 263 +++++++++++++- .../backend/workflow/util/BpmnConverter.java | 63 ++-- .../workflow/util/JsonSchemaValidator.java | 39 --- .../workflow/util/SchemaGenerator.java | 322 +++++++++++------- .../db/migration/V1.0.0__init_schema.sql | 1 - 9 files changed, 603 insertions(+), 287 deletions(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/config/NodeUiConfig.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/util/JsonSchemaValidator.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/config/NodeUiConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/config/NodeUiConfig.java new file mode 100644 index 00000000..a8088c72 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/config/NodeUiConfig.java @@ -0,0 +1,123 @@ +package com.qqchen.deploy.backend.workflow.config; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 节点UI配置类 + * 用于定义工作流节点的UI展示属性 + */ +@Data +@Accessors(chain = true) +public class NodeUiConfig { + private String shape; + private Size size; + private Style style; + private Ports ports; + + public NodeUiConfig setSize(int width, int height) { + this.size = new Size(width, height); + return this; + } + + public NodeUiConfig setStyle(String fill, String stroke, String icon) { + this.style = new Style(fill, stroke, icon); + return this; + } + + public NodeUiConfig setPorts(List types) { + this.ports = new Ports(); + Map groups = new HashMap<>(); + + types.forEach(type -> { + PortGroup group = new PortGroup(); + group.setPosition(type.equals("in") ? "left" : "right"); + + PortCircle circle = new PortCircle(); + circle.setR(4); + circle.setFill("#ffffff"); + circle.setStroke("#1890ff"); + + PortAttrs attrs = new PortAttrs(); + attrs.setCircle(circle); + + group.setAttrs(attrs); + groups.put(type, group); + }); + + this.ports.setGroups(groups); + return this; + } + + /** + * 节点大小配置 + */ + @Data + public static class Size { + private int width; + private int height; + + public Size(int width, int height) { + this.width = width; + this.height = height; + } + } + + /** + * 节点样式配置 + */ + @Data + public static class Style { + private String fill; // 填充颜色 + private String stroke; // 边框颜色 + private String icon; // 图标名称 + private String iconColor; // 图标颜色 + private int strokeWidth = 2;// 边框宽度 + + public Style(String fill, String stroke, String icon) { + this.fill = fill; + this.stroke = stroke; + this.icon = icon; + this.iconColor = stroke; + } + } + + /** + * 节点连接点配置 + */ + @Data + public static class Ports { + private Map groups; + } + + /** + * 连接点分组配置 + */ + @Data + public static class PortGroup { + private String position; // 位置:left, right, top, bottom + private PortAttrs attrs; // 连接点属性 + } + + /** + * 连接点属性配置 + */ + @Data + public static class PortAttrs { + private PortCircle circle; // 圆形连接点 + } + + /** + * 圆形连接点配置 + */ + @Data + public static class PortCircle { + private int r; // 半径 + private String fill; // 填充颜色 + private String stroke; // 边框颜色 + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java index 5f2244aa..ceb23baa 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/NodeConfig.java @@ -13,73 +13,7 @@ import java.util.Map; @Data @JsonInclude(JsonInclude.Include.NON_NULL) // 只序列化非空字段 public class NodeConfig { - /** - * 配置类型 - */ - private String type; - /** - * 实现类 - */ - private String implementation; - /** - * 字段配置 - */ - private Map fields; - /** - * 任务处理人 - */ - private String assignee; - - /** - * 候选用户列表 - */ - private List candidateUsers; - - /** - * 候选组列表 - */ - private List candidateGroups; - - /** - * 到期时间 - */ - private String dueDate; - - /** - * 优先级 - */ - private Integer priority; - - /** - * 表单标识 - */ - private String formKey; - - /** - * 跳过表达式 - */ - private String skipExpression; - - /** - * 是否异步 - */ - private Boolean isAsync; - - /** - * 是否排他 - */ - private Boolean exclusive; - - /** - * 脚本格式 - */ - private String scriptFormat; - - /** - * 脚本内容 - */ - private String script; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java index bd8b19eb..0592d7b8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/graph/WorkflowGraph.java @@ -21,8 +21,4 @@ public class WorkflowGraph { */ private List edges; - /** - * 工作流属性 - */ - private Map properties; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseTaskNodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseTaskNodeConfig.java index bfae98f0..13f2f487 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseTaskNodeConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseTaskNodeConfig.java @@ -1,7 +1,6 @@ package com.qqchen.deploy.backend.workflow.dto.nodeConfig; import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty; -import com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener.ListenerNodeConfig; import lombok.Data; import java.util.List; @@ -84,14 +83,6 @@ public class BaseTaskNodeConfig extends BaseNodeConfig { ) private String retryStrategy; - /** - * 监听器配置列表 - */ - @SchemaProperty( - title = "监听器配置", - description = "工作流节点的事件监听器配置列表" - ) - private List listeners; /** * 是否异步 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnums.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnums.java index b35f1bfd..e2c22696 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnums.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnums.java @@ -1,37 +1,270 @@ package com.qqchen.deploy.backend.workflow.enums; import com.fasterxml.jackson.annotation.JsonValue; +import com.qqchen.deploy.backend.workflow.config.NodeUiConfig; import lombok.Getter; +import java.util.Arrays; +/** + * 工作流节点类型枚举 + * 定义了工作流中所有可用的节点类型及其UI配置 + * + * 节点类型说明: + * 1. 事件类节点(Event):用于标记流程的开始和结束 + * 2. 任务类节点(Task):执行具体的业务操作 + * 3. 网关类节点(Gateway):控制流程的流转方向 + * 4. 容器类节点:包含子流程或调用其他流程 + */ @Getter public enum NodeTypeEnums { - START_EVENT("START_EVENT", "开始节点"), - END_EVENT("END_EVENT", "结束节点"), - USER_TASK("USER_TASK", "用户任务"), - SERVICE_TASK("SERVICE_TASK", "服务任务"), - SCRIPT_TASK("SCRIPT_TASK", "脚本任务"), - EXCLUSIVE_GATEWAY("EXCLUSIVE_GATEWAY", "排他网关"), - PARALLEL_GATEWAY("PARALLEL_GATEWAY", "并行网关"), - SUBPROCESS("SUB_PROCESS", "子流程"), - CALL_ACTIVITY("CALL_ACTIVITY", "调用活动"); + /** + * 开始节点 - 流程的起点 + * 功能: + * - 标记流程的开始位置 + * - 可以定义流程启动条件 + * - 可以初始化流程变量 + * + * 使用场景: + * - 用户手动启动流程 + * - 定时触发流程 + * - 外部系统调用启动 + * + * 限制: + * - 每个流程必须有且仅有一个开始节点 + * - 只能有出边,不能有入边 + */ + START_EVENT("START_EVENT", "开始节点", new NodeUiConfig() + .setShape("circle") + .setSize(40, 40) + .setStyle("#e8f7ff", "#1890ff", "play-circle") + .setPorts(Arrays.asList("out"))), + /** + * 结束节点 - 流程的终点 + * 功能: + * - 标记流程的结束位置 + * - 可以定义流程结束时的清理操作 + * - 可以设置流程结果和返回值 + * + * 使用场景: + * - 流程正常结束 + * - 流程异常终止 + * - 需要返回处理结果 + * + * 限制: + * - 每个流程必须有且仅有一个结束节点 + * - 只能有入边,不能有出边 + */ + END_EVENT("END_EVENT", "结束节点", new NodeUiConfig() + .setShape("circle") + .setSize(40, 40) + .setStyle("#fff1f0", "#ff4d4f", "stop") + .setPorts(Arrays.asList("in"))), + + /** + * 用户任务节点 - 需要人工处理的任务 + * 功能: + * - 分配任务给指定用户或角色 + * - 支持任务表单的填写 + * - 可以设置处理期限和提醒 + * - 支持任务的转办、委托、退回 + * + * 使用场景: + * - 审批流程 + * - 表单填写 + * - 人工审核 + * - 数据确认 + * + * 配置项: + * - 处理人/角色 + * - 表单定义 + * - 处理期限 + * - 催办规则 + */ + USER_TASK("USER_TASK", "用户任务", new NodeUiConfig() + .setShape("rectangle") + .setSize(120, 60) + .setStyle("#ffffff", "#1890ff", "user") + .setPorts(Arrays.asList("in", "out"))), + + /** + * 服务任务节点 - 自动执行的系统服务 + * 功能: + * - 调用系统服务或外部接口 + * - 执行自动化操作 + * - 支持异步执行和结果回调 + * - 可以进行数据转换和处理 + * + * 使用场景: + * - 调用外部系统API + * - 发送通知消息 + * - 数据同步处理 + * - 自动化操作 + * + * 配置项: + * - 服务类型 + * - 接口参数 + * - 超时设置 + * - 重试策略 + */ + SERVICE_TASK("SERVICE_TASK", "服务任务", new NodeUiConfig() + .setShape("rectangle") + .setSize(120, 60) + .setStyle("#ffffff", "#1890ff", "api") + .setPorts(Arrays.asList("in", "out"))), + + /** + * 脚本任务节点 - 执行自定义脚本 + * 功能: + * - 执行自定义脚本代码 + * - 支持多种脚本语言 + * - 可以访问流程变量 + * - 支持复杂的业务逻辑 + * + * 使用场景: + * - 数据处理和转换 + * - 条件判断 + * - 自定义业务规则 + * - 系统集成 + * + * 配置项: + * - 脚本语言类型 + * - 脚本内容 + * - 资源限制 + * - 超时设置 + */ + SCRIPT_TASK("SCRIPT_TASK", "脚本任务", new NodeUiConfig() + .setShape("rectangle") + .setSize(120, 60) + .setStyle("#ffffff", "#1890ff", "code") + .setPorts(Arrays.asList("in", "out"))), + + /** + * 排他网关 - 条件分支 + * 功能: + * - 根据条件选择一个分支执行 + * - 支持复杂的条件表达式 + * - 可以设置默认分支 + * + * 使用场景: + * - 条件判断 + * - 分支选择 + * - 业务规则路由 + * + * 配置项: + * - 分支条件表达式 + * - 默认分支设置 + * - 条件优先级 + * + * 限制: + * - 必须至少有两个出口 + * - 条件必须互斥 + */ + EXCLUSIVE_GATEWAY("EXCLUSIVE_GATEWAY", "排他网关", new NodeUiConfig() + .setShape("diamond") + .setSize(50, 50) + .setStyle("#fff7e6", "#faad14", "fork") + .setPorts(Arrays.asList("in", "out"))), + + /** + * 并行网关 - 并行分支 + * 功能: + * - 将流程分成多个并行分支 + * - 等待所有分支完成后合并 + * - 支持复杂的并行处理 + * + * 使用场景: + * - 并行审批 + * - 多任务同时处理 + * - 并行数据处理 + * + * 配置项: + * - 分支数量 + * - 合并策略 + * - 超时设置 + * + * 限制: + * - 必须有对应的合并节点 + * - 所有分支都必须完成才能继续 + */ + PARALLEL_GATEWAY("PARALLEL_GATEWAY", "并行网关", new NodeUiConfig() + .setShape("diamond") + .setSize(50, 50) + .setStyle("#fff7e6", "#faad14", "branches") + .setPorts(Arrays.asList("in", "out"))), + + /** + * 子流程节点 - 嵌入式子流程 + * 功能: + * - 在当前流程中嵌入子流程 + * - 可以重用流程片段 + * - 支持事务处理 + * - 可以有自己的变量范围 + * + * 使用场景: + * - 流程复用 + * - 模块化处理 + * - 事务管理 + * - 错误处理 + * + * 配置项: + * - 子流程定义 + * - 输入参数映射 + * - 输出参数映射 + * - 错误处理策略 + */ + SUBPROCESS("SUB_PROCESS", "子流程", new NodeUiConfig() + .setShape("rectangle") + .setSize(120, 60) + .setStyle("#ffffff", "#1890ff", "apartment") + .setPorts(Arrays.asList("in", "out"))), + + /** + * 调用活动节点 - 外部流程调用 + * 功能: + * - 调用外部定义的流程 + * - 支持跨系统流程调用 + * - 可以传递和接收参数 + * - 支持异步调用 + * + * 使用场景: + * - 跨系统流程集成 + * - 公共流程复用 + * - 分布式流程处理 + * - 大型流程解耦 + * + * 配置项: + * - 目标流程定义 + * - 版本控制 + * - 参数映射 + * - 调用方式(同步/异步) + */ + CALL_ACTIVITY("CALL_ACTIVITY", "调用活动", new NodeUiConfig() + .setShape("rectangle") + .setSize(120, 60) + .setStyle("#ffffff", "#1890ff", "api") + .setPorts(Arrays.asList("in", "out"))); @JsonValue private final String value; - private final String description; + private final NodeUiConfig uiConfig; - NodeTypeEnums(String value, String description) { + NodeTypeEnums(String value, String description, NodeUiConfig uiConfig) { this.value = value; this.description = description; + this.uiConfig = uiConfig; } - public String getCode() { - return value; - } - + /** + * 根据值查找对应的枚举 + * + * @param value 枚举值 + * @return 对应的枚举实例 + * @throws IllegalArgumentException 当找不到对应的枚举值时抛出 + */ public static NodeTypeEnums fromValue(String value) { for (NodeTypeEnums type : values()) { if (type.value.equals(value)) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java index 26d11df2..c97a2850 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java @@ -7,6 +7,7 @@ import org.flowable.bpmn.model.Process; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; /** @@ -156,15 +157,15 @@ public class BpmnConverter { 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); - }); - } +// 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; @@ -181,14 +182,14 @@ public class BpmnConverter { 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()); +// 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; @@ -205,11 +206,11 @@ public class BpmnConverter { 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()); - } +// 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; @@ -263,15 +264,15 @@ public class BpmnConverter { 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); - }); - } +// 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; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/JsonSchemaValidator.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/JsonSchemaValidator.java deleted file mode 100644 index 2f4bce5f..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/JsonSchemaValidator.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.qqchen.deploy.backend.workflow.util; - -import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SpecVersion; -import com.networknt.schema.ValidationMessage; -import com.qqchen.deploy.backend.framework.exception.BusinessException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.Set; -import java.util.stream.Collectors; - -@Slf4j -@Component -public class JsonSchemaValidator { - private final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - - /** - * 验证JSON数据是否符合Schema - */ - public void validate(JsonNode data, JsonNode schema) { - try { - JsonSchema jsonSchema = factory.getSchema(schema); - Set errors = jsonSchema.validate(data); - - if (!errors.isEmpty()) { - String errorMessages = errors.stream() - .map(ValidationMessage::getMessage) - .collect(Collectors.joining(", ")); - throw new IllegalStateException("JSON Schema validation failed: " + errorMessages); - } - } catch (Exception e) { - log.error("JSON Schema validation error", e); - throw new IllegalStateException("JSON Schema validation error: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGenerator.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGenerator.java index c41ab654..7142e6f2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGenerator.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGenerator.java @@ -11,6 +11,8 @@ import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnums; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Schema生成器 @@ -18,39 +20,46 @@ import java.util.List; public class SchemaGenerator { private static final ObjectMapper mapper = new ObjectMapper(); + /** + * 获取节点类型对应的配置类 + */ + private static Class getConfigClassForNodeType(NodeTypeEnums nodeType) { + switch (nodeType) { + case START_EVENT: + return StartNodeConfig.class; + case END_EVENT: + return EndNodeConfig.class; + case SCRIPT_TASK: + return ScriptNodeConfig.class; + // 其他节点类型的配置类 + case USER_TASK: + case SERVICE_TASK: + case EXCLUSIVE_GATEWAY: + case PARALLEL_GATEWAY: + case SUBPROCESS: + case CALL_ACTIVITY: + // TODO: 为其他节点类型添加对应的配置类 + return BaseNodeConfig.class; + default: + return BaseNodeConfig.class; + } + } + /** * 生成所有节点类型的schema */ public static JsonNode generateAllNodeTypeSchemas() { - ObjectNode schemas = mapper.createObjectNode(); + ArrayNode schemas = mapper.createArrayNode(); -// // 生成开始节点schema -// ObjectNode startNode = generateNodeSchema("START", "开始", "开始节点", StartNodeConfig.class); -// schemas.set("START", startNode); -// -// // 生成脚本节点schema -// ObjectNode scriptNode = generateNodeSchema("SCRIPT", "脚本执行", "执行脚本任务", ScriptNodeConfig.class); -// schemas.set("SCRIPT", scriptNode); -// -// // 生成Git克隆节点schema -// ObjectNode gitNode = generateNodeSchema("GIT_CLONE", "Git克隆", "克隆Git仓库", GitCloneConfig.class); -// schemas.set("GIT_CLONE", gitNode); -// -// -// // 生成事件节点schema -// ObjectNode eventNode = generateNodeSchema("EVENT", "事件", "事件节点", BaseEventNodeConfig.class); -// schemas.set("EVENT", eventNode); -// -// // 生成结束节点schema -// ObjectNode endNode = generateNodeSchema("END", "结束", "结束节点", EndNodeConfig.class); -// schemas.set("END", endNode); + // 遍历所有节点类型 + for (NodeTypeEnums nodeType : NodeTypeEnums.values()) { + // 获取节点类型对应的配置类 + Class configClass = getConfigClassForNodeType(nodeType); + // 生成该节点类型的schema + ObjectNode nodeSchema = generateNodeSchema(nodeType, configClass); + schemas.add(nodeSchema); + } - ObjectNode startNode = generateNodeSchema(NodeTypeEnums.START_EVENT, StartNodeConfig.class); - schemas.set(NodeTypeEnums.START_EVENT.getCode(), startNode); - ObjectNode scriptNode = generateNodeSchema(NodeTypeEnums.SCRIPT_TASK, ScriptNodeConfig.class); - schemas.set(NodeTypeEnums.SCRIPT_TASK.getCode(), scriptNode); - ObjectNode endNode = generateNodeSchema(NodeTypeEnums.END_EVENT, EndNodeConfig.class); - schemas.set(NodeTypeEnums.END_EVENT.getCode(), endNode); return schemas; } @@ -58,11 +67,18 @@ public class SchemaGenerator { * 生成节点schema */ private static ObjectNode generateNodeSchema(NodeTypeEnums nodeType, Class configClass) { - NodeTypeSchemaConfig node = new NodeTypeSchemaConfig(); - node.setCode(nodeType); - node.setDescription(nodeType.getDescription()); - ObjectNode schema = generateConfigSchema(configClass); - return schema; + ObjectNode node = mapper.createObjectNode(); + + // 设置基本信息 + node.put("code", nodeType.getCode()); + node.put("name", nodeType.getDescription()); + node.put("description", nodeType.getDescription()); + + // 生成配置schema并设置到configSchema字段 + ObjectNode configSchema = generateConfigSchema(configClass); + node.set("configSchema", configSchema); + + return node; } /** @@ -72,27 +88,15 @@ public class SchemaGenerator { ObjectNode schema = mapper.createObjectNode(); schema.put("type", "object"); + // 创建一个有序的properties对象 ObjectNode properties = mapper.createObjectNode(); List required = new ArrayList<>(); - // 处理所有字段 - for (Field field : configClass.getDeclaredFields()) { - SchemaProperty annotation = field.getAnnotation(SchemaProperty.class); - if (annotation != null) { - processField(properties, required, field, annotation); - } - } + // 先处理基础字段(按指定顺序) + processBaseFields(properties, required, configClass); - // 处理父类字段 - Class superClass = configClass.getSuperclass(); - if (superClass != null && !superClass.equals(Object.class)) { - for (Field field : superClass.getDeclaredFields()) { - SchemaProperty annotation = field.getAnnotation(SchemaProperty.class); - if (annotation != null) { - processField(properties, required, field, annotation); - } - } - } + // 处理其他字段 + processRemainingFields(properties, required, configClass); schema.set("properties", properties); if (!required.isEmpty()) { @@ -103,109 +107,183 @@ public class SchemaGenerator { return schema; } + /** + * 处理基础字段(code、name、description) + */ + private static void processBaseFields(ObjectNode properties, List required, Class configClass) { + String[] baseFields = {"code", "name", "description"}; + Class currentClass = configClass; + + while (currentClass != null && !currentClass.equals(Object.class)) { + for (String fieldName : baseFields) { + try { + Field field = currentClass.getDeclaredField(fieldName); + SchemaProperty annotation = field.getAnnotation(SchemaProperty.class); + if (annotation != null && !properties.has(fieldName)) { + processField(properties, required, field, annotation); + } + } catch (NoSuchFieldException ignored) { + // 字段不在当前类中,继续查找父类 + } + } + currentClass = currentClass.getSuperclass(); + } + } + + /** + * 处理剩余字段 + */ + private static void processRemainingFields(ObjectNode properties, List required, Class configClass) { + Set baseFields = Set.of("code", "name", "description"); + Class currentClass = configClass; + + while (currentClass != null && !currentClass.equals(Object.class)) { + for (Field field : currentClass.getDeclaredFields()) { + String fieldName = field.getName(); + if (!baseFields.contains(fieldName)) { + SchemaProperty annotation = field.getAnnotation(SchemaProperty.class); + if (annotation != null && !properties.has(fieldName)) { + processField(properties, required, field, annotation); + } + } + } + currentClass = currentClass.getSuperclass(); + } + } + /** * 处理字段 */ private static void processField(ObjectNode properties, List required, Field field, SchemaProperty annotation) { - ObjectNode property = properties.putObject(field.getName()); - - // 设置类型 - String type = getJsonSchemaType(field.getType()); - property.put("type", type); - - // 设置标题和描述 - if (!annotation.title().isEmpty()) { - property.put("title", annotation.title()); - } + ObjectNode fieldSchema = mapper.createObjectNode(); + Class fieldType = field.getType(); + + // 设置字段类型 + String jsonType = getJsonSchemaType(fieldType); + fieldSchema.put("type", jsonType); + + // 设置基本属性 + fieldSchema.put("title", annotation.title()); if (!annotation.description().isEmpty()) { - property.put("description", annotation.description()); + fieldSchema.put("description", annotation.description()); } - // 设置必填 + // 处理format + if (!annotation.format().isEmpty()) { + fieldSchema.put("format", annotation.format()); + } + + // 处理默认值 + if (!annotation.defaultValue().isEmpty()) { + setDefaultValue(fieldSchema, fieldType, annotation.defaultValue()); + } + + // 处理数值范围 + if (isNumberType(fieldType)) { + if (annotation.minimum() != Integer.MIN_VALUE) { + fieldSchema.put("minimum", annotation.minimum()); + } + if (annotation.maximum() != Integer.MAX_VALUE) { + fieldSchema.put("maximum", annotation.maximum()); + } + } + + // 处理枚举值 + if (annotation.enumValues().length > 0) { + ArrayNode enumValues = fieldSchema.putArray("enum"); + for (String value : annotation.enumValues()) { + enumValues.add(value); + } + + // 如果有枚举显示名称 + if (annotation.enumNames().length > 0) { + ArrayNode enumNames = fieldSchema.putArray("enumNames"); + for (String name : annotation.enumNames()) { + enumNames.add(name); + } + } + } + + // 处理数组类型 + if (jsonType.equals("array")) { + ObjectNode items = fieldSchema.putObject("items"); + if (field.getType().isArray()) { + Class componentType = field.getType().getComponentType(); + items.put("type", getJsonSchemaType(componentType)); + } else { + // 默认为字符串类型 + items.put("type", "string"); + } + } + + // 处理对象类型 + if (jsonType.equals("object") && Map.class.isAssignableFrom(fieldType)) { + ObjectNode additionalProperties = fieldSchema.putObject("additionalProperties"); + additionalProperties.put("type", "string"); + } + + // 如果是必填字段,添加到required列表 if (annotation.required()) { required.add(field.getName()); } - // 设置格式 - if (!annotation.format().isEmpty()) { - property.put("format", annotation.format()); - } + properties.set(field.getName(), fieldSchema); + } - // 设置默认值 - if (!annotation.defaultValue().isEmpty()) { - setDefaultValue(property, type, annotation.defaultValue()); - } - - // 设置数值范围 - if ("integer".equals(type) || "number".equals(type)) { - if (annotation.minimum() != Integer.MIN_VALUE) { - property.put("minimum", annotation.minimum()); - } - if (annotation.maximum() != Integer.MAX_VALUE) { - property.put("maximum", annotation.maximum()); - } - } - - // 设置枚举值 - if (annotation.enumValues().length > 0) { - ArrayNode enumArray = property.putArray("enum"); - for (String value : annotation.enumValues()) { - enumArray.add(value); - } - - if (annotation.enumNames().length > 0) { - ArrayNode enumNamesArray = property.putArray("enumNames"); - for (String name : annotation.enumNames()) { - enumNamesArray.add(name); + /** + * 设置默认值(根据字段类型进行类型转换) + */ + private static void setDefaultValue(ObjectNode fieldSchema, Class fieldType, String defaultValue) { + if (isNumberType(fieldType)) { + try { + if (fieldType == Integer.class || fieldType == int.class) { + fieldSchema.put("default", Integer.parseInt(defaultValue)); + } else if (fieldType == Long.class || fieldType == long.class) { + fieldSchema.put("default", Long.parseLong(defaultValue)); + } else if (fieldType == Double.class || fieldType == double.class) { + fieldSchema.put("default", Double.parseDouble(defaultValue)); + } else if (fieldType == Float.class || fieldType == float.class) { + fieldSchema.put("default", Float.parseFloat(defaultValue)); } + } catch (NumberFormatException e) { + // 如果转换失败,使用字符串形式 + fieldSchema.put("default", defaultValue); } + } else if (fieldType == Boolean.class || fieldType == boolean.class) { + fieldSchema.put("default", Boolean.parseBoolean(defaultValue)); + } else { + fieldSchema.put("default", defaultValue); } } + /** + * 判断是否为数值类型 + */ + private static boolean isNumberType(Class type) { + return type == Integer.class || type == int.class || + type == Long.class || type == long.class || + type == Double.class || type == double.class || + type == Float.class || type == float.class; + } + /** * 获取JSON Schema类型 */ private static String getJsonSchemaType(Class type) { if (type == String.class) { return "string"; - } else if (type == Integer.class || type == int.class) { + } else if (type == Integer.class || type == int.class || type == Long.class || type == long.class) { return "integer"; - } else if (type == Double.class || type == double.class || type == Float.class || type == float.class) { - return "number"; } else if (type == Boolean.class || type == boolean.class) { return "boolean"; + } else if (type == Double.class || type == double.class || type == Float.class || type == float.class) { + return "number"; } else if (type.isArray() || List.class.isAssignableFrom(type)) { return "array"; + } else if (type.isEnum()) { + return "string"; } else { return "object"; } } - - /** - * 设置默认值 - */ - private static void setDefaultValue(ObjectNode property, String type, String defaultValue) { - switch (type) { - case "string": - property.put("default", defaultValue); - break; - case "integer": - try { - property.put("default", Integer.parseInt(defaultValue)); - } catch (NumberFormatException e) { - // 忽略无效的默认值 - } - break; - case "number": - try { - property.put("default", Double.parseDouble(defaultValue)); - } catch (NumberFormatException e) { - // 忽略无效的默认值 - } - break; - case "boolean": - property.put("default", Boolean.parseBoolean(defaultValue)); - break; - } - } } diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index b479cadb..f335c668 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -492,7 +492,6 @@ CREATE TABLE workflow_node_definition ( category VARCHAR(50) NOT NULL COMMENT '节点分类', flowable_config TEXT COMMENT 'Flowable引擎配置JSON', graph_config TEXT NOT NULL COMMENT 'X6图形配置JSON', - form_config TEXT COMMENT '表单配置JSON', order_num INT NOT NULL DEFAULT 0 COMMENT '排序号', enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用', create_time DATETIME NOT NULL COMMENT '创建时间',