增加工作流代码可正常启动

This commit is contained in:
戚辰先生 2024-12-04 22:58:20 +08:00
parent ec59608cc3
commit dde2ad27ae
38 changed files with 1932 additions and 363 deletions

View File

@ -0,0 +1,362 @@
# 工作流平台前端开发指南
## 一、项目概述
### 1.1 技术栈
- 框架Ant Design Pro
- 语言TypeScript
- HTTP请求Umi Request
- 流程设计器LogicFlow
- 表单设计器FormRender
- 图表库ECharts
### 1.2 开发环境
- Node.js >= 16
- yarn >= 1.22
- TypeScript >= 4.x
## 二、接口定义
### 2.1 工作流定义管理
```typescript
// 工作流定义相关接口
interface WorkflowDefinitionAPI {
// 基础CRUD接口
list: '/api/v1/workflow-definitions' // GET 查询列表
create: '/api/v1/workflow-definitions' // POST 创建
update: '/api/v1/workflow-definitions/{id}' // PUT 更新
delete: '/api/v1/workflow-definitions/{id}' // DELETE 删除
get: '/api/v1/workflow-definitions/{id}' // GET 获取详情
// 特殊操作接口
publish: '/api/v1/workflow-definitions/{id}/publish' // POST 发布
disable: '/api/v1/workflow-definitions/{id}/disable' // POST 禁用
enable: '/api/v1/workflow-definitions/{id}/enable' // POST 启用
}
// 工作流定义数据结构
interface WorkflowDefinitionDTO {
id: number
code: string // 工作流编码
name: string // 工作流名称
description: string // 描述
status: 'DRAFT' | 'PUBLISHED' | 'DISABLED' // 状态
version: number // 版本号
nodeConfig: string // 节点配置(JSON)
transitionConfig: string // 流转配置(JSON)
formDefinition: string // 表单定义
graphDefinition: string // 图形信息
enabled: boolean // 是否启用
remark: string // 备注
nodes: NodeDefinitionDTO[] // 节点定义列表
}
// 节点定义数据结构
interface NodeDefinitionDTO {
id: number
nodeId: string // 节点ID
name: string // 节点名称
type: 'START' | 'END' | 'TASK' | 'GATEWAY' | 'SUB_PROCESS' | 'SHELL' // 节点类型
config: string // 节点配置(JSON)
description: string // 节点描述
workflowDefinitionId: number // 工作流定义ID
orderNum: number // 排序号
}
```
### 2.2 工作流实例管理
```typescript
// 工作流实例相关接口
interface WorkflowInstanceAPI {
// 基础CRUD接口
list: '/api/v1/workflow-instance' // GET 查询列表
get: '/api/v1/workflow-instance/{id}' // GET 获取详情
// 实例操作接口
create: '/api/v1/workflow-instance' // POST 创建实例
start: '/api/v1/workflow-instance/{id}/start' // POST 启动
cancel: '/api/v1/workflow-instance/{id}/cancel' // POST 取消
pause: '/api/v1/workflow-instance/{id}/pause' // POST 暂停
resume: '/api/v1/workflow-instance/{id}/resume' // POST 恢复
}
// 工作流实例数据结构
interface WorkflowInstanceDTO {
id: number
definitionId: number // 工作流定义ID
businessKey: string // 业务标识
status: 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'PAUSED' // 状态
startTime: string // 开始时间
endTime: string // 结束时间
variables: string // 工作流变量(JSON)
error: string // 错误信息
}
```
### 2.3 节点实例管理
```typescript
// 节点实例相关接口
interface NodeInstanceAPI {
// 查询接口
list: '/api/v1/node-instance' // GET 查询列表
get: '/api/v1/node-instance/{id}' // GET 获取详情
findByWorkflow: '/api/v1/node-instance/workflow/{workflowInstanceId}' // GET 查询工作流下的节点
findByStatus: '/api/v1/node-instance/workflow/{workflowInstanceId}/status/{status}' // GET 查询指定状态的节点
// 节点操作
updateStatus: '/api/v1/node-instance/{id}/status' // PUT 更新状态
}
// 节点实例数据结构
interface NodeInstanceDTO {
id: number
workflowInstanceId: number // 工作流实例ID
nodeId: string // 节点ID
nodeType: 'START' | 'END' | 'TASK' | 'GATEWAY' | 'SUB_PROCESS' | 'SHELL' // 节点类型
name: string // 节点名称
status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'PAUSED' | 'SKIPPED' // 状态
startTime: string // 开始时间
endTime: string // 结束时间
config: string // 节点配置(JSON)
input: string // 输入参数(JSON)
output: string // 输出结果(JSON)
error: string // 错误信息
preNodeId: string // 前置节点ID
}
```
### 2.4 工作流日志管理
```typescript
// 日志相关接口
interface WorkflowLogAPI {
// 基础CRUD接口
list: '/api/v1/workflow-logs' // GET 查询列表
create: '/api/v1/workflow-logs' // POST 创建
// 特殊查询接口
getWorkflowLogs: '/api/v1/workflow-logs/workflow/{workflowInstanceId}' // GET 查询工作流日志
getNodeLogs: '/api/v1/workflow-logs/node/{workflowInstanceId}/{nodeId}' // GET 查询节点日志
record: '/api/v1/workflow-logs/record' // POST 记录日志
}
// 日志数据结构
interface WorkflowLogDTO {
id: number
workflowInstanceId: number // 工作流实例ID
nodeId: string // 节点ID
message: string // 日志内容
level: 'INFO' | 'WARN' | 'ERROR' // 日志级别
detail: string // 详细信息
createTime: string // 创建时间
}
```
## 三、页面功能
### 3.1 工作流定义管理(/workflow/definition)
#### 3.1.1 列表页(/workflow/definition/list)
- 功能点:
- 工作流定义列表展示
- 支持按名称、编码、状态搜索
- 支持创建、编辑、删除操作
- 支持发布、禁用操作
- 支持查看历史版本
- 列表字段:
- 工作流编码
- 工作流名称
- 版本号
- 状态
- 创建时间
- 更新时间
- 操作按钮
#### 3.1.2 编辑页(/workflow/definition/edit)
- 功能点:
1. 基本信息编辑
- 工作流编码
- 工作流名称
- 描述
- 备注
2. 流程设计器
- 节点拖拽
- 连线绘制
- 节点配置
- 流程校验
3. 表单设计器
- 表单字段配置
- 字段验证规则
- 表单预览
4. 节点配置面板
- 节点基本信息
- 节点类型配置
- 执行条件配置
- 表单关联配置
### 3.2 工作流实例管理(/workflow/instance)
#### 3.2.1 列表页(/workflow/instance/list)
- 功能点:
- 工作流实例列表展示
- 支持按工作流名称、状态、时间搜索
- 支持启动、暂停、恢复、取消操作
- 列表字段:
- 实例ID
- 工作流名称
- 业务标识
- 状态
- 开始时间
- 结束时间
- 操作按钮
#### 3.2.2 详情页(/workflow/instance/detail)
- 功能点:
1. 基本信息展示
- 实例ID
- 工作流名称
- 状态
- 时间信息
2. 流程图展示
- 显示当前节点
- 节点状态标识
- 流程进度展示
3. 变量信息
- 变量列表
- 变量历史值
4. 日志信息
- 操作日志
- 执行日志
- 错误日志
### 3.3 监控大盘(/workflow/monitor)
- 功能点:
1. 统计信息
- 总工作流数
- 运行中实例数
- 完成实例数
- 失败实例数
2. 状态分布
- 饼图展示各状态占比
- 支持时间范围筛选
3. 执行时长统计
- 平均执行时长
- 最长执行时长
- 执行时长分布
4. 异常情况统计
- 异常类型分布
- 异常趋势图
- 异常节点TOP5
## 四、开发规范
### 4.1 项目结构
```
src/
├── components/ # 公共组件
│ ├── FlowDesigner/ # 流程设计器组件
│ ├── FormDesigner/ # 表单设计器组件
│ └── NodeConfig/ # 节点配置组件
├── pages/ # 页面组件
│ └── workflow/ # 工作流相关页面
├── services/ # API服务
│ └── workflow/ # 工作流相关API
├── models/ # 数据模型
├── utils/ # 工具函数
├── constants/ # 常量定义
├── types/ # TypeScript类型
└── locales/ # 国际化资源
```
### 4.2 命名规范
- 文件命名:使用 PascalCase
- 组件文件:`FlowDesigner.tsx`
- 类型文件:`WorkflowTypes.ts`
- 工具文件:`FlowUtils.ts`
- 变量命名:使用 camelCase
- 普通变量:`flowInstance`
- 布尔值:`isRunning`、`hasError`
- 常量命名:使用 UPPER_CASE
- `MAX_NODE_COUNT`
- `DEFAULT_CONFIG`
### 4.3 组件开发规范
1. 使用函数组件和 Hooks
2. 使用 TypeScript 类型声明
3. 添加必要的注释
4. 实现错误处理
5. 添加加载状态
6. 做好性能优化
### 4.4 代码提交规范
- feat: 新功能
- fix: 修复bug
- docs: 文档更新
- style: 代码格式
- refactor: 重构
- test: 测试
- chore: 构建过程或辅助工具的变动
## 五、开发流程
### 5.1 环境搭建
1. 创建项目
```bash
yarn create umi
```
2. 安装依赖
```bash
yarn add @ant-design/pro-components @logicflow/core @logicflow/extension form-render echarts
```
### 5.2 开发步骤
1. 搭建基础框架和路由(2天)
2. 实现工作流定义CRUD(3天)
3. 开发流程设计器(5天)
4. 开发表单设计器(3天)
5. 实现工作流实例管理(3天)
6. 实现节点实例管理(2天)
7. 实现日志管理(2天)
8. 开发监控大盘(3天)
9. 测试和优化(2天)
### 5.3 测试要求
1. 单元测试覆盖率 > 80%
2. 必须包含组件测试
3. 必须包含集成测试
4. 必须进行性能测试
### 5.4 部署要求
1. 使用 Docker 部署
2. 配置 Nginx 代理
3. 启用 GZIP 压缩
4. 配置缓存策略
## 六、注意事项
### 6.1 性能优化
1. 使用路由懒加载
2. 组件按需加载
3. 大数据列表虚拟化
4. 合理使用缓存
5. 避免不必要的渲染
### 6.2 安全性
1. 添加权限控制
2. 防止XSS攻击
3. 添加数据验证
4. 敏感信息加密
### 6.3 用户体验
1. 添加适当的加载状态
2. 提供操作反馈
3. 添加错误处理
4. 支持快捷键操作
5. 添加操作确认
6. 支持数据导出
### 6.4 兼容性
1. 支持主流浏览器最新版本
2. 支持响应式布局
3. 支持暗黑模式
4. 支持国际化

View File

@ -0,0 +1,84 @@
package com.qqchen.deploy.backend.workflow.api.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 节点实例DTO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NodeInstanceDTO extends BaseDTO {
/**
* 工作流实例ID
*/
@NotNull(message = "工作流实例ID不能为空")
private Long workflowInstanceId;
/**
* 节点ID
*/
@NotBlank(message = "节点ID不能为空")
private String nodeId;
/**
* 节点类型
*/
@NotNull(message = "节点类型不能为空")
private NodeTypeEnum nodeType;
/**
* 节点名称
*/
@NotBlank(message = "节点名称不能为空")
private String name;
/**
* 节点状态
*/
@NotNull(message = "节点状态不能为空")
private NodeStatusEnum status;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 节点配置(JSON)
*/
private String config;
/**
* 输入参数(JSON)
*/
private String input;
/**
* 输出结果(JSON)
*/
private String output;
/**
* 错误信息
*/
private String error;
/**
* 前置节点ID
*/
private String preNodeId;
}

View File

@ -0,0 +1,47 @@
package com.qqchen.deploy.backend.workflow.api.query;
import com.qqchen.deploy.backend.framework.annotation.QueryField;
import com.qqchen.deploy.backend.framework.enums.QueryType;
import com.qqchen.deploy.backend.framework.query.BaseQuery;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 节点实例查询条件
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NodeInstanceQuery extends BaseQuery {
/**
* 工作流实例ID
*/
@QueryField(field = "workflowInstanceId")
private Long workflowInstanceId;
/**
* 节点ID
*/
@QueryField(field = "nodeId", type = QueryType.LIKE)
private String nodeId;
/**
* 节点类型
*/
@QueryField(field = "nodeType")
private NodeTypeEnum nodeType;
/**
* 节点名称
*/
@QueryField(field = "name", type = QueryType.LIKE)
private String name;
/**
* 节点状态
*/
@QueryField(field = "status")
private NodeStatusEnum status;
}

View File

@ -44,7 +44,7 @@ public class WorkflowLogApiController extends BaseController<WorkflowLog, Workfl
} }
@Operation(summary = "记录日志") @Operation(summary = "记录日志")
@PostMapping @PostMapping("/record")
public Response<Void> log( public Response<Void> log(
@Parameter(description = "工作流实例ID", required = true) @RequestParam Long workflowInstanceId, @Parameter(description = "工作流实例ID", required = true) @RequestParam Long workflowInstanceId,
@Parameter(description = "节点ID") @RequestParam(required = false) String nodeId, @Parameter(description = "节点ID") @RequestParam(required = false) String nodeId,

View File

@ -0,0 +1,39 @@
package com.qqchen.deploy.backend.workflow.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.dto.NodeDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.NodeDefinition;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
/**
* 节点定义转换器
*/
@Mapper(config = BaseConverter.class)
public interface NodeDefinitionConverter extends BaseConverter<NodeDefinition, NodeDefinitionDTO> {
@Override
@Mapping(target = "workflowDefinitionId", source = "workflowDefinition", qualifiedByName = "getWorkflowDefinitionId")
NodeDefinitionDTO toDto(NodeDefinition entity);
@Override
@Mapping(target = "workflowDefinition", source = "workflowDefinitionId", qualifiedByName = "createWorkflowDefinition")
NodeDefinition toEntity(NodeDefinitionDTO dto);
@Named("getWorkflowDefinitionId")
default Long getWorkflowDefinitionId(WorkflowDefinition workflowDefinition) {
return workflowDefinition != null ? workflowDefinition.getId() : null;
}
@Named("createWorkflowDefinition")
default WorkflowDefinition createWorkflowDefinition(Long id) {
if (id == null) {
return null;
}
WorkflowDefinition workflowDefinition = new WorkflowDefinition();
workflowDefinition.setId(id);
return workflowDefinition;
}
}

View File

@ -1,7 +1,7 @@
package com.qqchen.deploy.backend.workflow.converter; package com.qqchen.deploy.backend.workflow.converter;
import com.qqchen.deploy.backend.framework.converter.BaseConverter; import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.dto.NodeInstanceDTO; import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance; import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@ -13,10 +13,33 @@ import org.mapstruct.Mapping;
public interface NodeInstanceConverter extends BaseConverter<NodeInstance, NodeInstanceDTO> { public interface NodeInstanceConverter extends BaseConverter<NodeInstance, NodeInstanceDTO> {
@Override @Override
@Mapping(target = "workflowInstanceId", source = "workflowInstance.id") @Mapping(target = "workflowInstanceId", source = "workflowInstanceId")
@Mapping(target = "nodeId", source = "nodeId")
@Mapping(target = "nodeType", source = "nodeType")
@Mapping(target = "name", source = "name")
@Mapping(target = "status", source = "status")
@Mapping(target = "startTime", source = "startTime")
@Mapping(target = "endTime", source = "endTime")
@Mapping(target = "config", source = "config")
@Mapping(target = "input", source = "input")
@Mapping(target = "output", source = "output")
@Mapping(target = "error", source = "error")
@Mapping(target = "preNodeId", source = "preNodeId")
NodeInstanceDTO toDto(NodeInstance entity); NodeInstanceDTO toDto(NodeInstance entity);
@Override @Override
@Mapping(target = "workflowInstance.id", source = "workflowInstanceId") @Mapping(target = "workflowInstanceId", source = "workflowInstanceId")
@Mapping(target = "nodeId", source = "nodeId")
@Mapping(target = "nodeType", source = "nodeType")
@Mapping(target = "name", source = "name")
@Mapping(target = "status", source = "status")
@Mapping(target = "startTime", source = "startTime")
@Mapping(target = "endTime", source = "endTime")
@Mapping(target = "config", source = "config")
@Mapping(target = "input", source = "input")
@Mapping(target = "output", source = "output")
@Mapping(target = "error", source = "error")
@Mapping(target = "preNodeId", source = "preNodeId")
@Mapping(target = "workflowInstance", ignore = true)
NodeInstance toEntity(NodeInstanceDTO dto); NodeInstance toEntity(NodeInstanceDTO dto);
} }

View File

@ -4,10 +4,19 @@ import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/** /**
* 工作流定义转换器 * 工作流定义转换器
*/ */
@Mapper(config = BaseConverter.class) @Mapper(config = BaseConverter.class, uses = {NodeDefinitionConverter.class})
public interface WorkflowDefinitionConverter extends BaseConverter<WorkflowDefinition, WorkflowDefinitionDTO> { public interface WorkflowDefinitionConverter extends BaseConverter<WorkflowDefinition, WorkflowDefinitionDTO> {
@Override
@Mapping(target = "nodes", source = "nodes")
WorkflowDefinitionDTO toDto(WorkflowDefinition entity);
@Override
@Mapping(target = "nodes", source = "nodes")
WorkflowDefinition toEntity(WorkflowDefinitionDTO dto);
} }

View File

@ -13,10 +13,10 @@ import org.mapstruct.Mapping;
public interface WorkflowInstanceConverter extends BaseConverter<WorkflowInstance, WorkflowInstanceDTO> { public interface WorkflowInstanceConverter extends BaseConverter<WorkflowInstance, WorkflowInstanceDTO> {
@Override @Override
@Mapping(target = "definitionId", source = "definition.id") @Mapping(target = "definitionId", source = "workflowDefinition.id")
WorkflowInstanceDTO toDto(WorkflowInstance entity); WorkflowInstanceDTO toDto(WorkflowInstance entity);
@Override @Override
@Mapping(target = "definition.id", source = "definitionId") @Mapping(target = "workflowDefinition.id", source = "definitionId")
WorkflowInstance toEntity(WorkflowInstanceDTO dto); WorkflowInstance toEntity(WorkflowInstanceDTO dto);
} }

View File

@ -7,16 +7,13 @@ import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
/**
* 节点定义DTO
*/
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class NodeDefinitionDTO extends BaseDTO { public class NodeDefinitionDTO extends BaseDTO {
/**
* 工作流定义ID
*/
@NotNull(message = "工作流定义ID不能为空")
private Long workflowDefinitionId;
/** /**
* 节点ID * 节点ID
*/ */
@ -41,7 +38,18 @@ public class NodeDefinitionDTO extends BaseDTO {
private String config; private String config;
/** /**
* 序号 * 节点描述
*/ */
private Integer sequence; private String description;
/**
* 工作流定义ID
*/
@NotNull(message = "工作流定义ID不能为空")
private Long workflowDefinitionId;
/**
* 排序号
*/
private Integer orderNum;
} }

View File

@ -7,16 +7,15 @@ import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 工作流定义DTO
*/
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class WorkflowDefinitionDTO extends BaseDTO { public class WorkflowDefinitionDTO extends BaseDTO {
/**
* 工作流名称
*/
@NotBlank(message = "工作流名称不能为空")
private String name;
/** /**
* 工作流编码 * 工作流编码
*/ */
@ -24,23 +23,61 @@ public class WorkflowDefinitionDTO extends BaseDTO {
private String code; private String code;
/** /**
* 描述 * 工作流名称
*/
@NotBlank(message = "工作流名称不能为空")
private String name;
/**
* 工作流描述
*/ */
private String description; private String description;
/** /**
* 状态 * 工作流状态
*/ */
@NotNull(message = "状态不能为空") @NotNull(message = "工作流状态不能为空")
private WorkflowStatusEnum status; private WorkflowStatusEnum status;
/** /**
* 节点配置(JSON) * 版本号
*/
@NotNull(message = "版本号不能为空")
private Integer version;
/**
* 节点定义列表
*/
private List<NodeDefinitionDTO> nodes;
/**
* 节点配置
*/ */
private String nodeConfig; private String nodeConfig;
/** /**
* 流转配置(JSON) * 流转配置
*/ */
private String transitionConfig; private String transitionConfig;
/**
* 表单定义
*/
private String formDefinition;
/**
* 图形信息
*/
private String graphDefinition;
/**
* 是否启用
*/
@NotNull(message = "是否启用不能为空")
private Boolean enabled = true;
/**
* 备注
*/
private String remark;
} }

View File

@ -142,7 +142,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine {
} }
// 终止所有运行中的节点 // 终止所有运行中的节点
List<NodeInstance> runningNodes = nodeInstanceRepository.findByInstanceIdAndStatus( List<NodeInstance> runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
instanceId, NodeStatusEnum.RUNNING); instanceId, NodeStatusEnum.RUNNING);
WorkflowContext context = workflowContextFactory.create(instance); WorkflowContext context = workflowContextFactory.create(instance);
@ -173,7 +173,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine {
} }
// 暂停所有运行中的节点 // 暂停所有运行中的节点
List<NodeInstance> runningNodes = nodeInstanceRepository.findByInstanceIdAndStatus( List<NodeInstance> runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
instanceId, NodeStatusEnum.RUNNING); instanceId, NodeStatusEnum.RUNNING);
for (NodeInstance node : runningNodes) { for (NodeInstance node : runningNodes) {
@ -196,7 +196,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine {
} }
// 恢复所有暂停的节点 // 恢复所有暂停的节点
List<NodeInstance> pausedNodes = nodeInstanceRepository.findByInstanceIdAndStatus( List<NodeInstance> pausedNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(
instanceId, NodeStatusEnum.PAUSED); instanceId, NodeStatusEnum.PAUSED);
for (NodeInstance node : pausedNodes) { for (NodeInstance node : pausedNodes) {

View File

@ -0,0 +1,34 @@
package com.qqchen.deploy.backend.workflow.engine.config;
import com.qqchen.deploy.backend.workflow.engine.executor.NodeExecutor;
import com.qqchen.deploy.backend.workflow.engine.executor.task.TaskExecutor;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Configuration
public class WorkflowEngineConfig {
@Bean
public Map<String, TaskExecutor> taskExecutorMap(List<TaskExecutor> executors) {
return executors.stream()
.collect(Collectors.toMap(
executor -> executor.getClass().getSimpleName().replace("TaskExecutor", "").toUpperCase(),
Function.identity()
));
}
@Bean
public Map<NodeTypeEnum, NodeExecutor> nodeExecutorMap(List<NodeExecutor> executors) {
return executors.stream()
.collect(Collectors.toMap(
NodeExecutor::getNodeType,
Function.identity()
));
}
}

View File

@ -10,7 +10,6 @@ import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Component
public class DefaultWorkflowContext implements WorkflowContext { public class DefaultWorkflowContext implements WorkflowContext {
@Getter @Getter

View File

@ -75,9 +75,6 @@ public class TaskNodeExecutor implements NodeExecutor {
private void executeTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { private void executeTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
switch (config.getType()) { switch (config.getType()) {
case SHELL:
executeShellTask(config, nodeInstance, context);
break;
case HTTP: case HTTP:
executeHttpTask(config, nodeInstance, context); executeHttpTask(config, nodeInstance, context);
break; break;
@ -92,9 +89,6 @@ public class TaskNodeExecutor implements NodeExecutor {
private void terminateTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { private void terminateTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// 根据任务类型执行终止操作 // 根据任务类型执行终止操作
switch (config.getType()) { switch (config.getType()) {
case SHELL:
terminateShellTask(config, nodeInstance, context);
break;
case HTTP: case HTTP:
terminateHttpTask(config, nodeInstance, context); terminateHttpTask(config, nodeInstance, context);
break; break;
@ -104,11 +98,6 @@ public class TaskNodeExecutor implements NodeExecutor {
} }
} }
// 具体任务执行方法...
private void executeShellTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Shell脚本执行
}
private void executeHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { private void executeHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现HTTP请求执行 // TODO: 实现HTTP请求执行
} }
@ -117,10 +106,6 @@ public class TaskNodeExecutor implements NodeExecutor {
// TODO: 实现Java方法调用 // TODO: 实现Java方法调用
} }
private void terminateShellTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现Shell脚本终止
}
private void terminateHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { private void terminateHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) {
// TODO: 实现HTTP请求终止 // TODO: 实现HTTP请求终止
} }

View File

@ -7,7 +7,6 @@ import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
public enum TaskType { public enum TaskType {
SHELL("SHELL", "Shell脚本"),
HTTP("HTTP", "HTTP请求"), HTTP("HTTP", "HTTP请求"),
JAVA("JAVA", "Java方法"); JAVA("JAVA", "Java方法");

View File

@ -25,7 +25,7 @@ public class ShellNodeExecutor extends AbstractNodeExecutor {
@Override @Override
public NodeTypeEnum getNodeType() { public NodeTypeEnum getNodeType() {
return NodeTypeEnum.TASK; return NodeTypeEnum.SHELL;
} }
@Override @Override

View File

@ -7,6 +7,9 @@ import jakarta.persistence.*;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
/**
* 节点定义实体
*/
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity @jakarta.persistence.Entity
@ -15,15 +18,16 @@ import lombok.EqualsAndHashCode;
public class NodeDefinition extends Entity<Long> { public class NodeDefinition extends Entity<Long> {
/** /**
* 工作流定义ID * 所属工作流定义
*/ */
@Column(name = "workflow_definition_id", nullable = false) @ManyToOne(fetch = FetchType.LAZY)
private Long workflowDefinitionId; @JoinColumn(name = "workflow_definition_id", nullable = false)
private WorkflowDefinition workflowDefinition;
/** /**
* 节点ID * 节点ID
*/ */
@Column(nullable = false) @Column(name = "node_id", nullable = false)
private String nodeId; private String nodeId;
/** /**
@ -46,7 +50,8 @@ public class NodeDefinition extends Entity<Long> {
private String config; private String config;
/** /**
* 序号 * 序号
*/ */
private Integer sequence; @Column(name = "order_num")
private Integer orderNum;
} }

View File

@ -41,8 +41,8 @@ public class TransitionConfig extends Entity<Long> {
* - "${amount > 1000}" * - "${amount > 1000}"
* - "${result.code == 200 && result.data != null}" * - "${result.code == 200 && result.data != null}"
*/ */
@Column(columnDefinition = "text") @Column(name = "transition_condition", columnDefinition = "text")
private String condition; private String transitionCondition;
/** /**
* 优先级数字越小优先级越高 * 优先级数字越小优先级越高
@ -86,7 +86,7 @@ public class TransitionConfig extends Entity<Long> {
* @return true if conditional, false otherwise * @return true if conditional, false otherwise
*/ */
public boolean isConditional() { public boolean isConditional() {
return condition != null && !condition.trim().isEmpty(); return transitionCondition != null && !transitionCondition.trim().isEmpty();
} }
/** /**

View File

@ -7,17 +7,25 @@ import jakarta.persistence.*;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.List;
/** /**
* 工作流定义实体 * 工作流定义实体
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity @jakarta.persistence.Entity
@Table(name = "wf_definition") @Table(name = "wf_workflow_definition")
@LogicDelete @LogicDelete
public class WorkflowDefinition extends Entity<Long> { public class WorkflowDefinition extends Entity<Long> {
/**
* 工作流编码唯一标识
*/
@Column(nullable = false, unique = true)
private String code;
/** /**
* 工作流名称 * 工作流名称
*/ */
@ -25,32 +33,46 @@ public class WorkflowDefinition extends Entity<Long> {
private String name; private String name;
/** /**
* 工作流编码 * 工作流描述
*/
@Column(nullable = false, unique = true)
private String code;
/**
* 描述
*/ */
@Column(columnDefinition = "TEXT")
private String description; private String description;
/** /**
* 状态 * 工作流状态
*/ */
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
private WorkflowStatusEnum status = WorkflowStatusEnum.DRAFT; private WorkflowStatusEnum status = WorkflowStatusEnum.DRAFT;
/**
* 是否启用
*/
@Column(nullable = false)
private Boolean enabled = true;
/** /**
* 节点配置(JSON) * 节点配置(JSON)
*/ */
@Column(columnDefinition = "TEXT") @Column(name = "node_config", columnDefinition = "TEXT")
private String nodeConfig; private String nodeConfig;
/** /**
* 流转配置(JSON) * 流转配置(JSON)
*/ */
@Column(columnDefinition = "TEXT") @Column(name = "transition_config", columnDefinition = "TEXT")
private String transitionConfig; private String transitionConfig;
/**
* 节点定义列表
*/
@OneToMany(mappedBy = "workflowDefinition", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NodeDefinition> nodes = new ArrayList<>();
/**
* 获取流转配置
*/
public String getTransitionConfig() {
return transitionConfig;
}
} }

View File

@ -3,13 +3,7 @@ package com.qqchen.deploy.backend.workflow.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete; import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity; import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -20,35 +14,40 @@ import java.time.LocalDateTime;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity @jakarta.persistence.Entity
@Table(name = "wf_instance") @Table(name = "wf_workflow_instance")
@LogicDelete @LogicDelete
public class WorkflowInstance extends Entity<Long> { public class WorkflowInstance extends Entity<Long> {
/** /**
* 工作流定义ID * 工作流定义
*/ */
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "definition_id", nullable = false) @JoinColumn(name = "workflow_definition_id", nullable = false)
private WorkflowDefinition definition; private WorkflowDefinition workflowDefinition;
/** /**
* 项目环境ID * 项目环境ID
*/ */
@Column(name = "project_env_id", nullable = false) @Column(name = "project_env_id")
private Long projectEnvId; private Long projectEnvId;
/**
* 业务标识
*/
@Column(name = "business_key")
private String businessKey;
/** /**
* 状态 * 状态
*/ */
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false) @Column(nullable = false)
private WorkflowStatusEnum status; private WorkflowStatusEnum status = WorkflowStatusEnum.RUNNING;
/** /**
* 开始时间 * 开始时间
*/ */
@Column(name = "start_time", nullable = false) @Column(name = "start_time")
private LocalDateTime startTime; private LocalDateTime startTime;
/** /**
@ -57,15 +56,16 @@ public class WorkflowInstance extends Entity<Long> {
@Column(name = "end_time") @Column(name = "end_time")
private LocalDateTime endTime; private LocalDateTime endTime;
/**
* 工作流变量(JSON)
*/
@Column(name = "variables", columnDefinition = "TEXT")
private String variables;
/** /**
* 错误信息 * 错误信息
*/ */
@Column(name = "error", columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String error; private String error;
/**
* 设置工作流定义
*/
public void setDefinition(WorkflowDefinition definition) {
this.workflowDefinition = definition;
}
} }

View File

@ -3,8 +3,7 @@ package com.qqchen.deploy.backend.workflow.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete; import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity; import com.qqchen.deploy.backend.framework.domain.Entity;
import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum; import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Table;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -14,7 +13,7 @@ import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity @jakarta.persistence.Entity
@Table(name = "wf_variable") @Table(name = "wf_workflow_variable")
@LogicDelete @LogicDelete
public class WorkflowVariable extends Entity<Long> { public class WorkflowVariable extends Entity<Long> {
@ -33,7 +32,7 @@ public class WorkflowVariable extends Entity<Long> {
/** /**
* 变量值(JSON) * 变量值(JSON)
*/ */
@Column(columnDefinition = "TEXT", nullable = false) @Column(columnDefinition = "TEXT")
private String value; private String value;
/** /**
@ -46,5 +45,11 @@ public class WorkflowVariable extends Entity<Long> {
* 变量作用域 * 变量作用域
*/ */
@Column(nullable = false) @Column(nullable = false)
private String scope = VariableScopeEnum.INSTANCE.name(); private String scope = VariableScopeEnum.GLOBAL.name();
/**
* 节点IDscope为NODE时必填
*/
@Column(name = "node_id")
private String nodeId;
} }

View File

@ -14,7 +14,8 @@ public enum NodeTypeEnum {
END("END", "结束节点"), END("END", "结束节点"),
TASK("TASK", "任务节点"), TASK("TASK", "任务节点"),
GATEWAY("GATEWAY", "网关节点"), GATEWAY("GATEWAY", "网关节点"),
SUB_PROCESS("SUB_PROCESS", "子流程节点"); SUB_PROCESS("SUB_PROCESS", "子流程节点"),
SHELL("SHELL", "Shell脚本节点");
private final String code; private final String code;
private final String description; private final String description;

View File

@ -2,23 +2,69 @@ package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.NodeDefinition; import com.qqchen.deploy.backend.workflow.entity.NodeDefinition;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
/**
* 节点定义仓库接口
*/
@Repository @Repository
public interface INodeDefinitionRepository extends IBaseRepository<NodeDefinition, Long> { public interface INodeDefinitionRepository extends IBaseRepository<NodeDefinition, Long> {
/** /**
* 查询工作流定义的所有节点 * 根据工作流定义ID查询节点列表
*
* @param workflowDefinitionId 工作流定义ID
* @return 节点列表
*/ */
@Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinitionId = :definitionId AND n.deleted = false ORDER BY n.sequence") @Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinition.id = :workflowDefinitionId AND n.deleted = false ORDER BY n.orderNum")
List<NodeDefinition> findByWorkflowDefinitionIdOrderBySequence(@Param("definitionId") Long definitionId); List<NodeDefinition> findByWorkflowDefinitionId(@Param("workflowDefinitionId") Long workflowDefinitionId);
/** /**
* 根据节点ID查询节点定义 * 根据工作流定义ID和节点类型查询节点列表
*
* @param workflowDefinitionId 工作流定义ID
* @param type 节点类型
* @return 节点列表
*/ */
NodeDefinition findByWorkflowDefinitionIdAndNodeIdAndDeletedFalse(Long workflowDefinitionId, String nodeId); @Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinition.id = :workflowDefinitionId AND n.type = :type AND n.deleted = false ORDER BY n.orderNum")
List<NodeDefinition> findByWorkflowDefinitionIdAndType(
@Param("workflowDefinitionId") Long workflowDefinitionId,
@Param("type") NodeTypeEnum type);
/**
* 根据工作流定义ID和节点ID查询节点
*
* @param workflowDefinitionId 工作流定义ID
* @param nodeId 节点ID
* @return 节点定义
*/
@Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinition.id = :workflowDefinitionId AND n.nodeId = :nodeId AND n.deleted = false")
NodeDefinition findByWorkflowDefinitionIdAndNodeIdAndDeletedFalse(
@Param("workflowDefinitionId") Long workflowDefinitionId,
@Param("nodeId") String nodeId);
/**
* 检查节点ID是否已存在
*
* @param workflowDefinitionId 工作流定义ID
* @param nodeId 节点ID
* @return 是否存在
*/
@Query("SELECT COUNT(n) > 0 FROM NodeDefinition n WHERE n.workflowDefinition.id = :workflowDefinitionId AND n.nodeId = :nodeId AND n.deleted = false")
boolean existsByWorkflowDefinitionIdAndNodeIdAndDeletedFalse(
@Param("workflowDefinitionId") Long workflowDefinitionId,
@Param("nodeId") String nodeId);
/**
* 删除工作流定义的所有节点
*
* @param workflowDefinitionId 工作流定义ID
*/
@Query("UPDATE NodeDefinition n SET n.deleted = true WHERE n.workflowDefinition.id = :workflowDefinitionId")
void deleteByWorkflowDefinitionId(@Param("workflowDefinitionId") Long workflowDefinitionId);
} }

View File

@ -23,9 +23,9 @@ public interface INodeInstanceRepository extends IBaseRepository<NodeInstance, L
*/ */
List<NodeInstance> findByWorkflowInstanceIdAndStatusAndDeletedFalse(Long workflowInstanceId, NodeStatusEnum status); List<NodeInstance> findByWorkflowInstanceIdAndStatusAndDeletedFalse(Long workflowInstanceId, NodeStatusEnum status);
List<NodeInstance> findByInstanceIdAndStatus(Long instanceId, NodeStatusEnum status); List<NodeInstance> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status);
List<NodeInstance> findByInstanceId(Long instanceId); List<NodeInstance> findByWorkflowInstanceId(Long workflowInstanceId);
void deleteByInstanceId(Long instanceId); void deleteByWorkflowInstanceId(Long workflowInstanceId);
} }

View File

@ -2,8 +2,14 @@ package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/** /**
* 工作流定义仓库接口 * 工作流定义仓库接口
*/ */
@ -11,17 +17,62 @@ import org.springframework.stereotype.Repository;
public interface IWorkflowDefinitionRepository extends IBaseRepository<WorkflowDefinition, Long> { public interface IWorkflowDefinitionRepository extends IBaseRepository<WorkflowDefinition, Long> {
/** /**
* 根据编码查询工作流定义 * 根据编码查询最新版本的工作流定义
*
* @param code 工作流编码
* @return 工作流定义
*/ */
WorkflowDefinition findByCodeAndDeletedFalse(String code); @Query("SELECT w FROM WorkflowDefinition w WHERE w.code = :code AND w.deleted = false ORDER BY w.version DESC")
Optional<WorkflowDefinition> findLatestByCode(@Param("code") String code);
/** /**
* 检查编码是否存在 * 根据编码和版本号查询工作流定义
*
* @param code 工作流编码
* @param version 版本号
* @return 工作流定义
*/
Optional<WorkflowDefinition> findByCodeAndVersionAndDeletedFalse(String code, Integer version);
/**
* 根据编码查询所有版本的工作流定义
*
* @param code 工作流编码
* @return 工作流定义列表
*/
@Query("SELECT w FROM WorkflowDefinition w WHERE w.code = :code AND w.deleted = false ORDER BY w.version DESC")
List<WorkflowDefinition> findAllVersionsByCode(@Param("code") String code);
/**
* 根据状态查询工作流定义列表
*
* @param status 状态
* @return 工作流定义列表
*/
List<WorkflowDefinition> findByStatusAndDeletedFalse(WorkflowStatusEnum status);
/**
* 检查编码是否已存在
*
* @param code 工作流编码
* @return 是否存在
*/ */
boolean existsByCodeAndDeletedFalse(String code); boolean existsByCodeAndDeletedFalse(String code);
/** /**
* 检查名称是否存在 * 获取工作流定义的最新版本号
*
* @param code 工作流编码
* @return 最新版本号
*/ */
boolean existsByNameAndDeletedFalse(String name); @Query("SELECT MAX(w.version) FROM WorkflowDefinition w WHERE w.code = :code AND w.deleted = false")
Integer findLatestVersionByCode(@Param("code") String code);
/**
* 根据编码查询工作流定义
*
* @param code 工作流编码
* @return 工作流定义
*/
WorkflowDefinition findByCodeAndDeletedFalse(String code);
} }

View File

@ -36,4 +36,22 @@ public interface IWorkflowVariableRepository extends IBaseRepository<WorkflowVar
* 删除工作流实例的所有变量 * 删除工作流实例的所有变量
*/ */
void deleteByWorkflowInstanceId(Long instanceId); void deleteByWorkflowInstanceId(Long instanceId);
/**
* 查询工作流实例的指定作用域和节点的变量
*/
@Query("SELECT v FROM WorkflowVariable v WHERE v.workflowInstanceId = :instanceId AND v.scope = :scope AND v.nodeId = :nodeId AND v.deleted = false")
List<WorkflowVariable> findByWorkflowInstanceIdAndScopeAndNodeId(
@Param("instanceId") Long instanceId,
@Param("scope") String scope,
@Param("nodeId") String nodeId);
/**
* 查询工作流实例的指定名称和作用域的变量
*/
@Query("SELECT v FROM WorkflowVariable v WHERE v.workflowInstanceId = :instanceId AND v.name = :name AND v.scope = :scope AND v.deleted = false")
Optional<WorkflowVariable> findByWorkflowInstanceIdAndNameAndScope(
@Param("instanceId") Long instanceId,
@Param("name") String name,
@Param("scope") String scope);
} }

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import java.util.List;
/**
* 节点实例服务接口
*/
public interface INodeInstanceService extends IBaseService<NodeInstance, NodeInstanceDTO, Long> {
/**
* 根据工作流实例ID查询节点列表
*
* @param workflowInstanceId 工作流实例ID
* @return 节点列表
*/
List<NodeInstanceDTO> findByWorkflowInstanceId(Long workflowInstanceId);
/**
* 根据工作流实例ID和状态查询节点列表
*
* @param workflowInstanceId 工作流实例ID
* @param status 状态
* @return 节点列表
*/
List<NodeInstanceDTO> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status);
/**
* 更新节点状态
*
* @param id 节点ID
* @param status 状态
* @param output 输出结果
* @param error 错误信息
*/
void updateStatus(Long id, NodeStatusEnum status, String output, String error);
}

View File

@ -3,7 +3,8 @@ package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.service.IBaseService; import com.qqchen.deploy.backend.framework.service.IBaseService;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import java.util.List;
/** /**
* 工作流定义服务接口 * 工作流定义服务接口
@ -11,32 +12,84 @@ import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
public interface IWorkflowDefinitionService extends IBaseService<WorkflowDefinition, WorkflowDefinitionDTO, Long> { public interface IWorkflowDefinitionService extends IBaseService<WorkflowDefinition, WorkflowDefinitionDTO, Long> {
/** /**
* 发布工作流定义 * 创建工作流定义
*
* @param dto 工作流定义DTO
* @return 工作流定义
*/ */
void publish(Long id); WorkflowDefinitionDTO create(WorkflowDefinitionDTO dto);
/**
* 更新工作流定义
*
* @param id 工作流定义ID
* @param dto 工作流定义DTO
* @return 工作流定义
*/
WorkflowDefinitionDTO update(Long id, WorkflowDefinitionDTO dto);
/**
* 发布工作流定义
*
* @param id 工作流定义ID
* @return 工作流定义
*/
WorkflowDefinitionDTO publish(Long id);
/** /**
* 禁用工作流定义 * 禁用工作流定义
*
* @param id 工作流定义ID
* @return 工作流定义
*/ */
void disable(Long id); WorkflowDefinitionDTO disable(Long id);
/**
* 创建新版本
*
* @param id 工作流定义ID
* @return 工作流定义
*/
WorkflowDefinitionDTO createNewVersion(Long id);
/**
* 根据编码查询最新版本
*
* @param code 工作流编码
* @return 工作流定义
*/
WorkflowDefinitionDTO findLatestByCode(String code);
/**
* 根据编码和版本号查询
*
* @param code 工作流编码
* @param version 版本号
* @return 工作流定义
*/
WorkflowDefinitionDTO findByCodeAndVersion(String code, Integer version);
/**
* 查询所有版本
*
* @param code 工作流编码
* @return 工作流定义列表
*/
List<WorkflowDefinitionDTO> findAllVersions(String code);
/**
* 验证工作流定义
*
* @param id 工作流定义ID
* @return 验证结果
*/
boolean validate(Long id);
/** /**
* 启用工作流定义 * 启用工作流定义
*
* @param id 工作流定义ID
* @return 工作流定义
*/ */
void enable(Long id); WorkflowDefinitionDTO enable(Long id);
/**
* 检查工作流名称是否唯一
*/
void checkNameUnique(String name);
/**
* 检查工作流编码是否唯一
*/
void checkCodeUnique(String code);
/**
* 验证工作流状态
*/
void validateStatus(WorkflowStatusEnum status);
} }

View File

@ -0,0 +1,68 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.api.dto.NodeInstanceDTO;
import com.qqchen.deploy.backend.workflow.converter.NodeInstanceConverter;
import com.qqchen.deploy.backend.workflow.entity.NodeInstance;
import com.qqchen.deploy.backend.workflow.enums.NodeStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeInstanceRepository;
import com.qqchen.deploy.backend.workflow.service.INodeInstanceService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 节点实例服务实现类
*/
@Service
public class NodeInstanceServiceImpl extends BaseServiceImpl<NodeInstance, NodeInstanceDTO, Long> implements INodeInstanceService {
@Resource
private INodeInstanceRepository nodeInstanceRepository;
@Resource
private NodeInstanceConverter nodeInstanceConverter;
@Override
public List<NodeInstanceDTO> findByWorkflowInstanceId(Long workflowInstanceId) {
return nodeInstanceRepository.findByWorkflowInstanceId(workflowInstanceId)
.stream()
.map(nodeInstanceConverter::toDto)
.collect(Collectors.toList());
}
@Override
public List<NodeInstanceDTO> findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status) {
return nodeInstanceRepository.findByWorkflowInstanceIdAndStatus(workflowInstanceId, status)
.stream()
.map(nodeInstanceConverter::toDto)
.collect(Collectors.toList());
}
@Override
@Transactional
public void updateStatus(Long id, NodeStatusEnum status, String output, String error) {
NodeInstance node = nodeInstanceRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NODE_NOT_FOUND));
node.setStatus(status);
node.setOutput(output);
node.setError(error);
if (NodeStatusEnum.RUNNING.equals(status)) {
node.setStartTime(LocalDateTime.now());
} else if (NodeStatusEnum.COMPLETED.equals(status) ||
NodeStatusEnum.FAILED.equals(status) ||
NodeStatusEnum.CANCELLED.equals(status) ||
NodeStatusEnum.SKIPPED.equals(status)) {
node.setEndTime(LocalDateTime.now());
}
nodeInstanceRepository.save(node);
}
}

View File

@ -3,9 +3,11 @@ package com.qqchen.deploy.backend.workflow.service.impl;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.converter.WorkflowDefinitionConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeDefinitionRepository;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
import com.qqchen.deploy.backend.workflow.service.IWorkflowDefinitionService; import com.qqchen.deploy.backend.workflow.service.IWorkflowDefinitionService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -13,69 +15,189 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 工作流定义服务实现
*/
@Slf4j @Slf4j
@Service @Service
public class WorkflowDefinitionServiceImpl public class WorkflowDefinitionServiceImpl extends BaseServiceImpl<WorkflowDefinition, WorkflowDefinitionDTO, Long>
extends BaseServiceImpl<WorkflowDefinition, WorkflowDefinitionDTO, Long>
implements IWorkflowDefinitionService { implements IWorkflowDefinitionService {
@Resource @Resource
private IWorkflowDefinitionRepository workflowDefinitionRepository; private IWorkflowDefinitionRepository workflowDefinitionRepository;
@Override @Resource
@Transactional private INodeDefinitionRepository nodeDefinitionRepository;
public void publish(Long id) {
WorkflowDefinition definition = findByIdWithLock(id); @Resource
if (definition.getStatus() != WorkflowStatusEnum.DRAFT) { private WorkflowDefinitionConverter workflowDefinitionConverter;
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DRAFT);
}
definition.setStatus(WorkflowStatusEnum.PUBLISHED);
repository.save(definition);
}
@Override @Override
@Transactional @Transactional
public void disable(Long id) { public WorkflowDefinitionDTO create(WorkflowDefinitionDTO dto) {
WorkflowDefinition definition = findByIdWithLock(id); // 检查编码是否已存在
if (definition.getStatus() != WorkflowStatusEnum.PUBLISHED) { if (workflowDefinitionRepository.existsByCodeAndDeletedFalse(dto.getCode())) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_PUBLISHED);
}
definition.setStatus(WorkflowStatusEnum.DISABLED);
repository.save(definition);
}
@Override
@Transactional
public void enable(Long id) {
WorkflowDefinition definition = findByIdWithLock(id);
if (definition.getStatus() != WorkflowStatusEnum.DISABLED) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DISABLED);
}
definition.setStatus(WorkflowStatusEnum.PUBLISHED);
repository.save(definition);
}
@Override
@Transactional
public void checkNameUnique(String name) {
if (workflowDefinitionRepository.existsByNameAndDeletedFalse(name)) {
throw new BusinessException(ResponseCode.WORKFLOW_NAME_EXISTS);
}
}
@Override
@Transactional
public void checkCodeUnique(String code) {
if (workflowDefinitionRepository.existsByCodeAndDeletedFalse(code)) {
throw new BusinessException(ResponseCode.WORKFLOW_CODE_EXISTS); throw new BusinessException(ResponseCode.WORKFLOW_CODE_EXISTS);
} }
// 设置初始状态
dto.setStatus(WorkflowStatusEnum.DRAFT);
dto.setEnabled(true);
// 保存工作流定义
WorkflowDefinition workflowDefinition = workflowDefinitionConverter.toEntity(dto);
workflowDefinition = workflowDefinitionRepository.save(workflowDefinition);
return workflowDefinitionConverter.toDto(workflowDefinition);
} }
@Override @Override
@Transactional @Transactional
public void validateStatus(WorkflowStatusEnum status) { public WorkflowDefinitionDTO update(Long id, WorkflowDefinitionDTO dto) {
if (status == null) { // 获取当前工作流定义
throw new BusinessException(ResponseCode.WORKFLOW_INVALID_STATUS); WorkflowDefinition current = workflowDefinitionRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
// 只有草稿状态可以修改
if (current.getStatus() != WorkflowStatusEnum.DRAFT) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DRAFT);
} }
// 检查编码是否已存在排除自身
if (!Objects.equals(current.getCode(), dto.getCode()) &&
workflowDefinitionRepository.existsByCodeAndDeletedFalse(dto.getCode())) {
throw new BusinessException(ResponseCode.WORKFLOW_CODE_EXISTS);
}
// 更新基本信息
current.setName(dto.getName());
current.setDescription(dto.getDescription());
current.setNodeConfig(dto.getNodeConfig());
// 保存更新
current = workflowDefinitionRepository.save(current);
return workflowDefinitionConverter.toDto(current);
}
@Override
@Transactional
public WorkflowDefinitionDTO publish(Long id) {
// 获取当前工作流定义
WorkflowDefinition current = workflowDefinitionRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
// 验证当前状态
if (current.getStatus() != WorkflowStatusEnum.DRAFT) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DRAFT);
}
// 验证工作流定义
if (!validate(id)) {
throw new BusinessException(ResponseCode.WORKFLOW_CONFIG_INVALID);
}
// 更新状态
current.setStatus(WorkflowStatusEnum.PUBLISHED);
current = workflowDefinitionRepository.save(current);
return workflowDefinitionConverter.toDto(current);
}
@Override
@Transactional
public WorkflowDefinitionDTO disable(Long id) {
// 获取当前工作流定义
WorkflowDefinition current = workflowDefinitionRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
// 验证当前状态
if (current.getStatus() != WorkflowStatusEnum.PUBLISHED) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_PUBLISHED);
}
// 更新状态
current.setStatus(WorkflowStatusEnum.DISABLED);
current = workflowDefinitionRepository.save(current);
return workflowDefinitionConverter.toDto(current);
}
@Override
@Transactional
public WorkflowDefinitionDTO enable(Long id) {
// 获取当前工作流定义
WorkflowDefinition current = workflowDefinitionRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
// 验证当前状态
if (current.getStatus() != WorkflowStatusEnum.DISABLED) {
throw new BusinessException(ResponseCode.WORKFLOW_NOT_DISABLED);
}
// 更新状态
current.setStatus(WorkflowStatusEnum.PUBLISHED);
current = workflowDefinitionRepository.save(current);
return workflowDefinitionConverter.toDto(current);
}
@Override
public WorkflowDefinitionDTO findLatestByCode(String code) {
return workflowDefinitionRepository.findLatestByCode(code)
.map(workflowDefinitionConverter::toDto)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
}
@Override
public List<WorkflowDefinitionDTO> findAllVersions(String code) {
return workflowDefinitionRepository.findAllVersionsByCode(code)
.stream()
.map(workflowDefinitionConverter::toDto)
.collect(Collectors.toList());
}
@Override
public boolean validate(Long id) {
// TODO: 实现工作流定义验证逻辑
// 1. 验证节点配置的完整性
// 2. 验证节点之间的连接关系
return true;
}
@Override
@Transactional(readOnly = true)
public WorkflowDefinitionDTO findByCodeAndVersion(String code, Integer version) {
return workflowDefinitionConverter.toDto(workflowDefinitionRepository.findByCodeAndVersionAndDeletedFalse(code, version)
.orElse(null));
}
@Override
@Transactional
public WorkflowDefinitionDTO createNewVersion(Long id) {
// 获取原工作流定义
WorkflowDefinition oldDefinition = workflowDefinitionRepository.findById(id)
.orElseThrow(() -> new BusinessException(ResponseCode.WORKFLOW_NOT_FOUND));
// 获取最新版本号
Integer latestVersion = workflowDefinitionRepository.findLatestVersionByCode(oldDefinition.getCode());
Integer newVersion = latestVersion != null ? latestVersion + 1 : 1;
// 创建新版本
WorkflowDefinition newDefinition = new WorkflowDefinition();
newDefinition.setCode(oldDefinition.getCode());
newDefinition.setName(oldDefinition.getName());
newDefinition.setDescription(oldDefinition.getDescription());
newDefinition.setStatus(WorkflowStatusEnum.DRAFT);
newDefinition.setEnabled(true);
newDefinition.setNodeConfig(oldDefinition.getNodeConfig());
newDefinition.setTransitionConfig(oldDefinition.getTransitionConfig());
// 保存新版本
return workflowDefinitionConverter.toDto(workflowDefinitionRepository.save(newDefinition));
} }
} }

View File

@ -3,15 +3,14 @@ package com.qqchen.deploy.backend.workflow.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.workflow.dto.WorkflowVariableDTO; import com.qqchen.deploy.backend.workflow.dto.WorkflowVariableDTO;
import com.qqchen.deploy.backend.workflow.engine.exception.WorkflowEngineException;
import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable; import com.qqchen.deploy.backend.workflow.entity.WorkflowVariable;
import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum; import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowVariableRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowVariableRepository;
import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService; import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -19,88 +18,68 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 工作流变量服务实现类
*/
@Slf4j
@Service @Service
public class WorkflowVariableServiceImpl extends BaseServiceImpl<WorkflowVariable, WorkflowVariableDTO, Long> implements IWorkflowVariableService { public class WorkflowVariableServiceImpl extends BaseServiceImpl<WorkflowVariable, WorkflowVariableDTO, Long> implements IWorkflowVariableService {
@Resource @Resource
private IWorkflowVariableRepository variableRepository; private IWorkflowVariableRepository variableRepository;
private final ObjectMapper objectMapper = new ObjectMapper(); @Resource
private ObjectMapper objectMapper;
@Override @Override
@Transactional @Transactional
public void setVariable(Long workflowInstanceId, String name, Object value) { public void setVariable(Long workflowInstanceId, String name, Object value) {
try { try {
WorkflowVariable variable = variableRepository.findByWorkflowInstanceIdAndName(workflowInstanceId, name) WorkflowVariable variable = variableRepository.findByWorkflowInstanceIdAndNameAndScope(
.orElse(new WorkflowVariable()); workflowInstanceId, name, VariableScopeEnum.GLOBAL.name())
.orElseGet(() -> {
WorkflowVariable newVar = new WorkflowVariable();
newVar.setWorkflowInstanceId(workflowInstanceId);
newVar.setName(name);
newVar.setScope(VariableScopeEnum.GLOBAL.name());
return newVar;
});
variable.setWorkflowInstanceId(workflowInstanceId);
variable.setName(name);
variable.setValue(objectMapper.writeValueAsString(value)); variable.setValue(objectMapper.writeValueAsString(value));
variable.setType(value.getClass().getName()); variable.setType(value.getClass().getName());
variableRepository.save(variable);
repository.save(variable); } catch (JsonProcessingException e) {
} catch (Exception e) { throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e);
} }
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Map<String, Object> getVariables(Long workflowInstanceId) { public Map<String, Object> getVariables(Long workflowInstanceId) {
try {
List<WorkflowVariable> variables = variableRepository.findByWorkflowInstanceId(workflowInstanceId); List<WorkflowVariable> variables = variableRepository.findByWorkflowInstanceId(workflowInstanceId);
Map<String, Object> result = new HashMap<>(); return convertToMap(variables);
for (WorkflowVariable variable : variables) {
Object value = deserializeValue(variable);
result.put(variable.getName(), value);
}
return result;
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_EXECUTION_ERROR, e);
}
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Map<String, Object> getVariablesByScope(Long workflowInstanceId, VariableScopeEnum scope) { public Map<String, Object> getVariablesByScope(Long workflowInstanceId, VariableScopeEnum scope) {
try { List<WorkflowVariable> variables = variableRepository.findByWorkflowInstanceIdAndScope(workflowInstanceId, scope.name());
List<WorkflowVariable> variables = variableRepository.findByWorkflowInstanceIdAndScope( return convertToMap(variables);
workflowInstanceId, scope.name());
Map<String, Object> result = new HashMap<>();
for (WorkflowVariable variable : variables) {
Object value = deserializeValue(variable);
result.put(variable.getName(), value);
}
return result;
} catch (Exception e) {
throw new WorkflowEngineException(ResponseCode.WORKFLOW_EXECUTION_ERROR, e);
}
} }
@Override @Override
@Transactional @Transactional
public void deleteVariables(Long workflowInstanceId) { public void deleteVariables(Long workflowInstanceId) {
variableRepository.deleteByWorkflowInstanceId(workflowInstanceId); variableRepository.deleteByWorkflowInstanceId(workflowInstanceId);
log.debug("删除工作流变量成功: instanceId={}", workflowInstanceId);
} }
private Object deserializeValue(WorkflowVariable variable) { private Map<String, Object> convertToMap(List<WorkflowVariable> variables) {
Map<String, Object> result = new HashMap<>();
for (WorkflowVariable variable : variables) {
try { try {
Class<?> type = Class.forName(variable.getType()); Class<?> type = Class.forName(variable.getType());
return objectMapper.readValue(variable.getValue(), type); Object value = objectMapper.readValue(variable.getValue(), type);
} catch (ClassNotFoundException e) { result.put(variable.getName(), value);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e); } catch (Exception e) {
} catch (JsonProcessingException e) { throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID);
throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e);
} }
} }
return result;
}
} }

View File

@ -145,7 +145,7 @@ CREATE TABLE sys_role_tag (
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
name VARCHAR(50) NOT NULL COMMENT '标签名称', name VARCHAR(50) NOT NULL COMMENT '标签名称',
color VARCHAR(20) NULL COMMENT '标签进制颜色码)' color VARCHAR(20) NULL COMMENT '标签颜色'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表';
-- 角色标签关联表 -- 角色标签关联表
@ -274,7 +274,7 @@ CREATE TABLE deploy_repo_group (
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0-未删除1-已删除', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0-未删除1-已删除',
-- 业务字段 -- 业务字段
name VARCHAR(100) NOT NULL COMMENT '仓库组名', name VARCHAR(100) NOT NULL COMMENT '仓库组名',
description VARCHAR(500) NULL COMMENT '仓库组描述', description VARCHAR(500) NULL COMMENT '仓库组描述',
group_id BIGINT NOT NULL COMMENT '外部系统中的组ID', group_id BIGINT NOT NULL COMMENT '外部系统中的组ID',
parent_id BIGINT NULL COMMENT '父级仓库组ID', parent_id BIGINT NULL COMMENT '父级仓库组ID',
@ -377,103 +377,135 @@ CREATE TABLE deploy_repo_branch (
REFERENCES sys_external_system (id) REFERENCES sys_external_system (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库分支表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库分支表';
-- --------------------------------------------------------------------------------------
-- 工作流相关表
-- --------------------------------------------------------------------------------------
-- 工作流定义表 -- 工作流定义表
CREATE TABLE wf_definition ( CREATE TABLE wf_workflow_definition (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
code VARCHAR(100) NOT NULL COMMENT '工作流编码',
name VARCHAR(100) NOT NULL COMMENT '工作流名称', name VARCHAR(100) NOT NULL COMMENT '工作流名称',
code VARCHAR(50) NOT NULL COMMENT '工作流编码', description TEXT NULL COMMENT '工作流描述',
description TEXT COMMENT '描述', status VARCHAR(50) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT草稿PUBLISHED已发布DISABLED已禁用',
status VARCHAR(20) NOT NULL COMMENT '状态', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
node_config TEXT COMMENT '节点配置(JSON)', node_config TEXT NULL COMMENT '节点配置(JSON)',
transition_config TEXT COMMENT '流转配置(JSON)',
create_time DATETIME NOT NULL COMMENT '创建时间', CONSTRAINT UK_workflow_definition_code UNIQUE (code)
create_by VARCHAR(50) NOT NULL COMMENT '创建人', ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流定义表';
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人', -- 流转配置表
version INT NOT NULL DEFAULT 0 COMMENT '版本号', CREATE TABLE wf_transition_config (
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除', id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
UNIQUE KEY uk_code (code) create_by VARCHAR(255) NULL COMMENT '创建人',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流定义表'; create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
source_node_id VARCHAR(255) NOT NULL COMMENT '源节点ID',
target_node_id VARCHAR(255) NOT NULL COMMENT '目标节点ID',
transition_condition TEXT NULL COMMENT '流转条件',
priority INT NOT NULL DEFAULT 0 COMMENT '优先级',
CONSTRAINT FK_transition_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_workflow_definition (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流转配置表';
-- 工作流实例表
CREATE TABLE wf_workflow_instance (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
project_env_id BIGINT NULL COMMENT '项目环境ID',
business_key VARCHAR(100) NULL COMMENT '业务标识',
status VARCHAR(50) NOT NULL DEFAULT 'RUNNING' COMMENT '状态RUNNING运行中COMPLETED已完成TERMINATED已终止FAILED失败',
start_time DATETIME(6) NULL COMMENT '开始时间',
end_time DATETIME(6) NULL COMMENT '结束时间',
error TEXT NULL COMMENT '错误信息',
CONSTRAINT FK_workflow_instance_definition FOREIGN KEY (workflow_definition_id) REFERENCES wf_workflow_definition (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流实例表';
-- 节点定义表 -- 节点定义表
CREATE TABLE wf_node_definition ( CREATE TABLE wf_node_definition (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID', workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
node_id VARCHAR(50) NOT NULL COMMENT '节点ID', node_id VARCHAR(50) NOT NULL COMMENT '节点ID',
name VARCHAR(100) NOT NULL COMMENT '节点名称', name VARCHAR(100) NOT NULL COMMENT '节点名称',
type VARCHAR(50) NOT NULL COMMENT '节点类型', type VARCHAR(50) NOT NULL COMMENT '节点类型',
config TEXT COMMENT '节点配置(JSON)', config TEXT NULL COMMENT '节点配置(JSON)',
sequence INT NOT NULL DEFAULT 0 COMMENT '序号', order_num INT NOT NULL DEFAULT 0 COMMENT '排序号',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by VARCHAR(50) NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
UNIQUE KEY uk_workflow_node (workflow_definition_id, node_id),
CONSTRAINT fk_node_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_definition(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点定义表';
-- 工作流实例表(修改现有表) CONSTRAINT FK_node_definition_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_workflow_definition (id)
DROP TABLE IF EXISTS sys_workflow_instance; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流节点定义表';
CREATE TABLE wf_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
business_key VARCHAR(100) NOT NULL COMMENT '业务标识',
status VARCHAR(20) NOT NULL COMMENT '状态',
variables TEXT COMMENT '工作流变量(JSON)',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
error TEXT COMMENT '错误信息',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by VARCHAR(50) NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
CONSTRAINT fk_instance_definition FOREIGN KEY (definition_id) REFERENCES wf_definition(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流实例表';
-- 节点实例表(修改现有表) -- 节点实例表
DROP TABLE IF EXISTS sys_node_instance;
CREATE TABLE wf_node_instance ( CREATE TABLE wf_node_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID', workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID',
node_id VARCHAR(50) NOT NULL COMMENT '节点ID', node_id VARCHAR(100) NOT NULL COMMENT '节点ID',
node_type VARCHAR(50) NOT NULL COMMENT '节点类型',
name VARCHAR(100) NOT NULL COMMENT '节点名称', name VARCHAR(100) NOT NULL COMMENT '节点名称',
status VARCHAR(20) NOT NULL COMMENT '<EFBFBD><EFBFBD><EFBFBD>', type VARCHAR(50) NOT NULL COMMENT '节点类型',
config TEXT COMMENT '节点配置(JSON)', status VARCHAR(50) NOT NULL DEFAULT 'PENDING' COMMENT '状态PENDING待执行RUNNING执行中COMPLETED已完成FAILED失败TERMINATED已终止',
input TEXT COMMENT '输入参数(JSON)', start_time DATETIME(6) NULL COMMENT '开始时间',
output TEXT COMMENT '输出结果(JSON)', end_time DATETIME(6) NULL COMMENT '结束时间',
error TEXT COMMENT '错误信息', config TEXT NULL COMMENT '节点配置(JSON)',
start_time DATETIME COMMENT '开始时间', input TEXT NULL COMMENT '输入参数(JSON)',
end_time DATETIME COMMENT '结束时间', output TEXT NULL COMMENT '输出结果(JSON)',
create_time DATETIME NOT NULL COMMENT '创建时间', error TEXT NULL COMMENT '错误信息',
create_by VARCHAR(50) NOT NULL COMMENT '创建人', pre_node_id VARCHAR(100) NULL COMMENT '前置节点ID',
update_time DATETIME NOT NULL COMMENT '更新时间',
update_by VARCHAR(50) NOT NULL COMMENT '更新人', CONSTRAINT FK_node_instance_workflow FOREIGN KEY (workflow_instance_id) REFERENCES wf_workflow_instance (id)
version INT NOT NULL DEFAULT 0 COMMENT '版本号', ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点实例表';
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
CONSTRAINT fk_node_instance_workflow FOREIGN KEY (workflow_instance_id) REFERENCES wf_instance(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点实例表';
-- 工作流变量表 -- 工作流变量表
CREATE TABLE wf_variable ( CREATE TABLE wf_workflow_variable (
id BIGINT PRIMARY KEY AUTO_INCREMENT, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
workflow_instance_id BIGINT NOT NULL, create_by VARCHAR(255) NULL COMMENT '创建人',
name VARCHAR(255) NOT NULL, create_time DATETIME(6) NULL COMMENT '创建时间',
value TEXT NOT NULL, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
type VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
scope VARCHAR(50), update_time DATETIME(6) NULL COMMENT '更新时间',
create_time DATETIME NOT NULL, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
create_by VARCHAR(50),
update_time DATETIME, workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID',
update_by VARCHAR(50), name VARCHAR(100) NOT NULL COMMENT '变量名',
version INT DEFAULT 0, value TEXT NULL COMMENT '变量值',
deleted BOOLEAN DEFAULT FALSE, type VARCHAR(50) NOT NULL COMMENT '变量类型',
CONSTRAINT uk_wf_variable UNIQUE (workflow_instance_id, name, deleted) scope VARCHAR(50) NOT NULL DEFAULT 'GLOBAL' COMMENT '作用域GLOBAL全局NODE节点',
); node_id VARCHAR(100) NULL COMMENT '节点IDscope为NODE时必填',
CONSTRAINT FK_variable_workflow FOREIGN KEY (workflow_instance_id) REFERENCES wf_workflow_instance (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流变量表';
-- 工作流日志表 -- 工作流日志表
CREATE TABLE wf_log ( CREATE TABLE wf_log (
@ -492,7 +524,7 @@ CREATE TABLE wf_log (
); );
-- 创建索引 -- 创建索引
CREATE INDEX idx_wf_variable_instance ON wf_variable (workflow_instance_id); CREATE INDEX idx_wf_workflow_variable_instance ON wf_workflow_variable (workflow_instance_id);
CREATE INDEX idx_wf_log_instance ON wf_log (workflow_instance_id); CREATE INDEX idx_wf_log_instance ON wf_log (workflow_instance_id);
CREATE INDEX idx_wf_log_node ON wf_log (workflow_instance_id, node_id); CREATE INDEX idx_wf_log_node ON wf_log (workflow_instance_id, node_id);

View File

@ -184,3 +184,41 @@ workflow.log.not.found=工作流日志不存在
workflow.transition.invalid=工作流流转配置无效 workflow.transition.invalid=工作流流转配置无效
workflow.node.type.not.supported=不支持的节点类型 workflow.node.type.not.supported=不支持的节点类型
workflow.condition.invalid=工作流条件配置无效 workflow.condition.invalid=工作流条件配置无效
# 工作流相关错误消息
workflow.not.found=工作流定义不存在
workflow.code.exists=工作流编码已存在
workflow.name.exists=工作流名称已存在
workflow.disabled=工作流已禁用
workflow.not.published=工作流未发布
workflow.already.published=工作流已发布
workflow.already.disabled=工作流已禁用
workflow.not.draft=工作流不是草稿状态
workflow.not.disabled=工作流不是禁用状态
workflow.invalid.status=工作流状态无效
workflow.config.invalid=工作流配置无效
workflow.transition.invalid=工作流流转规则无效
workflow.condition.invalid=工作流条件配置无效
workflow.instance.not.found=工作流实例不存在
workflow.instance.already.completed=工作流实例已完成
workflow.instance.already.canceled=工作流实例已取消
workflow.instance.not.running=工作流实例未运行
workflow.node.not.found=工作流节点不存在
workflow.node.type.not.supported=不支持的节点类型
workflow.node.config.invalid=节点配置无效
workflow.node.execution.failed=节点执行失败
workflow.node.timeout=节点执行超时
workflow.node.config.error=节点配置错误
workflow.execution.error=工作流执行错误
workflow.variable.not.found=工作流变量不存在
workflow.variable.type.invalid=工作流变量类型无效
workflow.permission.denied=无权限操作工作流
workflow.approval.required=需要审批
workflow.approval.rejected=审批被拒绝
workflow.dependency.not.satisfied=工作流依赖条件未满足
workflow.circular.dependency=工作流存在循环依赖
workflow.schedule.invalid=工作流调度配置无效
workflow.concurrent.limit.exceeded=工作流并发限制超出

View File

@ -4,3 +4,41 @@ department.code.exists=Department code already exists
department.name.exists=Department name already exists department.name.exists=Department name already exists
department.parent.not.found=Parent department not found department.parent.not.found=Parent department not found
department.has.children=Cannot delete department with children department.has.children=Cannot delete department with children
# Workflow related error messages
workflow.not.found=Workflow definition not found
workflow.code.exists=Workflow code already exists
workflow.name.exists=Workflow name already exists
workflow.disabled=Workflow is disabled
workflow.not.published=Workflow is not published
workflow.already.published=Workflow is already published
workflow.already.disabled=Workflow is already disabled
workflow.not.draft=Workflow is not in draft status
workflow.not.disabled=Workflow is not in disabled status
workflow.invalid.status=Invalid workflow status
workflow.config.invalid=Invalid workflow configuration
workflow.transition.invalid=Invalid workflow transition rules
workflow.condition.invalid=Invalid workflow condition configuration
workflow.instance.not.found=Workflow instance not found
workflow.instance.already.completed=Workflow instance already completed
workflow.instance.already.canceled=Workflow instance already canceled
workflow.instance.not.running=Workflow instance is not running
workflow.node.not.found=Workflow node not found
workflow.node.type.not.supported=Unsupported node type
workflow.node.config.invalid=Invalid node configuration
workflow.node.execution.failed=Node execution failed
workflow.node.timeout=Node execution timeout
workflow.node.config.error=Node configuration error
workflow.execution.error=Workflow execution error
workflow.variable.not.found=Workflow variable not found
workflow.variable.type.invalid=Invalid workflow variable type
workflow.permission.denied=Permission denied to operate workflow
workflow.approval.required=Approval required
workflow.approval.rejected=Approval rejected
workflow.dependency.not.satisfied=Workflow dependency not satisfied
workflow.circular.dependency=Circular dependency detected in workflow
workflow.schedule.invalid=Invalid workflow schedule configuration
workflow.concurrent.limit.exceeded=Workflow concurrent limit exceeded

View File

@ -93,3 +93,41 @@ workflow.variable.invalid=工作流变量"{0}"的值无效
workflow.permission.denied=无权操作此工作流 workflow.permission.denied=无权操作此工作流
workflow.operation.not.allowed=当前状态不允许此操作 workflow.operation.not.allowed=当前状态不允许此操作
workflow.concurrent.operation=工作流正在执行其他操作,请稍后重试 workflow.concurrent.operation=工作流正在执行其他操作,请稍后重试
# 工作流相关错误消息
workflow.not.found=工作流定义不存在
workflow.code.exists=工作流编码已存在
workflow.name.exists=工作流名称已存在
workflow.disabled=工作流已禁用
workflow.not.published=工作流未发布
workflow.already.published=工作流已发布
workflow.already.disabled=工作流已禁用
workflow.not.draft=工作流不是草稿状态
workflow.not.disabled=工作流不是禁用状态
workflow.invalid.status=工作流状态无效
workflow.config.invalid=工作流配置无效
workflow.transition.invalid=工作流流转规则无效
workflow.condition.invalid=工作流条件配置无效
workflow.instance.not.found=工作流实例不存在
workflow.instance.already.completed=工作流实例已完成
workflow.instance.already.canceled=工作流实例已取消
workflow.instance.not.running=工作流实例未运行
workflow.node.not.found=工作流节点不存在
workflow.node.type.not.supported=不支持的节点类型
workflow.node.config.invalid=节点配置无效
workflow.node.execution.failed=节点执行失败
workflow.node.timeout=节点执行超时
workflow.node.config.error=节点配置错误
workflow.execution.error=工作流执行错误
workflow.variable.not.found=工作流变量不存在
workflow.variable.type.invalid=工作流变量类型无效
workflow.permission.denied=无权限操作工作流
workflow.approval.required=需要审批
workflow.approval.rejected=审批被拒绝
workflow.dependency.not.satisfied=工作流依赖条件未满足
workflow.circular.dependency=工作流存在循环依赖
workflow.schedule.invalid=工作流调度配置无效
workflow.concurrent.limit.exceeded=工作流并发限制超出

View File

@ -0,0 +1,138 @@
package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.workflow.entity.NodeDefinition;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.NodeTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ActiveProfiles("test")
class NodeDefinitionRepositoryTest {
@Autowired
private INodeDefinitionRepository nodeDefinitionRepository;
@Autowired
private IWorkflowDefinitionRepository workflowDefinitionRepository;
private WorkflowDefinition workflowDefinition;
@BeforeEach
void setUp() {
// 创建工作流定义
workflowDefinition = new WorkflowDefinition();
workflowDefinition.setCode("TEST-WF");
workflowDefinition.setName("Test Workflow");
workflowDefinition.setStatus(WorkflowStatusEnum.DRAFT);
workflowDefinition.setVersion(1);
workflowDefinition.setEnabled(true);
workflowDefinition = workflowDefinitionRepository.save(workflowDefinition);
}
@Test
void findByWorkflowDefinitionId_ShouldReturnNodesOrderedByOrderNum() {
// Given
NodeDefinition node1 = createNodeDefinition("NODE-001", "Node 1", NodeTypeEnum.START, 1);
NodeDefinition node2 = createNodeDefinition("NODE-002", "Node 2", NodeTypeEnum.TASK, 2);
NodeDefinition node3 = createNodeDefinition("NODE-003", "Node 3", NodeTypeEnum.END, 3);
nodeDefinitionRepository.save(node1);
nodeDefinitionRepository.save(node2);
nodeDefinitionRepository.save(node3);
// When
List<NodeDefinition> results = nodeDefinitionRepository.findByWorkflowDefinitionId(workflowDefinition.getId());
// Then
assertThat(results).hasSize(3);
assertThat(results.get(0).getOrderNum()).isEqualTo(1);
assertThat(results.get(1).getOrderNum()).isEqualTo(2);
assertThat(results.get(2).getOrderNum()).isEqualTo(3);
}
@Test
void findByWorkflowDefinitionIdAndType_ShouldReturnNodesWithSpecificType() {
// Given
NodeDefinition startNode = createNodeDefinition("START-001", "Start Node", NodeTypeEnum.START, 1);
NodeDefinition taskNode1 = createNodeDefinition("TASK-001", "Task Node 1", NodeTypeEnum.TASK, 2);
NodeDefinition taskNode2 = createNodeDefinition("TASK-002", "Task Node 2", NodeTypeEnum.TASK, 3);
NodeDefinition endNode = createNodeDefinition("END-001", "End Node", NodeTypeEnum.END, 4);
nodeDefinitionRepository.save(startNode);
nodeDefinitionRepository.save(taskNode1);
nodeDefinitionRepository.save(taskNode2);
nodeDefinitionRepository.save(endNode);
// When
List<NodeDefinition> results = nodeDefinitionRepository.findByWorkflowDefinitionIdAndType(
workflowDefinition.getId(), NodeTypeEnum.TASK);
// Then
assertThat(results).hasSize(2);
assertThat(results).allMatch(node -> node.getType() == NodeTypeEnum.TASK);
}
@Test
void findByWorkflowDefinitionIdAndCode_ShouldReturnSpecificNode() {
// Given
NodeDefinition node = createNodeDefinition("NODE-001", "Test Node", NodeTypeEnum.TASK, 1);
nodeDefinitionRepository.save(node);
// When
NodeDefinition result = nodeDefinitionRepository.findByWorkflowDefinitionIdAndCodeAndDeletedFalse(
workflowDefinition.getId(), "NODE-001");
// Then
assertThat(result).isNotNull();
assertThat(result.getCode()).isEqualTo("NODE-001");
}
@Test
void existsByWorkflowDefinitionIdAndCode_ShouldReturnTrue_WhenNodeExists() {
// Given
NodeDefinition node = createNodeDefinition("NODE-001", "Test Node", NodeTypeEnum.TASK, 1);
nodeDefinitionRepository.save(node);
// When
boolean exists = nodeDefinitionRepository.existsByWorkflowDefinitionIdAndCodeAndDeletedFalse(
workflowDefinition.getId(), "NODE-001");
// Then
assertThat(exists).isTrue();
}
@Test
void deleteByWorkflowDefinitionId_ShouldMarkNodesAsDeleted() {
// Given
NodeDefinition node1 = createNodeDefinition("NODE-001", "Node 1", NodeTypeEnum.START, 1);
NodeDefinition node2 = createNodeDefinition("NODE-002", "Node 2", NodeTypeEnum.TASK, 2);
nodeDefinitionRepository.save(node1);
nodeDefinitionRepository.save(node2);
// When
nodeDefinitionRepository.deleteByWorkflowDefinitionId(workflowDefinition.getId());
// Then
List<NodeDefinition> remainingNodes = nodeDefinitionRepository.findByWorkflowDefinitionId(workflowDefinition.getId());
assertThat(remainingNodes).isEmpty();
}
private NodeDefinition createNodeDefinition(String code, String name, NodeTypeEnum type, Integer orderNum) {
NodeDefinition node = new NodeDefinition();
node.setCode(code);
node.setName(name);
node.setType(type);
node.setOrderNum(orderNum);
node.setWorkflowDefinition(workflowDefinition);
return node;
}
}

View File

@ -0,0 +1,131 @@
package com.qqchen.deploy.backend.workflow.repository;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ActiveProfiles("test")
class WorkflowDefinitionRepositoryTest {
@Autowired
private IWorkflowDefinitionRepository workflowDefinitionRepository;
@Test
void findLatestByCode_ShouldReturnLatestVersion() {
// Given
WorkflowDefinition def1 = createWorkflowDefinition("TEST-001", "Test Workflow 1", 1);
WorkflowDefinition def2 = createWorkflowDefinition("TEST-001", "Test Workflow 1", 2);
workflowDefinitionRepository.save(def1);
workflowDefinitionRepository.save(def2);
// When
Optional<WorkflowDefinition> result = workflowDefinitionRepository.findLatestByCode("TEST-001");
// Then
assertThat(result).isPresent();
assertThat(result.get().getVersion()).isEqualTo(2);
}
@Test
void findByCodeAndVersion_ShouldReturnSpecificVersion() {
// Given
WorkflowDefinition def1 = createWorkflowDefinition("TEST-002", "Test Workflow 2", 1);
WorkflowDefinition def2 = createWorkflowDefinition("TEST-002", "Test Workflow 2", 2);
workflowDefinitionRepository.save(def1);
workflowDefinitionRepository.save(def2);
// When
Optional<WorkflowDefinition> result = workflowDefinitionRepository.findByCodeAndVersionAndDeletedFalse("TEST-002", 1);
// Then
assertThat(result).isPresent();
assertThat(result.get().getVersion()).isEqualTo(1);
}
@Test
void findAllVersionsByCode_ShouldReturnAllVersionsOrderedByVersionDesc() {
// Given
WorkflowDefinition def1 = createWorkflowDefinition("TEST-003", "Test Workflow 3", 1);
WorkflowDefinition def2 = createWorkflowDefinition("TEST-003", "Test Workflow 3", 2);
WorkflowDefinition def3 = createWorkflowDefinition("TEST-003", "Test Workflow 3", 3);
workflowDefinitionRepository.save(def1);
workflowDefinitionRepository.save(def2);
workflowDefinitionRepository.save(def3);
// When
List<WorkflowDefinition> results = workflowDefinitionRepository.findAllVersionsByCode("TEST-003");
// Then
assertThat(results).hasSize(3);
assertThat(results.get(0).getVersion()).isEqualTo(3);
assertThat(results.get(1).getVersion()).isEqualTo(2);
assertThat(results.get(2).getVersion()).isEqualTo(1);
}
@Test
void findByStatus_ShouldReturnWorkflowsWithSpecificStatus() {
// Given
WorkflowDefinition def1 = createWorkflowDefinition("TEST-004", "Test Workflow 4", 1);
def1.setStatus(WorkflowStatusEnum.PUBLISHED);
WorkflowDefinition def2 = createWorkflowDefinition("TEST-005", "Test Workflow 5", 1);
def2.setStatus(WorkflowStatusEnum.DRAFT);
workflowDefinitionRepository.save(def1);
workflowDefinitionRepository.save(def2);
// When
List<WorkflowDefinition> results = workflowDefinitionRepository.findByStatusAndDeletedFalse(WorkflowStatusEnum.PUBLISHED);
// Then
assertThat(results).hasSize(1);
assertThat(results.get(0).getStatus()).isEqualTo(WorkflowStatusEnum.PUBLISHED);
}
@Test
void existsByCode_ShouldReturnTrueWhenCodeExists() {
// Given
WorkflowDefinition def = createWorkflowDefinition("TEST-006", "Test Workflow 6", 1);
workflowDefinitionRepository.save(def);
// When
boolean exists = workflowDefinitionRepository.existsByCodeAndDeletedFalse("TEST-006");
// Then
assertThat(exists).isTrue();
}
@Test
void findLatestVersionByCode_ShouldReturnHighestVersion() {
// Given
WorkflowDefinition def1 = createWorkflowDefinition("TEST-007", "Test Workflow 7", 1);
WorkflowDefinition def2 = createWorkflowDefinition("TEST-007", "Test Workflow 7", 2);
WorkflowDefinition def3 = createWorkflowDefinition("TEST-007", "Test Workflow 7", 3);
workflowDefinitionRepository.save(def1);
workflowDefinitionRepository.save(def2);
workflowDefinitionRepository.save(def3);
// When
Integer latestVersion = workflowDefinitionRepository.findLatestVersionByCode("TEST-007");
// Then
assertThat(latestVersion).isEqualTo(3);
}
private WorkflowDefinition createWorkflowDefinition(String code, String name, Integer version) {
WorkflowDefinition def = new WorkflowDefinition();
def.setCode(code);
def.setName(name);
def.setVersion(version);
def.setStatus(WorkflowStatusEnum.DRAFT);
def.setEnabled(true);
return def;
}
}

View File

@ -1,51 +1,168 @@
package com.qqchen.deploy.backend.workflow.service; package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.workflow.converter.WorkflowDefinitionConverter;
import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO;
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum;
import com.qqchen.deploy.backend.workflow.repository.INodeDefinitionRepository;
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
import com.qqchen.deploy.backend.workflow.service.impl.WorkflowDefinitionServiceImpl; import com.qqchen.deploy.backend.workflow.service.impl.WorkflowDefinitionServiceImpl;
import jakarta.annotation.Resource; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.mock.mockito.MockBean; import org.mockito.InjectMocks;
import org.springframework.context.annotation.ComponentScan; import org.mockito.Mock;
import org.springframework.context.annotation.FilterType; import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.*;
@SpringBootTest( @ExtendWith(MockitoExtension.class)
classes = {
WorkflowDefinitionServiceImpl.class
},
properties = {
"spring.main.allow-bean-definition-overriding=true"
}
)
@ComponentScan(
basePackages = "com.qqchen.deploy.backend",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Controller")
}
)
class WorkflowDefinitionServiceTest { class WorkflowDefinitionServiceTest {
@Resource @Mock
private IWorkflowDefinitionService workflowDefinitionService;
@MockBean
private IWorkflowDefinitionRepository workflowDefinitionRepository; private IWorkflowDefinitionRepository workflowDefinitionRepository;
@Test @Mock
void testCreateWorkflowDefinition() { private INodeDefinitionRepository nodeDefinitionRepository;
// 准备测试数据
when(workflowDefinitionRepository.save(any(WorkflowDefinition.class)))
.thenAnswer(invocation -> {
WorkflowDefinition definition = invocation.getArgument(0);
definition.setId(1L);
return definition;
});
// 执行测试 @Mock
// ... 测试代码 ... private WorkflowDefinitionConverter workflowDefinitionConverter;
@InjectMocks
private WorkflowDefinitionServiceImpl workflowDefinitionService;
private WorkflowDefinition mockWorkflowDefinition;
private WorkflowDefinitionDTO mockWorkflowDefinitionDTO;
@BeforeEach
void setUp() {
mockWorkflowDefinition = new WorkflowDefinition();
mockWorkflowDefinition.setId(1L);
mockWorkflowDefinition.setCode("TEST-001");
mockWorkflowDefinition.setName("Test Workflow");
mockWorkflowDefinition.setVersion(1);
mockWorkflowDefinition.setStatus(WorkflowStatusEnum.DRAFT);
mockWorkflowDefinition.setEnabled(true);
mockWorkflowDefinitionDTO = new WorkflowDefinitionDTO();
mockWorkflowDefinitionDTO.setId(1L);
mockWorkflowDefinitionDTO.setCode("TEST-001");
mockWorkflowDefinitionDTO.setName("Test Workflow");
mockWorkflowDefinitionDTO.setVersion(1);
mockWorkflowDefinitionDTO.setStatus(WorkflowStatusEnum.DRAFT);
mockWorkflowDefinitionDTO.setEnabled(true);
}
@Test
void create_ShouldCreateNewWorkflowDefinition() {
// Given
when(workflowDefinitionRepository.existsByCodeAndDeletedFalse(anyString())).thenReturn(false);
when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinition);
when(workflowDefinitionConverter.toEntity(any(WorkflowDefinitionDTO.class))).thenReturn(mockWorkflowDefinition);
when(workflowDefinitionConverter.toDto(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinitionDTO);
// When
WorkflowDefinitionDTO result = workflowDefinitionService.create(mockWorkflowDefinitionDTO);
// Then
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(WorkflowStatusEnum.DRAFT);
assertThat(result.getVersion()).isEqualTo(1);
verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class));
}
@Test
void create_ShouldThrowException_WhenCodeExists() {
// Given
when(workflowDefinitionRepository.existsByCodeAndDeletedFalse(anyString())).thenReturn(true);
// When/Then
assertThatThrownBy(() -> workflowDefinitionService.create(mockWorkflowDefinitionDTO))
.isInstanceOf(BusinessException.class)
.hasMessage("工作流编码已存在");
}
@Test
void publish_ShouldPublishWorkflowDefinition() {
// Given
mockWorkflowDefinition.setStatus(WorkflowStatusEnum.DRAFT);
when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(mockWorkflowDefinition));
when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinition);
when(workflowDefinitionConverter.toDto(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinitionDTO);
// When
WorkflowDefinitionDTO result = workflowDefinitionService.publish(1L);
// Then
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(WorkflowStatusEnum.PUBLISHED);
verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class));
}
@Test
void publish_ShouldThrowException_WhenNotDraft() {
// Given
mockWorkflowDefinition.setStatus(WorkflowStatusEnum.PUBLISHED);
when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(mockWorkflowDefinition));
// When/Then
assertThatThrownBy(() -> workflowDefinitionService.publish(1L))
.isInstanceOf(BusinessException.class)
.hasMessage("只有草稿状态的工作流定义可以发布");
}
@Test
void createNewVersion_ShouldCreateNewVersion() {
// Given
when(workflowDefinitionRepository.findById(anyLong())).thenReturn(Optional.of(mockWorkflowDefinition));
when(workflowDefinitionRepository.findLatestVersionByCode(anyString())).thenReturn(1);
when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinition);
when(workflowDefinitionConverter.toDto(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinitionDTO);
// When
WorkflowDefinitionDTO result = workflowDefinitionService.createNewVersion(1L);
// Then
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(WorkflowStatusEnum.DRAFT);
assertThat(result.getVersion()).isEqualTo(2);
verify(workflowDefinitionRepository).save(any(WorkflowDefinition.class));
}
@Test
void findAllVersions_ShouldReturnAllVersions() {
// Given
WorkflowDefinition version1 = createWorkflowDefinition(1);
WorkflowDefinition version2 = createWorkflowDefinition(2);
List<WorkflowDefinition> versions = Arrays.asList(version2, version1);
when(workflowDefinitionRepository.findAllVersionsByCode(anyString())).thenReturn(versions);
when(workflowDefinitionConverter.toDto(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinitionDTO);
// When
List<WorkflowDefinitionDTO> results = workflowDefinitionService.findAllVersions("TEST-001");
// Then
assertThat(results).hasSize(2);
verify(workflowDefinitionRepository).findAllVersionsByCode("TEST-001");
}
private WorkflowDefinition createWorkflowDefinition(int version) {
WorkflowDefinition def = new WorkflowDefinition();
def.setId((long) version);
def.setCode("TEST-001");
def.setName("Test Workflow");
def.setVersion(version);
def.setStatus(WorkflowStatusEnum.DRAFT);
def.setEnabled(true);
return def;
} }
} }