通用解析

This commit is contained in:
戚辰先生 2024-12-11 23:33:05 +08:00
parent d048858015
commit 205c87dc5a
27 changed files with 1736 additions and 0 deletions

View File

@ -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<Task> 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<WorkflowInstance> 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<Task> activeTasks = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.list();
// 2. 获取历史任务
List<HistoricTaskInstance> historicTasks = historyService
.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.finished()
.list();
// 3. 更新节点实例状态
Set<String> 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的数据同步
- 支持业务扩展
- 提供更好的性能
- 确保数据一致性
- 便于问题追踪和修复

View File

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

View File

@ -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<String> candidateUsers;
/**
* 候选组列表
*/
private List<String> candidateGroups;
/**
* 执行人表达式
*/
private String assigneeExpression;
/**
* 候选人表达式
*/
private String candidateExpression;
}

View File

@ -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<ListenerConfig> listeners;
/**
* 是否异步
*/
private Boolean async;
/**
* 是否独占
*/
private Boolean exclusive;
/**
* 自定义属性
*/
private Map<String, Object> customProperties;
/**
* 文档说明
*/
private String documentation;
}

View File

@ -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<String, Object> outputVariables;
/**
* 是否终止所有活动分支
*/
private Boolean terminateAll;
}

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.workflow.dto.nodeConfig;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 事件节点如STARTEND的基础配置
*/
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;
}
}

View File

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

View File

@ -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<String, Object> properties;
/**
* 是否必填
*/
private Boolean required;
/**
* 表单验证规则
*/
private Map<String, Object> validations;
}

View File

@ -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<String, String> gitConfig;
public GitCloneConfig() {
}
}

View File

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

View File

@ -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<String, String> environment;
/**
* 成功退出码
*/
@SchemaProperty(
title = "成功退出码",
description = "脚本执行成功时的退出码",
defaultValue = "0"
)
private Integer successExitCode;
/**
* 支持的脚本语言列表
*/
@SchemaProperty(
title = "支持的脚本语言",
enumValues = {"shell", "python", "javascript", "groovy"}
)
private List<String> supportedLanguages;
public ScriptExecutorConfig() {
setTaskType("scriptTask");
}
}

View File

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

View File

@ -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<String, Object> inputVariables;
/**
* 任务输出变量
*/
@SchemaProperty(
title = "任务输出变量",
description = "工作流节点的任务输出变量"
)
private Map<String, Object> outputVariables;
/**
* 任务特定的监听器配置
*/
@SchemaProperty(
title = "任务监听器配置",
description = "工作流节点的任务特定监听器配置"
)
private List<TaskListenerConfig> taskListeners;
}

View File

@ -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<String, Object> fields;
}

View File

@ -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<String, Object> fields;
}

View File

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

View File

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

View File

@ -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<String, Object> fields;
}

View File

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

View File

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

View File

@ -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<String, String> environment;
/**
* 成功退出码
*/
private Integer successExitCode;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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