diff --git a/backend/docs/workflow/workflow_instance_sync.md b/backend/docs/workflow/workflow_instance_sync.md new file mode 100644 index 00000000..b48f95b2 --- /dev/null +++ b/backend/docs/workflow/workflow_instance_sync.md @@ -0,0 +1,380 @@ +# 工作流实例同步机制设计文档 + +## 目录 +1. [整体数据流向](#整体数据流向) +2. [Flowable与系统映射关系](#Flowable与系统映射关系) +3. [数据同步实现](#数据同步实现) +4. [事件监听机制](#事件监听机制) +5. [状态管理](#状态管理) +6. [最佳实践](#最佳实践) + +## 整体数据流向 + +``` +[前端流程设计器] + | + | 1. 用户拖拽设计流程 + | - 选择节点类型(从workflow_node_definition获取可用节点) + | - 配置节点属性(根据节点的form_config渲染表单) + | - 连接节点、设置条件 + ↓ +[前端数据模型] + | - 图形数据(nodes、edges) + | - 表单数据(各节点的配置信息) + | - 流程基本信息(名称、描述等) + | + | 2. 保存流程设计 + ↓ +[后端 WorkflowDefinitionController] + | + | 3. 接收流程设计数据 + ↓ +[后端 WorkflowDefinitionService] + | + | 4. 处理流程数据 + | - 保存流程定义基本信息到 workflow_definition + | - 转换图形数据为BPMN XML + | - 解析节点配置,更新workflow_node_definition + | * 对于新的节点类型:创建新记录 + | * 对于已有节点:更新配置 + | + | 5. 部署到Flowable + ↓ +[Flowable引擎] + | + | 6. 部署流程 + | - 保存BPMN XML + | - 生成流程定义ID + | + | 7. 启动流程实例 + ↓ +[后端 WorkflowInstanceService] + | + | 8. 创建流程实例 + | - 创建workflow_instance记录 + | - 关联workflow_definition + | + | 9. 节点实例化 + | - 创建workflow_node_instance记录 + | - 关联workflow_node_definition + ↓ +[数据库] + | + | 实时同步的表: + | - workflow_definition(流程定义) + | - workflow_node_definition(节点定义) + | - workflow_instance(流程实例) + | - workflow_node_instance(节点实例) + | + | Flowable表: + | - ACT_RE_*(流程定义相关表) + | - ACT_RU_*(运行时数据表) + | - ACT_HI_*(历史数据表) + ↓ +[前端任务列表/监控页面] + | + | 10. 展示流程实例 + | - 查询实例状态 + | - 显示节点执行情况 + | - 处理用户任务 + | - 查看历史记录 +``` + +## Flowable与系统映射关系 + +### 表结构映射 + +#### Flowable核心表 +``` +[流程定义相关] +ACT_RE_DEPLOYMENT: 流程部署表 +ACT_RE_PROCDEF: 流程定义表 + +[流程实例相关] +ACT_RU_EXECUTION: 运行时流程实例表 +ACT_RU_TASK: 运行时任务表 +ACT_RU_VARIABLE: 运行时变量表 + +[历史数据] +ACT_HI_PROCINST: 历史流程实例表 +ACT_HI_TASKINST: 历史任务实例表 +ACT_HI_ACTINST: 历史活动实例表 +``` + +#### 系统表与Flowable映射关系 +``` +我们的表 Flowable的表 +-------------------------------------------------- +workflow_definition <-> ACT_RE_PROCDEF +workflow_instance <-> ACT_RU_EXECUTION/ACT_HI_PROCINST +workflow_node_instance <-> ACT_RU_TASK/ACT_HI_TASKINST +``` + +## 数据同步实现 + +### 流程实例服务实现 +```java +@Service +@Transactional +public class WorkflowInstanceService { + + @Resource + private RuntimeService runtimeService; // Flowable的运行时服务 + + @Resource + private TaskService taskService; // Flowable的任务服务 + + public WorkflowInstance startProcess(WorkflowInstanceCreateDTO createDTO) { + // 1. 启动Flowable流程实例 + ProcessInstance processInstance = runtimeService.startProcessInstanceById( + createDTO.getProcessDefinitionId(), + createDTO.getBusinessKey(), + createDTO.getVariables() + ); + + // 2. 创建我们自己的流程实例记录 + WorkflowInstance workflowInstance = new WorkflowInstance(); + workflowInstance.setProcessInstanceId(processInstance.getId()); + workflowInstance.setProcessDefinitionId(createDTO.getProcessDefinitionId()); + workflowInstance.setBusinessKey(createDTO.getBusinessKey()); + workflowInstance.setStatus(WorkflowInstanceStatusEnums.RUNNING); + workflowInstance.setVariables(JsonUtils.toJsonString(createDTO.getVariables())); + workflowInstance.setStartTime(LocalDateTime.now()); + workflowInstanceRepository.save(workflowInstance); + + // 3. 获取当前活动的任务 + List tasks = taskService.createTaskQuery() + .processInstanceId(processInstance.getId()) + .list(); + + // 4. 为每个活动的任务创建节点实例 + for (Task task : tasks) { + WorkflowNodeInstance nodeInstance = new WorkflowNodeInstance(); + nodeInstance.setWorkflowInstanceId(workflowInstance.getId()); + nodeInstance.setNodeId(task.getTaskDefinitionKey()); + nodeInstance.setNodeName(task.getName()); + nodeInstance.setNodeType("userTask"); // 或者从定义中获取 + nodeInstance.setStatus("ACTIVE"); + nodeInstance.setStartTime(LocalDateTime.now()); + workflowNodeInstanceRepository.save(nodeInstance); + } + + return workflowInstance; + } +} +``` + +### 数据同步服务实现 +```java +@Service +public class WorkflowSyncService { + + @Scheduled(fixedRate = 60000) // 每分钟执行一次 + public void syncWorkflowStatus() { + // 1. 查找所有运行中的实例 + List runningInstances = workflowInstanceRepository + .findByStatus(WorkflowInstanceStatusEnums.RUNNING); + + for (WorkflowInstance instance : runningInstances) { + // 2. 检查Flowable中的状态 + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() + .processInstanceId(instance.getProcessInstanceId()) + .singleResult(); + + if (processInstance == null) { + // Flowable中实例已结束,更新我们的状态 + HistoricProcessInstance historicInstance = historyService + .createHistoricProcessInstanceQuery() + .processInstanceId(instance.getProcessInstanceId()) + .singleResult(); + + instance.setStatus(convertFlowableStatus(historicInstance.getEndActivityId())); + instance.setEndTime(LocalDateTime.now()); + workflowInstanceRepository.save(instance); + } + + // 3. 同步节点实例状态 + syncNodeInstances(instance.getId(), instance.getProcessInstanceId()); + } + } + + private void syncNodeInstances(Long workflowInstanceId, String processInstanceId) { + // 1. 获取当前活动的任务 + List activeTasks = taskService.createTaskQuery() + .processInstanceId(processInstanceId) + .list(); + + // 2. 获取历史任务 + List historicTasks = historyService + .createHistoricTaskInstanceQuery() + .processInstanceId(processInstanceId) + .finished() + .list(); + + // 3. 更新节点实例状态 + Set processedTaskIds = new HashSet<>(); + + // 处理活动任务 + for (Task task : activeTasks) { + WorkflowNodeInstance nodeInstance = getOrCreateNodeInstance( + workflowInstanceId, task.getTaskDefinitionKey()); + updateNodeInstance(nodeInstance, task, null); + processedTaskIds.add(task.getTaskDefinitionKey()); + } + + // 处理历史任务 + for (HistoricTaskInstance historicTask : historicTasks) { + if (!processedTaskIds.contains(historicTask.getTaskDefinitionKey())) { + WorkflowNodeInstance nodeInstance = getOrCreateNodeInstance( + workflowInstanceId, historicTask.getTaskDefinitionKey()); + updateNodeInstance(nodeInstance, null, historicTask); + } + } + } +} +``` + +## 事件监听机制 + +### Flowable事件监听器 +```java +@Component +public class FlowableEventListener implements TaskListener, ExecutionListener { + + @Resource + private WorkflowInstanceService workflowInstanceService; + + @Override + public void notify(DelegateTask task) { + // 任务事件处理 + String eventName = task.getEventName(); + switch (eventName) { + case "create": + workflowInstanceService.onTaskCreate(task); + break; + case "complete": + workflowInstanceService.onTaskComplete(task); + break; + case "delete": + workflowInstanceService.onTaskDelete(task); + break; + } + } + + @Override + public void notify(DelegateExecution execution) { + // 流程执行事件处理 + String eventName = execution.getEventName(); + switch (eventName) { + case "start": + workflowInstanceService.onProcessStart(execution); + break; + case "end": + workflowInstanceService.onProcessEnd(execution); + break; + } + } +} +``` + +## 状态管理 + +### 工作流实例状态枚举 +```java +public enum WorkflowInstanceStatusEnums { + NOT_STARTED, // 未开始 + RUNNING, // 运行中 + SUSPENDED, // 已暂停 + COMPLETED, // 已完成 + TERMINATED, // 已终止 + FAILED; // 执行失败 + + public static WorkflowInstanceStatusEnums fromFlowableStatus(String flowableStatus) { + switch (flowableStatus) { + case "active": + return RUNNING; + case "suspended": + return SUSPENDED; + case "completed": + return COMPLETED; + case "terminated": + return TERMINATED; + default: + return FAILED; + } + } +} +``` + +## 最佳实践 + +### 为什么需要自己维护实例表 + +1. **业务扩展性** + - 可以添加更多业务相关的字段 + - 可以实现自定义的状态管理 + - 可以关联更多业务数据 + +2. **性能优化** + - 避免频繁查询Flowable表 + - 可以建立更适合业务查询的索引 + - 可以实现更好的缓存策略 + +3. **数据完整性** + - 保存完整的业务上下文 + - 记录更详细的审计信息 + - 支持自定义的数据分析 + +### 数据一致性保证 + +1. **事务管理** +```java +@Transactional +public void startProcess() { + // 1. 启动Flowable流程 + // 2. 创建系统流程实例 + // 3. 创建节点实例 +} +``` + +2. **定时同步** +- 定期检查运行中的实例状态 +- 自动修复不一致的数据 +- 记录同步日志 + +3. **事件驱动** +- 监听Flowable事件 +- 实时更新系统状态 +- 保证数据实时性 + +### 性能优化建议 + +1. **索引优化** +- 为常用查询字段建立索引 +- 使用复合索引优化多字段查询 +- 避免过多索引影响写入性能 + +2. **缓存策略** +- 缓存活动的流程实例 +- 缓存常用的节点定义 +- 使用分布式缓存提高性能 + +3. **批量处理** +- 批量同步数据 +- 批量更新状态 +- 使用队列处理异步任务 + +## 总结 + +1. Flowable负责流程的实际执行和调度 +2. 系统维护业务层面的状态和数据 +3. 通过事件监听和定时同步保证数据一致性 +4. 使用状态映射处理不同系统间的状态转换 +5. 通过事务确保关键操作的原子性 + +这样的设计可以: +- 保持与Flowable的数据同步 +- 支持业务扩展 +- 提供更好的性能 +- 确保数据一致性 +- 便于问题追踪和修复 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/annotation/SchemaProperty.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/annotation/SchemaProperty.java new file mode 100644 index 00000000..7f4a889c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/annotation/SchemaProperty.java @@ -0,0 +1,58 @@ +package com.qqchen.deploy.backend.workflow.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Schema属性注解 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SchemaProperty { + /** + * 字段标题 + */ + String title() default ""; + + /** + * 字段描述 + */ + String description() default ""; + + /** + * 是否必填 + */ + boolean required() default false; + + /** + * 字段格式 + */ + String format() default ""; + + /** + * 默认值 + */ + String defaultValue() default ""; + + /** + * 最小值(用于数值类型) + */ + int minimum() default Integer.MIN_VALUE; + + /** + * 最大值(用于数值类型) + */ + int maximum() default Integer.MAX_VALUE; + + /** + * 枚举值(用于字符串类型) + */ + String[] enumValues() default {}; + + /** + * 枚举值显示名称 + */ + String[] enumNames() default {}; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/AssigneeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/AssigneeConfig.java new file mode 100644 index 00000000..9db32681 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/AssigneeConfig.java @@ -0,0 +1,35 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import lombok.Data; +import java.util.List; + +/** + * 执行人配置 + */ +@Data +public class AssigneeConfig { + /** + * 执行人ID + */ + private String assignee; + + /** + * 候选执行人列表 + */ + private List candidateUsers; + + /** + * 候选组列表 + */ + private List candidateGroups; + + /** + * 执行人表达式 + */ + private String assigneeExpression; + + /** + * 候选人表达式 + */ + private String candidateExpression; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseNodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseNodeConfig.java new file mode 100644 index 00000000..9a3cb12a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/BaseNodeConfig.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener.ListenerConfig; +import lombok.Data; +import java.util.List; +import java.util.Map; + +/** + * 基础节点配置 + */ +@Data +public class BaseNodeConfig { + /** + * 监听器配置列表 + */ + private List listeners; + + /** + * 是否异步 + */ + private Boolean async; + + /** + * 是否独占 + */ + private Boolean exclusive; + + /** + * 自定义属性 + */ + private Map customProperties; + + /** + * 文档说明 + */ + private String documentation; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EndEventConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EndEventConfig.java new file mode 100644 index 00000000..2ec645e6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EndEventConfig.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.Map; + +/** + * 结束节点配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class EndEventConfig extends EventNodeConfig { + /** + * 结束类型(normal, terminate, error, etc.) + */ + private String terminateType; + + /** + * 错误代码(当terminateType为error时使用) + */ + private String errorCode; + + /** + * 错误消息(当terminateType为error时使用) + */ + private String errorMessage; + + /** + * 结束时的输出变量 + */ + private Map outputVariables; + + /** + * 是否终止所有活动分支 + */ + private Boolean terminateAll; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EventConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EventConfig.java new file mode 100644 index 00000000..2c962815 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EventConfig.java @@ -0,0 +1,29 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 事件节点(如START、END)的基础配置 + */ +public class EventConfig { + @JsonProperty(required = true) + private String name; + + private String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EventNodeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EventNodeConfig.java new file mode 100644 index 00000000..8db6f923 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/EventNodeConfig.java @@ -0,0 +1,27 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty; +import lombok.Data; + +/** + * 事件节点基础配置 + */ +@Data +public class EventNodeConfig { + /** + * 节点名称 + */ + @SchemaProperty( + title = "节点名称", + required = true + ) + private String name; + + /** + * 节点描述 + */ + @SchemaProperty( + title = "节点描述" + ) + private String description; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/FormConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/FormConfig.java new file mode 100644 index 00000000..072f5088 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/FormConfig.java @@ -0,0 +1,30 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import lombok.Data; +import java.util.Map; + +/** + * 表单配置 + */ +@Data +public class FormConfig { + /** + * 表单Key + */ + private String formKey; + + /** + * 表单属性 + */ + private Map properties; + + /** + * 是否必填 + */ + private Boolean required; + + /** + * 表单验证规则 + */ + private Map validations; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/GitCloneConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/GitCloneConfig.java new file mode 100644 index 00000000..97ce9030 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/GitCloneConfig.java @@ -0,0 +1,66 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.Map; + +/** + * Git Clone任务配置 + * 继承TaskConfig以获取通用的任务配置(优先级、超时、重试等) + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GitCloneConfig extends TaskConfig { + /** + * 仓库URL + */ + private String repositoryUrl; + + /** + * 分支或标签 + */ + private String ref; + + /** + * 目标目录 + */ + private String targetDirectory; + + /** + * 是否递归克隆子模块 + */ + private Boolean recursive; + + /** + * 克隆深度(0表示完整克隆) + */ + private Integer depth; + + /** + * 身份验证类型(NONE, SSH_KEY, USERNAME_PASSWORD) + */ + private String authenticationType; + + /** + * SSH私钥(当authenticationType为SSH_KEY时使用) + */ + private String sshPrivateKey; + + /** + * 用户名(当authenticationType为USERNAME_PASSWORD时使用) + */ + private String username; + + /** + * 密码或令牌(当authenticationType为USERNAME_PASSWORD时使用) + */ + private String password; + + /** + * Git配置 + */ + private Map gitConfig; + + public GitCloneConfig() { + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/NodeTypeConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/NodeTypeConfig.java new file mode 100644 index 00000000..46f6054c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/NodeTypeConfig.java @@ -0,0 +1,45 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +/** + * 节点类型配置 + */ +@Data +public class NodeTypeConfig { + /** + * 节点类型代码 + */ + @SchemaProperty( + title = "节点类型代码", + description = "节点类型的唯一标识", + required = true + ) + private String code; + + /** + * 节点类型名称 + */ + @SchemaProperty( + title = "节点类型名称", + description = "节点类型的显示名称", + required = true + ) + private String name; + + /** + * 节点类型描述 + */ + @SchemaProperty( + title = "节点类型描述", + description = "节点类型的详细描述" + ) + private String description; + + /** + * 节点配置Schema + */ + private JsonNode configSchema; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/ScriptExecutorConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/ScriptExecutorConfig.java new file mode 100644 index 00000000..78113fdd --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/ScriptExecutorConfig.java @@ -0,0 +1,90 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.List; +import java.util.Map; + +/** + * 脚本执行器配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ScriptExecutorConfig extends TaskConfig { + /** + * 脚本内容 + */ + @SchemaProperty( + title = "脚本内容", + description = "需要执行的脚本内容,例如:\n#!/bin/bash\necho \"开始执行脚本\"\nls -la\necho \"脚本执行完成\"", + format = "textarea", + required = true + ) + private String script; + + /** + * 脚本语言 + */ + @SchemaProperty( + title = "脚本语言", + description = "脚本语言类型", + required = true, + enumValues = {"shell", "python", "javascript", "groovy"}, + enumNames = {"Shell脚本 (已支持)", "Python脚本 (开发中)", "JavaScript脚本 (开发中)", "Groovy脚本 (开发中)"}, + defaultValue = "shell" + ) + private String language; + + /** + * 解释器路径 + */ + @SchemaProperty( + title = "解释器路径", + description = "脚本解释器的路径,例如:/bin/bash, /usr/bin/python3", + required = true + ) + private String interpreter; + + /** + * 工作目录 + */ + @SchemaProperty( + title = "工作目录", + description = "脚本执行的工作目录", + defaultValue = "/tmp" + ) + private String workingDirectory; + + /** + * 环境变量 + */ + @SchemaProperty( + title = "环境变量", + description = "脚本执行时的环境变量" + ) + private Map environment; + + /** + * 成功退出码 + */ + @SchemaProperty( + title = "成功退出码", + description = "脚本执行成功时的退出码", + defaultValue = "0" + ) + private Integer successExitCode; + + /** + * 支持的脚本语言列表 + */ + @SchemaProperty( + title = "支持的脚本语言", + enumValues = {"shell", "python", "javascript", "groovy"} + ) + private List supportedLanguages; + + public ScriptExecutorConfig() { + setTaskType("scriptTask"); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/StartEventConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/StartEventConfig.java new file mode 100644 index 00000000..405b46cf --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/StartEventConfig.java @@ -0,0 +1,35 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 开始节点配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class StartEventConfig extends EventNodeConfig { + /** + * 触发器类型 + */ + @SchemaProperty( + title = "触发器类型", + description = "工作流的触发方式" + ) + private String triggerType; + + /** + * 表单Key(当需要启动表单时使用) + */ + @SchemaProperty( + title = "表单Key", + description = "启动表单的标识符" + ) + private String formKey; + + /** + * 初始化变量 + */ + private String initiator; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/TaskConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/TaskConfig.java new file mode 100644 index 00000000..1e6bc118 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/TaskConfig.java @@ -0,0 +1,170 @@ +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.TaskListenerConfig; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.List; +import java.util.Map; + +/** + * 任务节点基础配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TaskConfig extends BaseNodeConfig { + /** + * 任务类型(如:userTask, serviceTask, scriptTask等) + */ + @SchemaProperty( + title = "任务类型", + description = "工作流节点的任务类型", + required = true + ) + private String taskType; + + /** + * 任务名称 + */ + @SchemaProperty( + title = "任务名称", + description = "工作流节点的显示名称", + required = true + ) + private String taskName; + + /** + * 任务描述 + */ + @SchemaProperty( + title = "任务描述", + description = "工作流节点的详细描述" + ) + private String description; + + /** + * 是否启用 + */ + @SchemaProperty( + title = "是否启用", + description = "是否启用该任务节点", + defaultValue = "true" + ) + private Boolean enabled = true; + + /** + * 任务优先级(0-100) + */ + @SchemaProperty( + title = "任务优先级", + description = "工作流节点的任务优先级", + minimum = 0, + maximum = 100 + ) + private Integer priority; + + /** + * 超时时间(秒) + */ + @SchemaProperty( + title = "超时时间", + description = "任务执行的最大时间(秒)", + minimum = 1, + maximum = 3600, + defaultValue = "300" + ) + private Integer timeoutDuration; + + /** + * 超时处理策略 + */ + @SchemaProperty( + title = "超时处理策略", + description = "任务超时后的处理策略", + enumValues = {"FAIL", "CONTINUE", "RETRY"}, + enumNames = {"失败", "继续", "重试"}, + defaultValue = "FAIL" + ) + private String timeoutStrategy; + + /** + * 重试次数 + */ + @SchemaProperty( + title = "重试次数", + description = "任务失败后的重试次数", + minimum = 0, + maximum = 10, + defaultValue = "0" + ) + private Integer retryTimes; + + /** + * 重试间隔(秒) + */ + @SchemaProperty( + title = "重试间隔", + description = "两次重试之间的等待时间(秒)", + minimum = 1, + maximum = 3600, + defaultValue = "60" + ) + private Integer retryInterval; + + /** + * 重试策略 + */ + @SchemaProperty( + title = "重试策略", + description = "任务重试的策略", + enumValues = {"FIXED", "EXPONENTIAL"}, + enumNames = {"固定间隔", "指数退避"}, + defaultValue = "FIXED" + ) + private String retryStrategy; + + /** + * 执行人配置 + */ + @SchemaProperty( + title = "执行人配置", + description = "工作流节点的执行人配置" + ) + private AssigneeConfig assignee; + + /** + * 表单配置 + */ + @SchemaProperty( + title = "表单配置", + description = "工作流节点的表单配置" + ) + private FormConfig form; + + /** + * 任务输入变量 + */ + @SchemaProperty( + title = "任务输入变量", + description = "工作流节点的任务输入变量" + ) + private Map inputVariables; + + /** + * 任务输出变量 + */ + @SchemaProperty( + title = "任务输出变量", + description = "工作流节点的任务输出变量" + ) + private Map outputVariables; + + /** + * 任务特定的监听器配置 + */ + @SchemaProperty( + title = "任务监听器配置", + description = "工作流节点的任务特定监听器配置" + ) + private List taskListeners; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/TaskListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/TaskListener.java new file mode 100644 index 00000000..fab7064b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/TaskListener.java @@ -0,0 +1,30 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig; + +import lombok.Data; +import java.util.Map; + +/** + * 任务监听器 + */ +@Data +public class TaskListener { + /** + * 监听器类型(如:class, expression, delegateExpression) + */ + private String type; + + /** + * 监听器实现(类名、表达式或代理表达式) + */ + private String implementation; + + /** + * 事件类型(create, assignment, complete, delete) + */ + private String event; + + /** + * 监听器字段 + */ + private Map fields; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/BaseListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/BaseListener.java new file mode 100644 index 00000000..f7714cd2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/BaseListener.java @@ -0,0 +1,25 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener; + +import lombok.Data; +import java.util.Map; + +/** + * 基础监听器配置 + */ +@Data +public class BaseListener { + /** + * 监听器类型(class, expression, delegateExpression) + */ + private String type; + + /** + * 监听器实现(类名、表达式或代理表达式) + */ + private String implementation; + + /** + * 监听器字段 + */ + private Map fields; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/EventListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/EventListener.java new file mode 100644 index 00000000..22c328c3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/EventListener.java @@ -0,0 +1,28 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 事件监听器配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class EventListener extends BaseListener { + /** + * 事件类型 + * start: 事件开始 + * end: 事件结束 + */ + private String event; + + /** + * 是否中断事件 + */ + private Boolean cancelActivity; + + /** + * 事件抛出的变量 + */ + private String eventVariable; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/EventListenerConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/EventListenerConfig.java new file mode 100644 index 00000000..90287d32 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/EventListenerConfig.java @@ -0,0 +1,28 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 事件监听器配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class EventListenerConfig extends ListenerConfig { + /** + * 事件类型 + * start: 事件开始 + * end: 事件结束 + */ + private String event; + + /** + * 是否中断事件 + */ + private Boolean cancelActivity; + + /** + * 事件抛出的变量 + */ + private String eventVariable; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/ListenerConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/ListenerConfig.java new file mode 100644 index 00000000..a8c252e3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/ListenerConfig.java @@ -0,0 +1,25 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener; + +import lombok.Data; +import java.util.Map; + +/** + * 基础监听器配置 + */ +@Data +public class ListenerConfig { + /** + * 监听器类型(class, expression, delegateExpression) + */ + private String type; + + /** + * 监听器实现(类名、表达式或代理表达式) + */ + private String implementation; + + /** + * 监听器字段 + */ + private Map fields; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/TaskListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/TaskListener.java new file mode 100644 index 00000000..bab88b30 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/TaskListener.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 任务监听器配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TaskListener extends BaseListener { + /** + * 事件类型 + * create: 任务创建 + * assignment: 任务分配 + * complete: 任务完成 + * delete: 任务删除 + * update: 任务更新 + * timeout: 任务超时 + */ + private String event; + + /** + * 任务相关的用户或组 + */ + private String assignee; + + /** + * 任务优先级 + */ + private Integer priority; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/TaskListenerConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/TaskListenerConfig.java new file mode 100644 index 00000000..70f8b9f5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/listener/TaskListenerConfig.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.listener; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 任务监听器配置 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TaskListenerConfig extends ListenerConfig { + /** + * 事件类型 + * create: 任务创建 + * assignment: 任务分配 + * complete: 任务完成 + * delete: 任务删除 + * update: 任务更新 + * timeout: 任务超时 + */ + private String event; + + /** + * 任务相关的用户或组 + */ + private String assignee; + + /** + * 任务优先级 + */ + private Integer priority; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptExecutionConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptExecutionConfig.java new file mode 100644 index 00000000..e8f4b9ee --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptExecutionConfig.java @@ -0,0 +1,35 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.script; + +import lombok.Data; +import java.util.Map; + +/** + * 脚本执行配置 + */ +@Data +public class ScriptExecutionConfig { + /** + * 脚本内容 + */ + private String script; + + /** + * 工作目录 + */ + private String workingDirectory; + + /** + * 超时时间(秒) + */ + private Integer timeout; + + /** + * 环境变量 + */ + private Map environment; + + /** + * 成功退出码 + */ + private Integer successExitCode; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptLanguageConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptLanguageConfig.java new file mode 100644 index 00000000..6b35bf34 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptLanguageConfig.java @@ -0,0 +1,25 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.script; + +import lombok.Data; +import java.util.List; + +/** + * 脚本语言配置 + */ +@Data +public class ScriptLanguageConfig { + /** + * 脚本语言类型 + */ + private String language; + + /** + * 解释器路径 + */ + private String interpreter; + + /** + * 支持的脚本语言列表 + */ + private List supportedLanguages; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptRetryConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptRetryConfig.java new file mode 100644 index 00000000..1b8bceb3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/nodeConfig/script/ScriptRetryConfig.java @@ -0,0 +1,19 @@ +package com.qqchen.deploy.backend.workflow.dto.nodeConfig.script; + +import lombok.Data; + +/** + * 脚本重试配置 + */ +@Data +public class ScriptRetryConfig { + /** + * 重试次数 + */ + private Integer retryTimes; + + /** + * 重试间隔(秒) + */ + private Integer retryInterval; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java new file mode 100644 index 00000000..83f32973 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java @@ -0,0 +1,42 @@ +package com.qqchen.deploy.backend.workflow.enums; + +/** + * 节点类型枚举 + */ +public enum NodeTypeEnum { + START_EVENT("START", "开始节点", "流程的开始节点"), + END_EVENT("END", "结束节点", "流程的结束节点"), + TASK("TASK", "任务节点", "通用任务节点"), + SCRIPT_TASK("SCRIPT", "脚本执行器", "支持执行多种脚本语言(Shell、Python、JavaScript等)"); + + private final String code; + private final String name; + private final String description; + + NodeTypeEnum(String code, String name, String description) { + this.code = code; + this.name = name; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public static NodeTypeEnum fromCode(String code) { + for (NodeTypeEnum type : values()) { + if (type.getCode().equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown node type code: " + code); + } +} 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 new file mode 100644 index 00000000..4ee8a100 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGenerator.java @@ -0,0 +1,292 @@ +package com.qqchen.deploy.backend.workflow.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty; +import com.qqchen.deploy.backend.workflow.dto.nodeConfig.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Schema生成器工具类 + */ +public class SchemaGenerator { + private static final ObjectMapper mapper = new ObjectMapper(); + + private SchemaGenerator() { + // 私有构造函数防止实例化 + } + + /** + * 生成所有节点类型的配置Schema + */ + public static JsonNode generateAllNodeTypeSchemas() { + ObjectNode schemas = mapper.createObjectNode(); + + // START节点 + schemas.set("START", generateNodeTypeSchema("START", "开始", "开始节点", StartEventConfig.class)); + + // SCRIPT节点 + schemas.set("SCRIPT", generateNodeTypeSchema("SCRIPT", "脚本执行", "执行脚本", ScriptExecutorConfig.class)); + + // GIT_CLONE节点 + schemas.set("GIT_CLONE", generateNodeTypeSchema("GIT_CLONE", "Git克隆", "克隆Git仓库到指定目录", GitCloneConfig.class)); + + // END节点 + schemas.set("END", generateNodeTypeSchema("END", "结束", "结束节点", EndEventConfig.class)); + + return schemas; + } + + /** + * 生成节点类型的配置Schema + */ + private static JsonNode generateNodeTypeSchema(String code, String name, String description, Class configClass) { + ObjectNode schema = mapper.createObjectNode(); + schema.put("code", code); + schema.put("name", name); + schema.put("description", description); + schema.set("configSchema", generateNodeConfigSchema(configClass)); + return schema; + } + + /** + * 生成节点配置的JSON Schema + */ + public static JsonNode generateNodeConfigSchema(Class configClass) { + ObjectNode schema = mapper.createObjectNode(); + schema.put("type", "object"); + ObjectNode properties = schema.putObject("properties"); + List required = new ArrayList<>(); + + // 只有TaskConfig的子类才添加任务相关字段 + if (TaskConfig.class.isAssignableFrom(configClass)) { + processTaskConfigFields(properties, required); + } + + // 处理特定配置类的字段 + for (Field field : configClass.getDeclaredFields()) { + SchemaProperty annotation = field.getAnnotation(SchemaProperty.class); + if (annotation != null) { + ObjectNode property = properties.putObject(field.getName()); + generateFieldSchema(property, field, annotation); + if (annotation.required()) { + required.add(field.getName()); + } + } + } + + // 处理父类的字段(除了TaskConfig) + Class superClass = configClass.getSuperclass(); + while (superClass != null && superClass != TaskConfig.class && superClass != Object.class) { + for (Field field : superClass.getDeclaredFields()) { + SchemaProperty annotation = field.getAnnotation(SchemaProperty.class); + if (annotation != null) { + ObjectNode property = properties.putObject(field.getName()); + generateFieldSchema(property, field, annotation); + if (annotation.required()) { + required.add(field.getName()); + } + } + } + superClass = superClass.getSuperclass(); + } + + if (!required.isEmpty()) { + ArrayNode requiredArray = schema.putArray("required"); + required.forEach(requiredArray::add); + } + + return schema; + } + + /** + * 处理TaskConfig中的基础字段 + */ + private static void processTaskConfigFields(ObjectNode properties, List required) { + // 添加name字段 + ObjectNode nameProperty = properties.putObject("name"); + nameProperty.put("type", "string"); + nameProperty.put("title", "节点名称"); + required.add("name"); + + // 添加description字段 + ObjectNode descProperty = properties.putObject("description"); + descProperty.put("type", "string"); + descProperty.put("title", "节点描述"); + + // 添加timeout字段 + ObjectNode timeoutProperty = properties.putObject("timeout"); + timeoutProperty.put("type", "object"); + timeoutProperty.put("title", "超时配置"); + + ObjectNode timeoutProperties = timeoutProperty.putObject("properties"); + + ObjectNode durationProperty = timeoutProperties.putObject("duration"); + durationProperty.put("type", "integer"); + durationProperty.put("title", "超时时间"); + durationProperty.put("description", "任务执行的最大时间(秒)"); + durationProperty.put("minimum", 1); + durationProperty.put("maximum", 3600); + durationProperty.put("default", 300); + + ObjectNode strategyProperty = timeoutProperties.putObject("strategy"); + strategyProperty.put("type", "string"); + strategyProperty.put("title", "超时处理策略"); + strategyProperty.put("description", "任务超时后的处理策略"); + ArrayNode strategyEnum = strategyProperty.putArray("enum"); + strategyEnum.add("FAIL").add("CONTINUE").add("RETRY"); + ArrayNode strategyEnumNames = strategyProperty.putArray("enumNames"); + strategyEnumNames.add("失败").add("继续").add("重试"); + strategyProperty.put("default", "FAIL"); + + // 添加retry字段 + ObjectNode retryProperty = properties.putObject("retry"); + retryProperty.put("type", "object"); + retryProperty.put("title", "重试配置"); + + ObjectNode retryProperties = retryProperty.putObject("properties"); + + ObjectNode timesProperty = retryProperties.putObject("times"); + timesProperty.put("type", "integer"); + timesProperty.put("title", "重试次数"); + timesProperty.put("description", "任务失败后的重试次数"); + timesProperty.put("minimum", 0); + timesProperty.put("maximum", 10); + timesProperty.put("default", 0); + + ObjectNode intervalProperty = retryProperties.putObject("interval"); + intervalProperty.put("type", "integer"); + intervalProperty.put("title", "重试间隔"); + intervalProperty.put("description", "两次重试之间的等待时间(秒)"); + intervalProperty.put("minimum", 1); + intervalProperty.put("maximum", 3600); + intervalProperty.put("default", 60); + + ObjectNode retryStrategyProperty = retryProperties.putObject("strategy"); + retryStrategyProperty.put("type", "string"); + retryStrategyProperty.put("title", "重试策略"); + retryStrategyProperty.put("description", "任务重试的策略"); + ArrayNode retryStrategyEnum = retryStrategyProperty.putArray("enum"); + retryStrategyEnum.add("FIXED").add("EXPONENTIAL"); + ArrayNode retryStrategyEnumNames = retryStrategyProperty.putArray("enumNames"); + retryStrategyEnumNames.add("固定间隔").add("指数退避"); + retryStrategyProperty.put("default", "FIXED"); + } + + /** + * 生成字段的Schema + */ + private static void generateFieldSchema(ObjectNode property, Field field, SchemaProperty annotation) { + // 设置基本属性 + if (!annotation.title().isEmpty()) { + property.put("title", annotation.title()); + } + if (!annotation.description().isEmpty()) { + property.put("description", annotation.description()); + } + if (!annotation.format().isEmpty()) { + property.put("format", annotation.format()); + } + if (!annotation.defaultValue().isEmpty()) { + setDefaultValue(property, field, annotation.defaultValue()); + } + + // 设置字段类型 + setFieldType(property, field); + + // 设置数值范围 + if (isNumericType(field.getType()) && + (annotation.minimum() != Integer.MIN_VALUE || annotation.maximum() != Integer.MAX_VALUE)) { + 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 enumNode = property.putArray("enum"); + Arrays.stream(annotation.enumValues()).forEach(enumNode::add); + + if (annotation.enumNames().length > 0) { + ArrayNode enumNamesNode = property.putArray("enumNames"); + Arrays.stream(annotation.enumNames()).forEach(enumNamesNode::add); + + // 生成oneOf结构 + ArrayNode oneOf = property.putArray("oneOf"); + for (int i = 0; i < annotation.enumValues().length; i++) { + ObjectNode oneOfItem = oneOf.addObject(); + oneOfItem.put("const", annotation.enumValues()[i]); + oneOfItem.put("title", annotation.enumNames()[i]); + + // 对于非shell的选项,设置为只读 + if (!"shell".equals(annotation.enumValues()[i])) { + oneOfItem.put("readOnly", true); + } + } + } + } + } + + /** + * 设置字段类型 + */ + private static void setFieldType(ObjectNode property, Field field) { + Class type = field.getType(); + if (String.class.isAssignableFrom(type)) { + property.put("type", "string"); + } else if (Integer.class.isAssignableFrom(type) || int.class.isAssignableFrom(type)) { + property.put("type", "integer"); + } else if (Boolean.class.isAssignableFrom(type) || boolean.class.isAssignableFrom(type)) { + property.put("type", "boolean"); + } else if (List.class.isAssignableFrom(type)) { + property.put("type", "array"); + // 如果需要,可以在这里添加items的schema + } else if (Map.class.isAssignableFrom(type)) { + property.put("type", "object"); + ObjectNode additionalProperties = property.putObject("additionalProperties"); + additionalProperties.put("type", "string"); + } + } + + /** + * 设置默认值 + */ + private static void setDefaultValue(ObjectNode property, Field field, String defaultValue) { + Class type = field.getType(); + if (String.class.isAssignableFrom(type)) { + property.put("default", defaultValue); + } else if (Integer.class.isAssignableFrom(type) || int.class.isAssignableFrom(type)) { + try { + property.put("default", Integer.parseInt(defaultValue)); + } catch (NumberFormatException e) { + // 忽略无效的默认值 + } + } else if (Boolean.class.isAssignableFrom(type) || boolean.class.isAssignableFrom(type)) { + property.put("default", Boolean.parseBoolean(defaultValue)); + } + } + + /** + * 判断是否为数值类型 + */ + private static boolean isNumericType(Class type) { + return Integer.class.isAssignableFrom(type) || + int.class.isAssignableFrom(type) || + Long.class.isAssignableFrom(type) || + long.class.isAssignableFrom(type) || + Double.class.isAssignableFrom(type) || + double.class.isAssignableFrom(type) || + Float.class.isAssignableFrom(type) || + float.class.isAssignableFrom(type); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGeneratorMain.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGeneratorMain.java new file mode 100644 index 00000000..b3701193 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/SchemaGeneratorMain.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.workflow.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Schema生成器的主类,用于测试和生成Schema + */ +public class SchemaGeneratorMain { + private static final ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + private SchemaGeneratorMain() { + // 私有构造函数防止实例化 + } + + public static void main(String[] args) { + try { + // 生成所有节点类型的Schema + System.out.println("=== Node Type Schemas ==="); + JsonNode schemas = SchemaGenerator.generateAllNodeTypeSchemas(); + System.out.println(mapper.writeValueAsString(schemas)); + } catch (Exception e) { + System.err.println("生成Schema时发生错误:"); + e.printStackTrace(); + } + } +} diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/SchemaGeneratorTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/SchemaGeneratorTest.java new file mode 100644 index 00000000..3a6b801f --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/util/SchemaGeneratorTest.java @@ -0,0 +1,52 @@ +package com.qqchen.deploy.backend.workflow.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class SchemaGeneratorTest { + private final SchemaGenerator generator = new SchemaGenerator(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testGenerateNodeTypeSchema() throws Exception { + JsonNode schema = generator.generateNodeTypeSchema(); + + // 将生成的schema打印出来以便查看 + String prettySchema = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema); + System.out.println(prettySchema); + + // 基本验证 + assertTrue(schema.isArray()); + assertEquals(1, schema.size()); + + JsonNode firstNode = schema.get(0); + assertEquals("SCRIPT", firstNode.get("code").asText()); + assertEquals("脚本执行器", firstNode.get("name").asText()); + + JsonNode configSchema = firstNode.get("configSchema"); + assertTrue(configSchema.has("properties")); + assertTrue(configSchema.has("required")); + + // 验证必填字段 + JsonNode required = configSchema.get("required"); + assertTrue(required.isArray()); + assertTrue(required.toString().contains("script")); + assertTrue(required.toString().contains("language")); + assertTrue(required.toString().contains("interpreter")); + + // 验证属性 + JsonNode properties = configSchema.get("properties"); + assertTrue(properties.has("script")); + assertTrue(properties.has("language")); + assertTrue(properties.has("interpreter")); + assertTrue(properties.has("workingDirectory")); + assertTrue(properties.has("timeout")); + assertTrue(properties.has("retryTimes")); + assertTrue(properties.has("retryInterval")); + assertTrue(properties.has("environment")); + assertTrue(properties.has("successExitCode")); + assertTrue(properties.has("supportedLanguages")); + } +}