diff --git a/backend/docs/frontend-guide.md b/backend/docs/frontend-guide.md new file mode 100644 index 00000000..60f753c8 --- /dev/null +++ b/backend/docs/frontend-guide.md @@ -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. 支持国际化 \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/dto/NodeInstanceDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/dto/NodeInstanceDTO.java new file mode 100644 index 00000000..6cfe9839 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/dto/NodeInstanceDTO.java @@ -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; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/query/NodeInstanceQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/query/NodeInstanceQuery.java new file mode 100644 index 00000000..e7d53804 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/api/query/NodeInstanceQuery.java @@ -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; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/controller/WorkflowLogApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/controller/WorkflowLogApiController.java index eed949f8..58829241 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/controller/WorkflowLogApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/controller/WorkflowLogApiController.java @@ -44,7 +44,7 @@ public class WorkflowLogApiController extends BaseController log( @Parameter(description = "工作流实例ID", required = true) @RequestParam Long workflowInstanceId, @Parameter(description = "节点ID") @RequestParam(required = false) String nodeId, diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeDefinitionConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeDefinitionConverter.java new file mode 100644 index 00000000..fe9561c2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeDefinitionConverter.java @@ -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 { + + @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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeInstanceConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeInstanceConverter.java index 64caefa8..708875b0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeInstanceConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/NodeInstanceConverter.java @@ -1,7 +1,7 @@ package com.qqchen.deploy.backend.workflow.converter; 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 org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -13,10 +13,33 @@ import org.mapstruct.Mapping; public interface NodeInstanceConverter extends BaseConverter { @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); @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); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowDefinitionConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowDefinitionConverter.java index 95a51649..421040bd 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowDefinitionConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowDefinitionConverter.java @@ -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.entity.WorkflowDefinition; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; /** * 工作流定义转换器 */ -@Mapper(config = BaseConverter.class) +@Mapper(config = BaseConverter.class, uses = {NodeDefinitionConverter.class}) public interface WorkflowDefinitionConverter extends BaseConverter { + + @Override + @Mapping(target = "nodes", source = "nodes") + WorkflowDefinitionDTO toDto(WorkflowDefinition entity); + + @Override + @Mapping(target = "nodes", source = "nodes") + WorkflowDefinition toEntity(WorkflowDefinitionDTO dto); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java index 80668abd..f5181561 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/converter/WorkflowInstanceConverter.java @@ -13,10 +13,10 @@ import org.mapstruct.Mapping; public interface WorkflowInstanceConverter extends BaseConverter { @Override - @Mapping(target = "definitionId", source = "definition.id") + @Mapping(target = "definitionId", source = "workflowDefinition.id") WorkflowInstanceDTO toDto(WorkflowInstance entity); @Override - @Mapping(target = "definition.id", source = "definitionId") + @Mapping(target = "workflowDefinition.id", source = "definitionId") WorkflowInstance toEntity(WorkflowInstanceDTO dto); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinitionDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinitionDTO.java index d70a4243..c1d8404d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinitionDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/NodeDefinitionDTO.java @@ -7,16 +7,13 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; +/** + * 节点定义DTO + */ @Data @EqualsAndHashCode(callSuper = true) public class NodeDefinitionDTO extends BaseDTO { - /** - * 工作流定义ID - */ - @NotNull(message = "工作流定义ID不能为空") - private Long workflowDefinitionId; - /** * 节点ID */ @@ -41,7 +38,18 @@ public class NodeDefinitionDTO extends BaseDTO { private String config; /** - * 序号 + * 节点描述 */ - private Integer sequence; + private String description; + + /** + * 工作流定义ID + */ + @NotNull(message = "工作流定义ID不能为空") + private Long workflowDefinitionId; + + /** + * 排序号 + */ + private Integer orderNum; } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java index a2aee719..e30d054f 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowDefinitionDTO.java @@ -7,16 +7,15 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + +/** + * 工作流定义DTO + */ @Data @EqualsAndHashCode(callSuper = true) public class WorkflowDefinitionDTO extends BaseDTO { - /** - * 工作流名称 - */ - @NotBlank(message = "工作流名称不能为空") - private String name; - /** * 工作流编码 */ @@ -24,23 +23,61 @@ public class WorkflowDefinitionDTO extends BaseDTO { private String code; /** - * 描述 + * 工作流名称 + */ + @NotBlank(message = "工作流名称不能为空") + private String name; + + /** + * 工作流描述 */ private String description; /** - * 状态 + * 工作流状态 */ - @NotNull(message = "状态不能为空") + @NotNull(message = "工作流状态不能为空") private WorkflowStatusEnum status; /** - * 节点配置(JSON) + * 版本号 + */ + @NotNull(message = "版本号不能为空") + private Integer version; + + /** + * 节点定义列表 + */ + private List nodes; + + /** + * 节点配置 */ private String nodeConfig; /** - * 流转配置(JSON) + * 流转配置 */ private String transitionConfig; + + /** + * 表单定义 + */ + private String formDefinition; + + /** + * 图形信息 + */ + private String graphDefinition; + + /** + * 是否启用 + */ + @NotNull(message = "是否启用不能为空") + private Boolean enabled = true; + + /** + * 备注 + */ + private String remark; } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java index 41ef4fd5..02fb8c08 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/DefaultWorkflowEngine.java @@ -142,7 +142,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine { } // 终止所有运行中的节点 - List runningNodes = nodeInstanceRepository.findByInstanceIdAndStatus( + List runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus( instanceId, NodeStatusEnum.RUNNING); WorkflowContext context = workflowContextFactory.create(instance); @@ -173,7 +173,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine { } // 暂停所有运行中的节点 - List runningNodes = nodeInstanceRepository.findByInstanceIdAndStatus( + List runningNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus( instanceId, NodeStatusEnum.RUNNING); for (NodeInstance node : runningNodes) { @@ -196,7 +196,7 @@ public class DefaultWorkflowEngine implements WorkflowEngine { } // 恢复所有暂停的节点 - List pausedNodes = nodeInstanceRepository.findByInstanceIdAndStatus( + List pausedNodes = nodeInstanceRepository.findByWorkflowInstanceIdAndStatus( instanceId, NodeStatusEnum.PAUSED); for (NodeInstance node : pausedNodes) { diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/config/WorkflowEngineConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/config/WorkflowEngineConfig.java new file mode 100644 index 00000000..467c7225 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/config/WorkflowEngineConfig.java @@ -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 taskExecutorMap(List executors) { + return executors.stream() + .collect(Collectors.toMap( + executor -> executor.getClass().getSimpleName().replace("TaskExecutor", "").toUpperCase(), + Function.identity() + )); + } + + @Bean + public Map nodeExecutorMap(List executors) { + return executors.stream() + .collect(Collectors.toMap( + NodeExecutor::getNodeType, + Function.identity() + )); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/context/DefaultWorkflowContext.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/context/DefaultWorkflowContext.java index 828f8527..a25a06df 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/context/DefaultWorkflowContext.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/context/DefaultWorkflowContext.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; -@Component public class DefaultWorkflowContext implements WorkflowContext { @Getter diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutor.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutor.java index c322e995..e06daeae 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutor.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskNodeExecutor.java @@ -75,9 +75,6 @@ public class TaskNodeExecutor implements NodeExecutor { private void executeTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { switch (config.getType()) { - case SHELL: - executeShellTask(config, nodeInstance, context); - break; case HTTP: executeHttpTask(config, nodeInstance, context); break; @@ -92,9 +89,6 @@ public class TaskNodeExecutor implements NodeExecutor { private void terminateTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { // 根据任务类型执行终止操作 switch (config.getType()) { - case SHELL: - terminateShellTask(config, nodeInstance, context); - break; case HTTP: terminateHttpTask(config, nodeInstance, context); 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) { // TODO: 实现HTTP请求执行 } @@ -117,10 +106,6 @@ public class TaskNodeExecutor implements NodeExecutor { // TODO: 实现Java方法调用 } - private void terminateShellTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { - // TODO: 实现Shell脚本终止 - } - private void terminateHttpTask(TaskConfig config, NodeInstance nodeInstance, WorkflowContext context) { // TODO: 实现HTTP请求终止 } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskType.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskType.java index 93d01e8d..0e02ac1a 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskType.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/executor/TaskType.java @@ -7,7 +7,6 @@ import lombok.Getter; @AllArgsConstructor public enum TaskType { - SHELL("SHELL", "Shell脚本"), HTTP("HTTP", "HTTP请求"), JAVA("JAVA", "Java方法"); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java index 81c3b523..46ecc80b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/engine/node/executor/ShellNodeExecutor.java @@ -25,7 +25,7 @@ public class ShellNodeExecutor extends AbstractNodeExecutor { @Override public NodeTypeEnum getNodeType() { - return NodeTypeEnum.TASK; + return NodeTypeEnum.SHELL; } @Override diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java index f3f4f291..2aa276de 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/NodeDefinition.java @@ -7,6 +7,9 @@ import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; +/** + * 节点定义实体 + */ @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity @@ -15,15 +18,16 @@ import lombok.EqualsAndHashCode; public class NodeDefinition extends Entity { /** - * 工作流定义ID + * 所属工作流定义 */ - @Column(name = "workflow_definition_id", nullable = false) - private Long workflowDefinitionId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "workflow_definition_id", nullable = false) + private WorkflowDefinition workflowDefinition; /** * 节点ID */ - @Column(nullable = false) + @Column(name = "node_id", nullable = false) private String nodeId; /** @@ -46,7 +50,8 @@ public class NodeDefinition extends Entity { private String config; /** - * 序号 + * 排序号 */ - private Integer sequence; + @Column(name = "order_num") + private Integer orderNum; } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java index 8c9970f9..0f51bd7d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/TransitionConfig.java @@ -41,8 +41,8 @@ public class TransitionConfig extends Entity { * - "${amount > 1000}" * - "${result.code == 200 && result.data != null}" */ - @Column(columnDefinition = "text") - private String condition; + @Column(name = "transition_condition", columnDefinition = "text") + private String transitionCondition; /** * 优先级,数字越小优先级越高 @@ -86,7 +86,7 @@ public class TransitionConfig extends Entity { * @return true if conditional, false otherwise */ public boolean isConditional() { - return condition != null && !condition.trim().isEmpty(); + return transitionCondition != null && !transitionCondition.trim().isEmpty(); } /** diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java index a1caa71a..b231d67b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowDefinition.java @@ -7,17 +7,25 @@ import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.ArrayList; +import java.util.List; + /** * 工作流定义实体 */ - @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity -@Table(name = "wf_definition") +@Table(name = "wf_workflow_definition") @LogicDelete public class WorkflowDefinition extends Entity { + /** + * 工作流编码,唯一标识 + */ + @Column(nullable = false, unique = true) + private String code; + /** * 工作流名称 */ @@ -25,32 +33,46 @@ public class WorkflowDefinition extends Entity { private String name; /** - * 工作流编码 - */ - @Column(nullable = false, unique = true) - private String code; - - /** - * 描述 + * 工作流描述 */ + @Column(columnDefinition = "TEXT") private String description; /** - * 状态 + * 工作流状态 */ @Enumerated(EnumType.STRING) @Column(nullable = false) private WorkflowStatusEnum status = WorkflowStatusEnum.DRAFT; + /** + * 是否启用 + */ + @Column(nullable = false) + private Boolean enabled = true; + /** * 节点配置(JSON) */ - @Column(columnDefinition = "TEXT") + @Column(name = "node_config", columnDefinition = "TEXT") private String nodeConfig; /** * 流转配置(JSON) */ - @Column(columnDefinition = "TEXT") + @Column(name = "transition_config", columnDefinition = "TEXT") private String transitionConfig; + + /** + * 节点定义列表 + */ + @OneToMany(mappedBy = "workflowDefinition", cascade = CascadeType.ALL, orphanRemoval = true) + private List nodes = new ArrayList<>(); + + /** + * 获取流转配置 + */ + public String getTransitionConfig() { + return transitionConfig; + } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java index fd8bc4d2..be6e7ac8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowInstance.java @@ -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.domain.Entity; import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; -import jakarta.persistence.Column; -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 jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; @@ -20,35 +14,40 @@ import java.time.LocalDateTime; @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity -@Table(name = "wf_instance") +@Table(name = "wf_workflow_instance") @LogicDelete - public class WorkflowInstance extends Entity { /** - * 工作流定义ID + * 工作流定义 */ @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "definition_id", nullable = false) - private WorkflowDefinition definition; + @JoinColumn(name = "workflow_definition_id", nullable = false) + private WorkflowDefinition workflowDefinition; /** * 项目环境ID */ - @Column(name = "project_env_id", nullable = false) + @Column(name = "project_env_id") private Long projectEnvId; + /** + * 业务标识 + */ + @Column(name = "business_key") + private String businessKey; + /** * 状态 */ @Enumerated(EnumType.STRING) - @Column(name = "status", nullable = false) - private WorkflowStatusEnum status; + @Column(nullable = false) + private WorkflowStatusEnum status = WorkflowStatusEnum.RUNNING; /** * 开始时间 */ - @Column(name = "start_time", nullable = false) + @Column(name = "start_time") private LocalDateTime startTime; /** @@ -57,15 +56,16 @@ public class WorkflowInstance extends Entity { @Column(name = "end_time") private LocalDateTime endTime; - /** - * 工作流变量(JSON) - */ - @Column(name = "variables", columnDefinition = "TEXT") - private String variables; - /** * 错误信息 */ - @Column(name = "error", columnDefinition = "TEXT") + @Column(columnDefinition = "TEXT") private String error; + + /** + * 设置工作流定义 + */ + public void setDefinition(WorkflowDefinition definition) { + this.workflowDefinition = definition; + } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java index 9997184f..1f253ac8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/entity/WorkflowVariable.java @@ -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.domain.Entity; import com.qqchen.deploy.backend.workflow.enums.VariableScopeEnum; -import jakarta.persistence.Column; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; @@ -14,7 +13,7 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity -@Table(name = "wf_variable") +@Table(name = "wf_workflow_variable") @LogicDelete public class WorkflowVariable extends Entity { @@ -33,7 +32,7 @@ public class WorkflowVariable extends Entity { /** * 变量值(JSON) */ - @Column(columnDefinition = "TEXT", nullable = false) + @Column(columnDefinition = "TEXT") private String value; /** @@ -46,5 +45,11 @@ public class WorkflowVariable extends Entity { * 变量作用域 */ @Column(nullable = false) - private String scope = VariableScopeEnum.INSTANCE.name(); + private String scope = VariableScopeEnum.GLOBAL.name(); + + /** + * 节点ID(scope为NODE时必填) + */ + @Column(name = "node_id") + private String nodeId; } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java index 4eca7b04..8c8e2992 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/enums/NodeTypeEnum.java @@ -14,7 +14,8 @@ public enum NodeTypeEnum { END("END", "结束节点"), TASK("TASK", "任务节点"), GATEWAY("GATEWAY", "网关节点"), - SUB_PROCESS("SUB_PROCESS", "子流程节点"); + SUB_PROCESS("SUB_PROCESS", "子流程节点"), + SHELL("SHELL", "Shell脚本节点"); private final String code; private final String description; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeDefinitionRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeDefinitionRepository.java index 36b38baf..1e869f2a 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeDefinitionRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeDefinitionRepository.java @@ -2,23 +2,69 @@ package com.qqchen.deploy.backend.workflow.repository; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; 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.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +/** + * 节点定义仓库接口 + */ @Repository public interface INodeDefinitionRepository extends IBaseRepository { /** - * 查询工作流定义的所有节点 + * 根据工作流定义ID查询节点列表 + * + * @param workflowDefinitionId 工作流定义ID + * @return 节点列表 */ - @Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinitionId = :definitionId AND n.deleted = false ORDER BY n.sequence") - List findByWorkflowDefinitionIdOrderBySequence(@Param("definitionId") Long definitionId); - + @Query("SELECT n FROM NodeDefinition n WHERE n.workflowDefinition.id = :workflowDefinitionId AND n.deleted = false ORDER BY n.orderNum") + List 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 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); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeInstanceRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeInstanceRepository.java index 2cd3c210..53518cfe 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeInstanceRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/INodeInstanceRepository.java @@ -23,9 +23,9 @@ public interface INodeInstanceRepository extends IBaseRepository findByWorkflowInstanceIdAndStatusAndDeletedFalse(Long workflowInstanceId, NodeStatusEnum status); - List findByInstanceIdAndStatus(Long instanceId, NodeStatusEnum status); + List findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status); - List findByInstanceId(Long instanceId); + List findByWorkflowInstanceId(Long workflowInstanceId); - void deleteByInstanceId(Long instanceId); + void deleteByWorkflowInstanceId(Long workflowInstanceId); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowDefinitionRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowDefinitionRepository.java index edee816a..0a7cd949 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowDefinitionRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowDefinitionRepository.java @@ -2,26 +2,77 @@ package com.qqchen.deploy.backend.workflow.repository; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; 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 java.util.List; +import java.util.Optional; + /** * 工作流定义仓库接口 */ @Repository public interface IWorkflowDefinitionRepository extends IBaseRepository { - + /** - * 根据编码查询工作流定义 + * 根据编码查询最新版本的工作流定义 + * + * @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 findLatestByCode(@Param("code") String code); + /** - * 检查编码是否存在 + * 根据编码和版本号查询工作流定义 + * + * @param code 工作流编码 + * @param version 版本号 + * @return 工作流定义 + */ + Optional 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 findAllVersionsByCode(@Param("code") String code); + + /** + * 根据状态查询工作流定义列表 + * + * @param status 状态 + * @return 工作流定义列表 + */ + List findByStatusAndDeletedFalse(WorkflowStatusEnum status); + + /** + * 检查编码是否已存在 + * + * @param code 工作流编码 + * @return 是否存在 */ 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); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java index 0629beb9..f25791b5 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/repository/IWorkflowVariableRepository.java @@ -36,4 +36,22 @@ public interface IWorkflowVariableRepository extends IBaseRepository 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 findByWorkflowInstanceIdAndNameAndScope( + @Param("instanceId") Long instanceId, + @Param("name") String name, + @Param("scope") String scope); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/INodeInstanceService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/INodeInstanceService.java new file mode 100644 index 00000000..a52ed888 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/INodeInstanceService.java @@ -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 { + + /** + * 根据工作流实例ID查询节点列表 + * + * @param workflowInstanceId 工作流实例ID + * @return 节点列表 + */ + List findByWorkflowInstanceId(Long workflowInstanceId); + + /** + * 根据工作流实例ID和状态查询节点列表 + * + * @param workflowInstanceId 工作流实例ID + * @param status 状态 + * @return 节点列表 + */ + List findByWorkflowInstanceIdAndStatus(Long workflowInstanceId, NodeStatusEnum status); + + /** + * 更新节点状态 + * + * @param id 节点ID + * @param status 状态 + * @param output 输出结果 + * @param error 错误信息 + */ + void updateStatus(Long id, NodeStatusEnum status, String output, String error); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowDefinitionService.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowDefinitionService.java index 32e9f773..3d134ec1 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowDefinitionService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/IWorkflowDefinitionService.java @@ -3,40 +3,93 @@ package com.qqchen.deploy.backend.workflow.service; import com.qqchen.deploy.backend.framework.service.IBaseService; import com.qqchen.deploy.backend.workflow.dto.WorkflowDefinitionDTO; import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition; -import com.qqchen.deploy.backend.workflow.enums.WorkflowStatusEnum; + +import java.util.List; /** * 工作流定义服务接口 */ public interface IWorkflowDefinitionService extends IBaseService { - + + /** + * 创建工作流定义 + * + * @param dto 工作流定义DTO + * @return 工作流定义 + */ + WorkflowDefinitionDTO create(WorkflowDefinitionDTO dto); + + /** + * 更新工作流定义 + * + * @param id 工作流定义ID + * @param dto 工作流定义DTO + * @return 工作流定义 + */ + WorkflowDefinitionDTO update(Long id, WorkflowDefinitionDTO dto); + /** * 发布工作流定义 + * + * @param id 工作流定义ID + * @return 工作流定义 */ - void publish(Long id); - + 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 findAllVersions(String code); + + /** + * 验证工作流定义 + * + * @param id 工作流定义ID + * @return 验证结果 + */ + boolean validate(Long id); + /** * 启用工作流定义 + * + * @param id 工作流定义ID + * @return 工作流定义 */ - void enable(Long id); - - /** - * 检查工作流名称是否唯一 - */ - void checkNameUnique(String name); - - /** - * 检查工作流编码是否唯一 - */ - void checkCodeUnique(String code); - - /** - * 验证工作流状态 - */ - void validateStatus(WorkflowStatusEnum status); + WorkflowDefinitionDTO enable(Long id); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/NodeInstanceServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/NodeInstanceServiceImpl.java new file mode 100644 index 00000000..273de477 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/NodeInstanceServiceImpl.java @@ -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 implements INodeInstanceService { + + @Resource + private INodeInstanceRepository nodeInstanceRepository; + + @Resource + private NodeInstanceConverter nodeInstanceConverter; + + @Override + public List findByWorkflowInstanceId(Long workflowInstanceId) { + return nodeInstanceRepository.findByWorkflowInstanceId(workflowInstanceId) + .stream() + .map(nodeInstanceConverter::toDto) + .collect(Collectors.toList()); + } + + @Override + public List 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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java index b6ed8a72..614a80dd 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowDefinitionServiceImpl.java @@ -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.exception.BusinessException; 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.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.service.IWorkflowDefinitionService; import jakarta.annotation.Resource; @@ -13,69 +15,189 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 工作流定义服务实现 + */ @Slf4j @Service -public class WorkflowDefinitionServiceImpl - extends BaseServiceImpl - implements IWorkflowDefinitionService { +public class WorkflowDefinitionServiceImpl extends BaseServiceImpl + implements IWorkflowDefinitionService { @Resource private IWorkflowDefinitionRepository workflowDefinitionRepository; - @Override - @Transactional - public void publish(Long id) { - WorkflowDefinition definition = findByIdWithLock(id); - if (definition.getStatus() != WorkflowStatusEnum.DRAFT) { - throw new BusinessException(ResponseCode.WORKFLOW_NOT_DRAFT); - } - definition.setStatus(WorkflowStatusEnum.PUBLISHED); - repository.save(definition); - } + @Resource + private INodeDefinitionRepository nodeDefinitionRepository; + + @Resource + private WorkflowDefinitionConverter workflowDefinitionConverter; @Override @Transactional - public void disable(Long id) { - WorkflowDefinition definition = findByIdWithLock(id); - if (definition.getStatus() != WorkflowStatusEnum.PUBLISHED) { - 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)) { + public WorkflowDefinitionDTO create(WorkflowDefinitionDTO dto) { + // 检查编码是否已存在 + if (workflowDefinitionRepository.existsByCodeAndDeletedFalse(dto.getCode())) { 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 @Transactional - public void validateStatus(WorkflowStatusEnum status) { - if (status == null) { - throw new BusinessException(ResponseCode.WORKFLOW_INVALID_STATUS); + public WorkflowDefinitionDTO update(Long id, WorkflowDefinitionDTO dto) { + // 获取当前工作流定义 + 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 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)); } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java index 2a75ee3a..328e7534 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/WorkflowVariableServiceImpl.java @@ -3,15 +3,14 @@ package com.qqchen.deploy.backend.workflow.service.impl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; 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.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.enums.VariableScopeEnum; import com.qqchen.deploy.backend.workflow.repository.IWorkflowVariableRepository; import com.qqchen.deploy.backend.workflow.service.IWorkflowVariableService; import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,88 +18,68 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * 工作流变量服务实现类 - */ -@Slf4j @Service public class WorkflowVariableServiceImpl extends BaseServiceImpl implements IWorkflowVariableService { @Resource private IWorkflowVariableRepository variableRepository; - private final ObjectMapper objectMapper = new ObjectMapper(); + @Resource + private ObjectMapper objectMapper; @Override @Transactional public void setVariable(Long workflowInstanceId, String name, Object value) { try { - WorkflowVariable variable = variableRepository.findByWorkflowInstanceIdAndName(workflowInstanceId, name) - .orElse(new WorkflowVariable()); + WorkflowVariable variable = variableRepository.findByWorkflowInstanceIdAndNameAndScope( + 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.setType(value.getClass().getName()); - - repository.save(variable); - } catch (Exception e) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e); + variableRepository.save(variable); + } catch (JsonProcessingException e) { + throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID); } } @Override @Transactional(readOnly = true) public Map getVariables(Long workflowInstanceId) { - try { - List variables = variableRepository.findByWorkflowInstanceId(workflowInstanceId); - Map 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); - } + List variables = variableRepository.findByWorkflowInstanceId(workflowInstanceId); + return convertToMap(variables); } @Override @Transactional(readOnly = true) public Map getVariablesByScope(Long workflowInstanceId, VariableScopeEnum scope) { - try { - List variables = variableRepository.findByWorkflowInstanceIdAndScope( - workflowInstanceId, scope.name()); - Map 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); - } + List variables = variableRepository.findByWorkflowInstanceIdAndScope(workflowInstanceId, scope.name()); + return convertToMap(variables); } @Override @Transactional public void deleteVariables(Long workflowInstanceId) { variableRepository.deleteByWorkflowInstanceId(workflowInstanceId); - log.debug("删除工作流变量成功: instanceId={}", workflowInstanceId); } - private Object deserializeValue(WorkflowVariable variable) { - try { - Class type = Class.forName(variable.getType()); - return objectMapper.readValue(variable.getValue(), type); - } catch (ClassNotFoundException e) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e); - } catch (JsonProcessingException e) { - throw new WorkflowEngineException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID, e); + private Map convertToMap(List variables) { + Map result = new HashMap<>(); + for (WorkflowVariable variable : variables) { + try { + Class type = Class.forName(variable.getType()); + Object value = objectMapper.readValue(variable.getValue(), type); + result.put(variable.getName(), value); + } catch (Exception e) { + throw new BusinessException(ResponseCode.WORKFLOW_VARIABLE_TYPE_INVALID); + } } + return result; } } \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index 9b310820..2835c9a7 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -145,7 +145,7 @@ CREATE TABLE sys_role_tag ( version INT NOT NULL DEFAULT 0 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='角色标签表'; -- 角色标签关联表 @@ -274,7 +274,7 @@ CREATE TABLE deploy_repo_group ( 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 '仓库组描述', group_id BIGINT NOT NULL COMMENT '外部系统中的组ID', parent_id BIGINT NULL COMMENT '父级仓库组ID', @@ -377,103 +377,135 @@ CREATE TABLE deploy_repo_branch ( REFERENCES sys_external_system (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库分支表'; +-- -------------------------------------------------------------------------------------- +-- 工作流相关表 +-- -------------------------------------------------------------------------------------- + -- 工作流定义表 -CREATE TABLE wf_definition ( - id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', - name VARCHAR(100) NOT NULL COMMENT '工作流名称', - code VARCHAR(50) NOT NULL COMMENT '工作流编码', - description TEXT COMMENT '描述', - status VARCHAR(20) NOT NULL COMMENT '状态', - node_config TEXT COMMENT '节点配置(JSON)', - transition_config TEXT COMMENT '流转配置(JSON)', - 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_code (code) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流定义表'; +CREATE TABLE wf_workflow_definition ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + create_by VARCHAR(255) NULL COMMENT '创建人', + create_time DATETIME(6) NULL COMMENT '创建时间', + deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除(0:未删除,1:已删除)', + update_by VARCHAR(255) NULL COMMENT '更新人', + update_time DATETIME(6) NULL COMMENT '更新时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + + code VARCHAR(100) NOT NULL COMMENT '工作流编码', + name VARCHAR(100) NOT NULL COMMENT '工作流名称', + description TEXT NULL COMMENT '工作流描述', + status VARCHAR(50) NOT NULL DEFAULT 'DRAFT' COMMENT '状态(DRAFT:草稿,PUBLISHED:已发布,DISABLED:已禁用)', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', + node_config TEXT NULL COMMENT '节点配置(JSON)', + + CONSTRAINT UK_workflow_definition_code UNIQUE (code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流定义表'; + +-- 流转配置表 +CREATE TABLE wf_transition_config ( + 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', + 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 ( - 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', - node_id VARCHAR(50) NOT NULL COMMENT '节点ID', - name VARCHAR(100) NOT NULL COMMENT '节点名称', - type VARCHAR(50) NOT NULL COMMENT '节点类型', - config TEXT COMMENT '节点配置(JSON)', - sequence 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='节点定义表'; + node_id VARCHAR(50) NOT NULL COMMENT '节点ID', + name VARCHAR(100) NOT NULL COMMENT '节点名称', + type VARCHAR(50) NOT NULL COMMENT '节点类型', + config TEXT NULL COMMENT '节点配置(JSON)', + order_num INT NOT NULL DEFAULT 0 COMMENT '排序号', + + CONSTRAINT FK_node_definition_workflow FOREIGN KEY (workflow_definition_id) REFERENCES wf_workflow_definition (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流节点定义表'; --- 工作流实例表(修改现有表) -DROP TABLE IF EXISTS sys_workflow_instance; -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 ( - id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', - workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID', - node_id VARCHAR(50) NOT NULL COMMENT '节点ID', - node_type VARCHAR(50) NOT NULL COMMENT '节点类型', - name VARCHAR(100) NOT NULL COMMENT '节点名称', - status VARCHAR(20) NOT NULL COMMENT '状���', - config TEXT COMMENT '节点配置(JSON)', - input TEXT COMMENT '输入参数(JSON)', - output TEXT COMMENT '输出结果(JSON)', - error TEXT COMMENT '错误信息', - start_time DATETIME COMMENT '开始时间', - end_time DATETIME 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_node_instance_workflow FOREIGN KEY (workflow_instance_id) REFERENCES wf_instance(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点实例表'; + 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', + node_id VARCHAR(100) NOT NULL COMMENT '节点ID', + name VARCHAR(100) NOT NULL COMMENT '节点名称', + type VARCHAR(50) NOT NULL COMMENT '节点类型', + status VARCHAR(50) NOT NULL DEFAULT 'PENDING' COMMENT '状态(PENDING:待执行,RUNNING:执行中,COMPLETED:已完成,FAILED:失败,TERMINATED:已终止)', + start_time DATETIME(6) NULL COMMENT '开始时间', + end_time DATETIME(6) NULL COMMENT '结束时间', + config TEXT NULL COMMENT '节点配置(JSON)', + input TEXT NULL COMMENT '输入参数(JSON)', + output TEXT NULL COMMENT '输出结果(JSON)', + error TEXT NULL COMMENT '错误信息', + pre_node_id VARCHAR(100) NULL COMMENT '前置节点ID', + + CONSTRAINT FK_node_instance_workflow FOREIGN KEY (workflow_instance_id) REFERENCES wf_workflow_instance (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点实例表'; -- 工作流变量表 -CREATE TABLE wf_variable ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - workflow_instance_id BIGINT NOT NULL, - name VARCHAR(255) NOT NULL, - value TEXT NOT NULL, - type VARCHAR(255), - scope VARCHAR(50), - create_time DATETIME NOT NULL, - create_by VARCHAR(50), - update_time DATETIME, - update_by VARCHAR(50), - version INT DEFAULT 0, - deleted BOOLEAN DEFAULT FALSE, - CONSTRAINT uk_wf_variable UNIQUE (workflow_instance_id, name, deleted) -); +CREATE TABLE wf_workflow_variable ( + 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', + name VARCHAR(100) NOT NULL COMMENT '变量名', + value TEXT NULL COMMENT '变量值', + type VARCHAR(50) NOT NULL COMMENT '变量类型', + scope VARCHAR(50) NOT NULL DEFAULT 'GLOBAL' COMMENT '作用域(GLOBAL:全局,NODE:节点)', + node_id VARCHAR(100) NULL COMMENT '节点ID(scope为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 ( @@ -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_node ON wf_log (workflow_instance_id, node_id); diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 719a3e53..87be5285 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -183,4 +183,42 @@ workflow.variable.not.found=工作流变量不存在 workflow.log.not.found=工作流日志不存在 workflow.transition.invalid=工作流流转配置无效 workflow.node.type.not.supported=不支持的节点类型 -workflow.condition.invalid=工作流条件配置无效 \ No newline at end of file +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=工作流并发限制超出 \ No newline at end of file diff --git a/backend/src/main/resources/messages_en_US.properties b/backend/src/main/resources/messages_en_US.properties index 1d49aba6..2a06d03a 100644 --- a/backend/src/main/resources/messages_en_US.properties +++ b/backend/src/main/resources/messages_en_US.properties @@ -3,4 +3,42 @@ department.not.found=Department not found department.code.exists=Department code already exists department.name.exists=Department name already exists department.parent.not.found=Parent department not found -department.has.children=Cannot delete department with children \ No newline at end of file +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 \ No newline at end of file diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties index 10ac55b7..c36fdd5d 100644 --- a/backend/src/main/resources/messages_zh_CN.properties +++ b/backend/src/main/resources/messages_zh_CN.properties @@ -92,4 +92,42 @@ workflow.variable.invalid=工作流变量"{0}"的值无效 workflow.permission.denied=无权操作此工作流 workflow.operation.not.allowed=当前状态不允许此操作 -workflow.concurrent.operation=工作流正在执行其他操作,请稍后重试 \ No newline at end of file +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=工作流并发限制超出 \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/repository/NodeDefinitionRepositoryTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/repository/NodeDefinitionRepositoryTest.java new file mode 100644 index 00000000..28f9496e --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/repository/NodeDefinitionRepositoryTest.java @@ -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 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 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 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; + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/repository/WorkflowDefinitionRepositoryTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/repository/WorkflowDefinitionRepositoryTest.java new file mode 100644 index 00000000..07da62c5 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/repository/WorkflowDefinitionRepositoryTest.java @@ -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 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 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 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 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; + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java b/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java index 3ea45e0f..25c32e34 100644 --- a/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java +++ b/backend/src/test/java/com/qqchen/deploy/backend/workflow/service/WorkflowDefinitionServiceTest.java @@ -1,51 +1,168 @@ 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.enums.WorkflowStatusEnum; +import com.qqchen.deploy.backend.workflow.repository.INodeDefinitionRepository; import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository; 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.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +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.Mockito.when; +import static org.mockito.Mockito.*; -@SpringBootTest( - 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") - } -) +@ExtendWith(MockitoExtension.class) class WorkflowDefinitionServiceTest { - @Resource - private IWorkflowDefinitionService workflowDefinitionService; - - @MockBean + @Mock private IWorkflowDefinitionRepository workflowDefinitionRepository; - @Test - void testCreateWorkflowDefinition() { - // 准备测试数据 - when(workflowDefinitionRepository.save(any(WorkflowDefinition.class))) - .thenAnswer(invocation -> { - WorkflowDefinition definition = invocation.getArgument(0); - definition.setId(1L); - return definition; - }); + @Mock + private INodeDefinitionRepository nodeDefinitionRepository; - // 执行测试 - // ... 测试代码 ... + @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 versions = Arrays.asList(version2, version1); + + when(workflowDefinitionRepository.findAllVersionsByCode(anyString())).thenReturn(versions); + when(workflowDefinitionConverter.toDto(any(WorkflowDefinition.class))).thenReturn(mockWorkflowDefinitionDTO); + + // When + List 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; } } \ No newline at end of file