打印了JENKINS节点日志

This commit is contained in:
dengqichen 2025-11-04 16:30:16 +08:00
parent c3ee349760
commit e27bbca4b3
43 changed files with 1740 additions and 7921 deletions

View File

@ -1,660 +0,0 @@
# Deploy Ease Platform
Deploy Ease Platform 是一个基于Spring Boot 3.x和Flowable 7.x的现代化部署管理平台提供工作流驱动的自动化部署解决方案。
## 技术栈
### 后端
- **Java 21** + **Spring Boot 3.2.0**
- **Spring Security 6.2.0** + JWT认证
- **Spring Data JPA** + Hibernate
- **Flowable 7.2.0** - 工作流引擎
- **MySQL 8.0+** + Flyway数据库版本控制
- **MapStruct 1.5.5** - 对象映射
- **QueryDSL 5.0.0** - 类型安全查询
- **OpenAPI/Swagger** - API文档
- **Lombok** - 代码简化
- **Hutool** - 工具类库
### 构建工具
- **Maven 3.8+**
## 核心功能模块
### ✅ 1. 系统管理(已完成)
#### 1.1 用户管理
- [x] 用户注册和登录
- [x] JWT Token认证
- [x] 密码重置
- [x] 用户信息维护
- [x] 用户状态管理(启用/禁用)
- [x] 部门分配
- [x] 基础CRUD操作分页查询、创建、更新、删除
**主要接口:**
```
POST /api/v1/user/login - 用户登录
GET /api/v1/user/current - 获取当前用户信息
POST /api/v1/user/{id}/reset-password - 重置密码
PUT /api/v1/user/{id}/department - 分配部门
```
#### 1.2 角色管理
- [x] 角色创建与维护
- [x] 角色标签管理
- [x] 角色权限分配
- [x] 用户角色分配
- [x] 角色权限查询
**主要接口:**
```
POST /api/v1/role - 创建角色
POST /api/v1/role/{id}/tags - 分配标签
POST /api/v1/role/{userId}/assignRoles - 分配角色
GET /api/v1/role/{id}/permissions - 获取角色权限
POST /api/v1/role/{id}/permissions - 分配权限
```
#### 1.3 权限管理
- [x] 权限创建与维护
- [x] 权限分类管理(菜单权限、按钮权限)
- [x] 权限与菜单关联
- [x] 基于权限的访问控制
#### 1.4 菜单管理
- [x] 菜单树维护
- [x] 菜单排序
- [x] 菜单显示控制
- [x] 动态菜单加载
- [x] 菜单权限配置
- [x] 权限树查询
**主要接口:**
```
GET /api/v1/menu/current - 获取当前用户菜单
GET /api/v1/menu/tree - 获取菜单树
GET /api/v1/menu/permission-tree - 获取权限树
```
#### 1.5 部门管理
- [x] 部门树形结构维护
- [x] 部门编码管理
- [x] 部门负责人设置
- [x] 部门人员管理
- [x] 部门排序
#### 1.6 租户管理
- [x] 多租户支持
- [x] 租户信息管理
- [x] 租户状态控制
**实体统计:** 9个核心实体
- User用户
- Role角色
- RoleTag角色标签
- Permission权限
- PermissionTemplate权限模板
- Menu菜单
- Department部门
- Tenant租户
- SysParam系统参数
---
### ✅ 2. 外部系统集成(已完成)
#### 2.1 外部系统管理
- [x] 外部系统统一管理Jenkins、Git等
- [x] 多种认证方式支持
- 用户名密码认证
- Token认证
- SSH密钥认证
- [x] 连接测试
- [x] 同步状态追踪
**主要接口:**
```
POST /api/v1/external-system - 创建外部系统
GET /api/v1/external-system/{id}/test-connection - 测试连接
POST /api/v1/external-system/{id}/sync - 同步数据
GET /api/v1/external-system/{id}/status - 获取同步状态
```
#### 2.2 Jenkins集成
- [x] Jenkins实例管理
- [x] 视图(View)同步
- [x] 任务(Job)同步
- [x] 构建(Build)信息同步
- [x] 构建历史记录
- [x] 同步历史追踪
**主要接口:**
```
POST /api/v1/jenkins-manager/{externalSystemId}/sync-all - 同步所有数据
POST /api/v1/jenkins-manager/{externalSystemId}/sync-views - 同步视图
POST /api/v1/jenkins-manager/{externalSystemId}/sync-jobs - 同步任务
POST /api/v1/jenkins-manager/{externalSystemId}/sync-builds - 同步构建
GET /api/v1/jenkins-manager/{externalSystemId}/instance - 获取实例信息
```
**相关实体:**
- JenkinsViewJenkins视图
- JenkinsJobJenkins任务
- JenkinsBuildJenkins构建
- JenkinsSyncHistory同步历史
#### 2.3 Git仓库集成
- [x] Git仓库组(Group)管理
- [x] Git项目(Project)管理
- [x] Git分支(Branch)管理
- [x] 多级同步(组 → 项目 → 分支)
- [x] 同步历史记录
**主要接口:**
```
POST /api/v1/repository-manager/{externalSystemId}/sync-groups - 同步仓库组
POST /api/v1/repository-manager/{externalSystemId}/groups/sync-projects - 同步项目
POST /api/v1/repository-manager/{externalSystemId}/projects/sync-branches - 同步分支
GET /api/v1/repository-manager/{externalSystemId}/instance - 获取实例信息
```
**相关实体:**
- RepositoryGroup仓库组
- RepositoryProject仓库项目
- RepositoryBranch仓库分支
- RepositorySyncHistory同步历史
---
### ✅ 3. 部署管理(已完成)
#### 3.1 应用管理
- [x] 应用创建与维护
- [x] 应用编码管理
- [x] 开发语言类型支持Java、Python、Node.js等
- [x] 应用与仓库关联
- [x] 应用分组管理
**主要接口:**
```
POST /api/v1/applications - 创建应用
GET /api/v1/applications/development-languages - 获取开发语言列表
```
**相关实体:**
- Application应用
- ProjectGroup项目组
#### 3.2 环境管理
- [x] 环境配置管理(开发、测试、生产等)
- [x] 环境编码管理
- [x] 构建类型配置Jenkins、GitLab Runner、GitHub Action
- [x] 部署方式配置K8S、Docker、VM
- [x] 项目组与环境关联
**相关实体:**
- Environment环境
#### 3.3 部署配置
- [x] 应用部署配置管理
- [x] 构建配置Schema定义
- [x] 部署操作
- [x] 部署日志记录
**主要接口:**
```
POST /api/v1/deploy-app-config - 创建部署配置
GET /api/v1/deploy-app-config/defined - 获取构建类型Schema
POST /api/v1/deploy-app-config/deploy - 执行部署
```
**相关实体:**
- DeployAppConfig部署配置
- DeployLog部署日志
---
### ✅ 4. 工作流引擎已完成基于Flowable
#### 4.1 工作流定义
- [x] 工作流设计和保存
- [x] 工作流发布管理
- [x] 工作流启用/禁用
- [x] 工作流状态管理(草稿、已发布、已禁用)
- [x] 图形化配置GraphJSON
- [x] BPMN XML自动生成
- [x] 工作流分类管理
**主要接口:**
```
POST /api/v1/workflow/definition/design - 保存工作流设计
GET /api/v1/workflow/definition/published - 查询已发布工作流
POST /api/v1/workflow/definition/{id}/published - 发布工作流
POST /api/v1/workflow/definition/{id}/disable - 禁用工作流
POST /api/v1/workflow/definition/{id}/enable - 启用工作流
GET /api/v1/workflow/definition/categories - 获取工作流分类
```
#### 4.2 工作流实例
- [x] 工作流实例启动
- [x] 工作流实例挂起
- [x] 工作流实例恢复
- [x] 工作流执行状态查询
- [x] 历史实例查询
- [x] 实例与模板关联查询
**主要接口:**
```
POST /api/v1/workflow/instance/start - 启动工作流
GET /api/v1/workflow/instance/templates-with-instances - 查询模板及实例
GET /api/v1/workflow/instance/historical-instances - 查询历史实例
POST /api/v1/workflow/definition/{processInstanceId}/suspend - 挂起实例
POST /api/v1/workflow/definition/{processInstanceId}/resume - 恢复实例
```
#### 4.3 节点定义与执行
- [x] 节点类型管理
- [x] 节点配置Schema
- [x] 节点实例状态追踪
- [x] 节点重试机制
- [x] 节点跳过机制
**主要接口:**
```
POST /api/v1/workflow/node-instance/{id}/retry - 重试节点
POST /api/v1/workflow/node-instance/{id}/skip - 跳过节点
```
#### 4.4 节点类型实现Delegate
- [x] **ShellNodeDelegate** - Shell脚本执行
- [x] **DeployNodeDelegate** - 部署节点执行
- [x] **ApprovalNodeDelegate** - 审批节点
- [x] **NotificationNodeDelegate** - 通知节点
- [x] **BaseNodeDelegate** - 基础节点抽象
#### 4.5 工作流日志
- [x] 实时日志记录
- [x] 节点级日志
- [x] 日志查询
**相关实体:**
- WorkflowDefinition工作流定义
- WorkflowNodeDefinition节点定义
- WorkflowInstance工作流实例
- WorkflowNodeInstance节点实例
- WorkflowLog工作流日志
---
### ✅ 5. 基础框架(已完成)
#### 5.1 通用CRUD框架
- [x] BaseController - 统一REST控制器
- [x] BaseServiceImpl - 通用服务实现
- [x] IBaseRepository - 通用数据访问接口
- [x] BaseConverter - 对象转换抽象
- [x] 统一响应格式Response<T>
- [x] 统一分页格式Page<T>
**标准CRUD接口所有Controller继承**
```
POST /{resource} - 创建
PUT /{resource}/{id} - 更新
DELETE /{resource}/{id} - 删除
GET /{resource}/{id} - 查询详情
GET /{resource} - 查询列表
GET /{resource}/page - 分页查询
GET /{resource}/list - 条件查询
POST /{resource}/batch - 批量处理
GET /{resource}/export - 导出数据
```
#### 5.2 查询框架
- [x] 基于QueryDSL的类型安全查询
- [x] 动态查询条件构建
- [x] @QueryField注解支持
- [x] 多种查询类型EQUAL、LIKE、BETWEEN、IN等
- [x] 软删除查询支持
#### 5.3 安全框架
- [x] Spring Security集成
- [x] JWT Token认证
- [x] 权限拦截器
- [x] 自定义认证入口点
- [x] JWT过滤器
#### 5.4 审计框架
- [x] 审计注解(@Audited
- [x] 自动记录创建人/创建时间
- [x] 自动记录更新人/更新时间
- [x] 乐观锁版本控制
- [x] 审计事件监听
#### 5.5 异常处理
- [x] 统一异常处理GlobalExceptionHandler
- [x] 业务异常BusinessException
- [x] 系统异常SystemException
- [x] 唯一约束异常UniqueConstraintException
- [x] 实体未找到异常EntityNotFoundException
- [x] 国际化错误消息
#### 5.6 数据库版本控制
- [x] Flyway集成
- [x] 表结构版本管理V1.0.0__init_schema.sql
- [x] 初始数据管理V1.0.1__init_data.sql
- [x] 28个数据库表
#### 5.7 其他框架功能
- [x] 国际化支持i18n
- [x] JSON序列化配置
- [x] 租户过滤器
- [x] 逻辑删除支持(@LogicDelete
- [x] 依赖注入后处理器
- [x] Formily Schema生成用于前端表单
---
## 数据库表结构28张表
### 系统管理表7张
1. `sys_tenant` - 租户表
2. `sys_department` - 部门表
3. `sys_user` - 用户表
4. `sys_param` - 系统参数表
5. `sys_role` - 角色表
6. `sys_role_tag` - 角色标签表
7. `sys_menu` - 菜单表
### 权限管理表5张
8. `sys_role_tag_relation` - 角色标签关系表
9. `sys_user_role` - 用户角色关系表
10. `sys_role_menu` - 角色菜单关系表
11. `sys_permission_template` - 权限模板表
12. `sys_template_menu` - 模板菜单关系表
13. `sys_permission` - 权限表
### 外部系统表1张
14. `sys_external_system` - 外部系统表
### Git仓库表3张
15. `deploy_repo_group` - 仓库组表
16. `deploy_repo_project` - 仓库项目表
17. `deploy_repo_branch` - 仓库分支表
### Jenkins表4张
18. Jenkins相关表通过代码推断未在SQL中完整展示
### 工作流表5张
19. `workflow_definition` - 工作流定义表
20. `workflow_node_definition` - 节点定义表
21. `workflow_instance` - 工作流实例表
22. `workflow_node_instance` - 节点实例表
23. `workflow_log` - 工作流日志表
### 部署管理表6张
24. `deploy_project_group` - 项目组表
25. `deploy_application` - 应用表
26. `deploy_environment` - 环境表
27. `deploy_project_group_environment` - 项目组环境关系表
28. `deploy_log` - 部署日志表
29. `deploy_app_config` - 应用部署配置表
---
## API接口统计
### Controller统计26个
**系统管理7个**
- UserApiController
- RoleApiController
- RoleTagApiController
- PermissionApiController
- MenuApiController
- DepartmentApiController
- TenantApiController
**部署管理8个**
- ApplicationApiController
- ProjectGroupApiController
- EnvironmentApiController
- DeployAppConfigApiController
- DeployLogApiController
- ExternalSystemApiController
- RepositoryManagerApiController
- RepositoryProjectApiController
- RepositoryGroupApiController
- RepositoryBranchApiController
**Jenkins管理5个**
- JenkinsManagerApiController
- JenkinsViewApiController
- JenkinsJobApiController
- JenkinsBuildApiController
- JenkinsSyncHistoryApiController
**工作流管理4个**
- WorkflowDefinitionApiController
- WorkflowNodeDefinitionApiController
- WorkflowInstanceApiController
- WorkflowNodeInstanceApiController
**接口方法统计:**
- 约49个自定义@Operation接口方法
- 每个Controller继承BaseController获得9个标准CRUD方法
- **总计约280+个API接口**
---
## 🚧 待完善功能
### 1. 测试覆盖
- [ ] Service层单元测试
- [ ] Controller层集成测试
- [ ] Repository层数据访问测试
- [ ] 工作流引擎测试
- **当前测试覆盖率约1%仅1个测试类**
### 2. 缓存机制
- [ ] Redis集成
- [ ] 工作流定义缓存
- [ ] 权限数据缓存
- [ ] 菜单数据缓存
- **当前状态已引入Caffeine依赖但未使用**
### 3. 监控和运维
- [ ] 健康检查端点完善
- [ ] 性能监控APM集成
- [ ] 日志聚合ELK
- [ ] 告警机制
- [ ] 链路追踪
### 4. 部署和DevOps
- [ ] Docker镜像构建
- [ ] Kubernetes部署配置
- [ ] CI/CD Pipeline
- [ ] 环境配置分离dev/test/prod
- **当前配置application.yml包含硬编码数据库密码和JWT密钥**
### 5. 文档完善
- [ ] API文档完善Swagger注释
- [ ] 架构图和流程图
- [ ] 部署手册
- [ ] 运维手册
- [ ] 最佳实践指南
### 6. 代码质量
- [ ] 代码质量检查工具集成Checkstyle、PMD、SonarQube
- [ ] TODO注释处理14处待处理
- [ ] System.out.println清理14处
- [ ] 泛化异常处理优化95处catch Exception
### 7. 安全加固
- [ ] 敏感配置外部化
- [ ] JWT密钥加密存储
- [ ] 数据库连接加密
- [ ] API访问频率限制
- [ ] SQL注入防护验证
- [ ] XSS攻击防护
### 8. 性能优化
- [ ] 数据库索引优化
- [ ] N+1查询问题排查
- [ ] 连接池配置优化当前仅10个
- [ ] 大数据量分页优化
- [ ] 异步任务处理
---
## 快速开始
### 环境要求
- JDK 21+
- Maven 3.8+
- MySQL 8.0+
- Node.js 16+(前端)
### 1. 数据库准备
```sql
CREATE DATABASE `deploy-ease-platform`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
```
### 2. 配置修改
编辑 `src/main/resources/application.yml`
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/deploy-ease-platform?...
username: your_username
password: your_password
jwt:
secret: 'your_secret_key' # 建议使用环境变量
```
⚠️ **安全提示:** 生产环境请使用环境变量或配置中心管理敏感信息!
### 3. 启动项目
```bash
# 编译项目
mvn clean package -DskipTests
# 启动服务
mvn spring-boot:run
# 或直接运行
java -jar target/deploy-cd-1.0-SNAPSHOT.jar
```
### 4. 访问服务
- **API文档** http://localhost:8080/swagger-ui.html
- **健康检查:** http://localhost:8080/actuator/health
### 5. 默认用户需通过初始化SQL配置
```
用户名admin
密码请查看V1.0.1__init_data.sql
```
---
## 项目规范
### 代码规范
详见项目根目录下的规范文件:
- `.cursor/rules/project.mdc` - 完整开发规范
- `frontend.rules` - 前端开发规范
### 核心规范要点
#### 包结构
```
com.qqchen.deploy.backend
├── framework/ # 框架核心
│ ├── annotation/ # 注解定义
│ ├── controller/ # 基础控制器
│ ├── service/ # 基础服务
│ ├── repository/ # 基础仓库
│ └── ...
├── system/ # 系统模块
│ ├── api/ # REST接口
│ ├── entity/ # 实体类
│ ├── service/ # 业务服务
│ └── ...
├── deploy/ # 部署模块
└── workflow/ # 工作流模块
```
#### 命名规范
- **Entity** 名词如User、Role
- **DTO** 实体名+DTO如UserDTO
- **Service接口** I+实体名+Service如IUserService
- **Service实现** 实体名+ServiceImpl如UserServiceImpl
- **Controller** 实体名+ApiController如UserApiController
- **Repository** I+实体名+Repository如IUserRepository
#### 注解使用
- **@ServiceType(DATABASE)** - 数据库服务
- **@ServiceType(INTEGRATION)** - 集成服务(禁用数据库操作)
- **@LogicDelete** - 启用逻辑删除
- **@Audited** - 启用审计
- **@QueryField** - 查询字段映射
---
## 贡献指南
### 提交规范
```
feat: 新功能
fix: Bug修复
docs: 文档更新
style: 代码格式调整
refactor: 重构
perf: 性能优化
test: 测试相关
chore: 构建/工具变动
```
### 分支管理
- `master` - 生产环境分支
- `develop` - 开发分支
- `feature/*` - 功能分支
- `bugfix/*` - Bug修复分支
- `hotfix/*` - 热修复分支
---
## 许可证
MIT License
---
## 联系方式
项目维护者Deploy Ease Team
---
## 更新日志
### v1.0.0(当前版本)
- ✅ 完成系统管理核心功能
- ✅ 完成Jenkins和Git集成
- ✅ 完成工作流引擎基础功能
- ✅ 完成部署管理框架
- ✅ 完成28张数据库表设计
- ✅ 完成280+个API接口
- ⚠️ 测试覆盖率待提升
- ⚠️ 缓存机制待实现
- ⚠️ 生产环境配置待优化
---
**注意事项:**
1. 本项目处于开发阶段,部分功能仍在完善中
2. 生产环境使用前请务必完成安全加固
3. 建议补充完整的单元测试和集成测试
4. 数据库配置和JWT密钥需要外部化管理
5. 建议集成专业的日志分析和监控方案

View File

@ -1,659 +0,0 @@
# API 接口文档
## 1. 通用说明
### 1.1 接口规范
- 基础路径: `/api/v1`
- 请求方式: REST风格
- 数据格式: JSON
- 字符编码: UTF-8
- 时间格式: ISO8601 (YYYY-MM-DDTHH:mm:ss.SSSZ)
### 1.2 通用响应格式
```json
{
"code": 0, // 响应码0表示成功非0表示失败
"message": "成功", // 响应消息
"data": { // 响应数据
// 具体数据结构
}
}
```
### 1.3 通用查询参数
分页查询参数:
```json
{
"page": 1, // 页码从1开始
"size": 10, // 每页大小
"sort": ["id,desc"], // 排序字段,可选
"keyword": "", // 关键字搜索,可选
}
```
### 1.4 通用错误码
| 错误码 | 说明 | 处理建议 |
|--------|------|----------|
| 0 | 成功 | - |
| 1000 | 系统内部错误 | 联系管理员 |
| 1001 | 数据库操作失败 | 重试或联系管理员 |
| 1002 | 并发操作冲突 | 刷新后重试 |
| 2000 | 参数验证失败 | 检查参数 |
| 2001 | 数据不存在 | 检查参数 |
| 2002 | 数据已存在 | 检查参数 |
## 2. 工作流定义<E5AE9A><E4B989>
### 2.1 创建工作流定义
**接口说明**:创建新的工作流定义
**请求路径**POST /workflow-definitions
**请求参数**
```json
{
"code": "string", // 工作流编码,必填,唯一
"name": "string", // 工作流名称,必填
"description": "string", // 描述,可选
"nodeConfig": { // 节点配置,必填
"nodes": [{
"id": "string", // 节点ID
"type": "string", // 节点类型
"name": "string", // 节点名称
"config": { // 节点配置
// 具体配置项根据节点类型定义
}
}]
},
"transitionConfig": { // 流转配置,必填
"transitions": [{
"from": "string", // 来源节点ID
"to": "string", // 目标节点ID
"condition": "string" // 流转条件
}]
},
"formDefinition": { // 表单定义,必填
// 表单配置项
},
"graphDefinition": { // 图形定义,必填
// 图形布局配置
}
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"id": "long", // 工作流定义ID
"code": "string", // 工作流编码
"name": "string", // 工作流名称
"description": "string", // 描述
"version": 1, // 版本号
"status": "DRAFT", // 状态DRAFT-草稿、PUBLISHED-已发布、DISABLED-已禁用
"enabled": true, // 是否启用
"nodeConfig": {}, // 节点配置
"transitionConfig": {}, // 流转配置
"formDefinition": {}, // 表单定义
"graphDefinition": {}, // 图形定义
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}
}
```
### 2.2 更新工作流定义
**接口说明**:更新工作流定义,仅草稿状态可更新
**请求路径**PUT /workflow-definitions/{id}
**路径参数**
- id: 工作流定义ID
**请求参数**:同创建接口
**响应数据**:同创建接口
### 2.3 发布工作流定义
**接口说明**:发布工作流定义,使其可被使用
**请求路径**POST /workflow-definitions/{id}/publish
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.4 禁用工作流定义
**接口说明**:禁用工作流定义,禁止新建实例
**请求路径**POST /workflow-definitions/{id}/disable
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.5 启用工作流定义
**接口说明**启用已<E794A8><E5B7B2>用的工作流定义
**请求路径**POST /workflow-definitions/{id}/enable
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.6 创建新版本
**接口说明**:基于现有工作流定义创建新版本
**请求路径**POST /workflow-definitions/{id}/versions
**路径参数**
- id: 工作流定义ID
**响应数据**:同创建接口
### 2.7 查询工作流定义列表
**接口说明**:分页查询工作流定义列表
**请求路径**GET /workflow-definitions
**请求参数**
```json
{
"page": 1, // 页码从1开始
"size": 10, // 每页大小
"sort": ["id,desc"], // 排序字段
"keyword": "", // 关键字搜索
"status": "DRAFT", // 状态过滤
"enabled": true // 是否启用
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"content": [{ // 列表数据
// 工作流定义数据,同创建接口
}],
"totalElements": 100, // 总记录数
"totalPages": 10, // 总页数
"size": 10, // 每页大小
"number": 1 // 当前页码
}
}
```
## 3. 工作流实例接口
### 3.1 创建工作流实例
**接口说明**:创建工作流实例
**请求路径**POST /workflow-instances
**请求参数**
```json
{
"definitionId": "long", // 工作流定义ID必填
"businessKey": "string", // 业务标识,可选
"variables": { // 初始变量,可选
"key": "value"
}
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"id": "long", // 实例ID
"definitionId": "long", // 定义ID
"businessKey": "string",// 业务标识
"status": "CREATED", // 状态CREATED-已创建、RUNNING-运行中、SUSPENDED-已暂停、COMPLETED-已完成、TERMINATED-已终止
"startTime": "string", // 开始时间
"endTime": "string", // 结束时间
"variables": {}, // 变量
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}
}
```
### 3.2 启动工作流实例
**接口说明**:启动工作流实例
**请求路径**POST /workflow-instances/{id}/start
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.3 暂停工作流实例
**接口说明**:暂停运行中的工作流实例
**请求路径**POST /workflow-instances/{id}/suspend
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.4 恢复工作流实例
**接口说明**:恢复已暂停的工作流实例
**请求路径**POST /workflow-instances/{id}/resume
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.5 终止工作流实例
**接口说明**:强制终止工作流实例
**请求路径**POST /workflow-instances/{id}/terminate
**路径参数**
- id: 实例ID
**响应数据**:同创建接口
### 3.6 查询工作流实例列表
**接口说明**:分页查询工作流实例列表
**请求路径**GET /workflow-instances
**请求参数**
```json
{
"page": 1, // 页码从1开始
"size": 10, // 每页大小
"sort": ["id,desc"], // 排序字段
"keyword": "", // 关键字搜索
"status": "RUNNING", // 状态过滤
"definitionId": "long", // 定义ID过滤
"businessKey": "string" // 业务标识过滤
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"content": [{ // 列表数据
// 工作流实例数据,同创建接口
}],
"totalElements": 100, // 总记录数
"totalPages": 10, // 总页数
"size": 10, // 每页大小
"number": 1 // 当前页码
}
}
```
## 4. 节点实例接口
### 4.1 查询节点实例列表
**接口说明**:查询工作流实例的节点列表
**请求路径**GET /workflow-instances/{instanceId}/nodes
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"status": "RUNNING" // 状态过滤,可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"id": "long", // 节点实例ID
"workflowInstanceId": "long", // 工作流实例ID
"nodeId": "string", // 节点定义ID
"nodeName": "string", // 节点名称
"nodeType": "string", // 节点类型
"status": "string", // 状态CREATED、RUNNING、COMPLETED、FAILED
"startTime": "string", // 开始时间
"endTime": "string", // 结束时间
"output": "string", // 输出结果
"error": "string", // 错误信息
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}]
}
```
## 5. 工作流变量接口
### 5.1 设置变量
**接口说明**:设置工作流实例变量
**请求路径**POST /workflow-instances/{instanceId}/variables
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"name": "string", // 变量名,必填
"value": "object", // 变量值<EFBC8C><E5BF85>
"scope": "GLOBAL" // 作用域GLOBAL-全局、NODE-节点可选默认GLOBAL
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功"
}
```
### 5.2 获取变量
**接口说明**:获取工作流实例变量
**请求路径**GET /workflow-instances/{instanceId}/variables
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"scope": "GLOBAL" // 作用域过滤,可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": {
"variableName": "variableValue"
}
}
```
## 6. 工作流日志接口
### 6.1 查询日志列表
**接口说明**:查询工作流实例日志
**请求路径**GET /workflow-instances/{instanceId}/logs
**路径参数**
- instanceId: 工作流实例ID
**请求参数**
```json
{
"nodeId": "string", // 节点ID过滤可选
"level": "INFO", // 日志级别过滤DEBUG、INFO、WARN、ERROR可选
"startTime": "string", // 开始时间过滤,可选
"endTime": "string" // 结束时间过滤,可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"id": "long", // 日志ID
"workflowInstanceId": "long", // 工作流实例ID
"nodeId": "string", // 节点ID
"level": "string", // 日志级别
"content": "string", // 日志内容
"detail": "string", // 详细信息
"createTime": "string" // 创建时间
}]
}
```
## 7. 节点类型接口
### 7.1 查询节点类型列表
**接口说明**:查询可用的节点类型列表
**请求路径**GET /node-types
**请求参数**
```json
{
"enabled": true, // 是否启用过滤,可选
"category": "TASK" // 类型分类过滤TASK、EVENT、GATEWAY可选
}
```
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"id": "long", // 节点类型ID
"code": "string", // 节点类型编码
"name": "string", // 节点类型名称
"category": "string", // 分类
"description": "string", // 描述
"enabled": true, // 是否启用
"icon": "string", // 图标
"color": "string", // 颜色
"executors": [{ // 执行器列表仅TASK类型
"code": "string", // 执行器编码
"name": "string", // 执行器名称
"description": "string", // 描述
"configSchema": {} // 配置模式
}],
"createTime": "string", // 创建时间
"updateTime": "string" // 更新时间
}]
}
```
### 7.2 获取节点执行器列表
**接口说明**:获取指定节点类型支持的执行器列表
**请求路径**GET /node-types/{code}/executors
**路径参数**
- code: 节点类型编码
**响应数据**
```json
{
"code": 0,
"message": "成功",
"data": [{
"code": "string", // 执行器编码
"name": "string", // 执行器名称
"description": "string", // 描述
"configSchema": { // 配置模式
"type": "object",
"properties": {
// 具体配置项定义
},
"required": [] // 必填项
}
}]
}
```
## 8. 错误码说明
### 8.1 系统错误 (1xxx)
- 1000: 系统内部错误
- 1001: 数据库操作失败
- 1002: 并发操作冲突
### 8.2 通用业务错误 (2xxx)
- 2000: 参数验证失败
- 2001: 数据不存在
- 2002: 数据已存在
### 8.3 工作流定义错误 (3xxx)
- 3000: 工作流定义不存在
- 3001: 工作流编码已存在
- 3002: 工作流定义非草稿状态
- 3003: 工作流定义未发布
- 3004: 工作流定义已禁用
- 3005: 节点配置无效
- 3006: 流转配置无效
- 3007: 表单配置无效
### 8.4 工作流实例错误 (4xxx)
- 4000: 工作流实例不存在
- 4001: 工作流实例状态无效
- 4002: 工作流实例已终止
- 4003: 节点实例不存在
- 4004: 变量类型无效
## 9. 数据结构说明
### 9.1 工作流状态
```typescript
enum WorkflowStatus {
DRAFT = "DRAFT", // 草稿
PUBLISHED = "PUBLISHED", // 已发布
DISABLED = "DISABLED" // 已禁用
}
```
### 9.2 实例状态
```typescript
enum InstanceStatus {
CREATED = "CREATED", // 已创建
RUNNING = "RUNNING", // 运行中
SUSPENDED = "SUSPENDED", // 已暂停
COMPLETED = "COMPLETED", // 已完成
TERMINATED = "TERMINATED"// 已终止
}
```
### 9.3 节点状态
```typescript
enum NodeStatus {
CREATED = "CREATED", // 已创建
RUNNING = "RUNNING", // 运行中
COMPLETED = "COMPLETED", // 已完成
FAILED = "FAILED" // 失败
}
```
### 9.4 日志级别
```typescript
enum LogLevel {
DEBUG = "DEBUG",
INFO = "INFO",
WARN = "WARN",
ERROR = "ERROR"
}
```
### 9.5 变量作用域
```typescript
enum VariableScope {
GLOBAL = "GLOBAL", // 全局变量
NODE = "NODE" // 节点变量
}
```
## 10. 最佳实践
### 10.1 工作流设计
1. 工作流定义创建流程:
- 创建草稿
- 配置节点和流转
- 验证配置
- 发布使用
2. 节点类型选择:
- 根据业务场景选择合适的节点类型
- 配置合适的执行器
- 设置必要的变量
### 10.2 实例管理
1. 实例生命周期管理:
- 创建 -> 启动 -> 运行 -> 完成
- 必要时可暂停或终止
- 记录关键节点日志
2. 变量使用:
- 合理使用变量作用域
- 及时清理无用变量
- 注意变量类型匹配
### 10.3 错误处理
1. 异常处理:
- 捕获所有可能的错误码
- 提供友好的错误提示
- 记录详细的错误日志
2. 重试机制:
- 对非致命错误进行重试
- 设置合理的重试间隔
- 限制最大重试次数
### 10.4 性能优化
1. 查询优化:
- 合理使用分页
- 避免大量数据查询
- 使用合适的查询条件
2. 缓存使用:
- 缓存常用数据
- 及时更新缓存
- 避免缓存穿透

File diff suppressed because it is too large Load Diff

View File

@ -1,176 +0,0 @@
# 后端开发指南
## 待开发接口清单
### 1. 工作流定义管理 (WorkflowDefinitionApiController)
#### 1.1 更新工作流定义
```typescript
PUT /api/v1/workflow-definitions/{id}
Request: {
// 基本信息
code: string; // 工作流编码
name: string; // 工作流名称
description: string; // 工作流描述
version: number; // 版本号
// 设计数据
design?: {
nodes: [{
id: string;
type: string;
position: { x: number; y: number };
data: {
name: string;
description?: string;
config: Record<string, any>;
}
}];
edges: [{
id: string;
source: string;
target: string;
type: string;
data?: {
condition?: string;
}
}]
}
}
Response: {
code: number;
data: WorkflowDefinitionDTO;
message: string;
}
```
#### 1.2 导入工作流定义(待实现)
```typescript
POST /api/v1/workflow-definitions/import
Request: FormData {
file: File; // JSON格式的工作流定义文件
}
Response: {
code: number;
data: WorkflowDefinitionDTO;
message: string;
}
```
#### 1.3 导出工作流定义(待实现)
```typescript
GET /api/v1/workflow-definitions/{id}/export
Response: File // JSON格式的工作流定义文件
```
### 2. 工作流实例管理 (WorkflowInstanceApiController)
#### 2.1 导出工作流实例
```typescript
GET /api/v1/workflow-instances/export
Query Parameters:
- startTime?: string // 开始时间
- endTime?: string // 结束时间
- status?: string // 实例状态
- keyword?: string // 关键字搜索
Response: File // Excel格式的工作流实例数据
```
### 3. 节点实例管理 (NodeInstanceApiController)
#### 3.1 导出节点实例
```typescript
GET /api/v1/node-instances/export
Query Parameters:
- workflowInstanceId?: string // 工作流实例ID
- nodeType?: string // 节点类型
- status?: string // 节点状态
- startTime?: string // 开始时间
- endTime?: string // 结束时间
Response: File // Excel格式的节点实例数据
```
### 4. 节点类型管理 (NodeTypeApiController)
#### 4.1 导出节点类型
```typescript
GET /api/v1/node-types/export
Query Parameters:
- category?: string // 节点类型分类
- enabled?: boolean // 是否启用
Response: File // Excel格式的节点类型数据
```
### 5. 工作流日志管理 (WorkflowLogApiController)
#### 5.1 导出日志
```typescript
GET /api/v1/workflow-logs/export
Query Parameters:
- workflowInstanceId?: string // 工作流实例ID
- nodeId?: string // 节点ID
- level?: string // 日志级别
- startTime?: string // 开始时间
- endTime?: string // 结束时间
Response: File // Excel格式的日志数据
```
## 开发注意事项
### 1. 工作流设计数据存储
- 工作流设计数据应该存储在 `workflow_definition` 表的 `design_data` 字段中
- 数据格式为JSON字符串
- 需要实现JSON序列化和反序列化
### 2. 数据导出功能
- 使用 Apache POI 处理Excel文件
- 支持大数据量导出
- 添加导出进度提示
- 支持自定义导出字段
### 3. 数据验证
- 工作流设计数据的完整性验证
- 节点连接的有效性验证
- 配置数据的格式验证
### 4. 错误处理
- 添加详细的错误提示
- 记录操作日志
- 支持事务回滚
### 5. 性能优化
- 大数据量导出时使用分页
- 添加必要的索引
- 使用缓存优化查询性能
## 开发优先级建议
1. 工作流设计数据的保存和获取
- 这是工作流设计器的核心功能
- 前端需要这些接口来实现基本功能
2. 工作流设计的验证
- 确保工作流设计的正确性
- 避免错误的工作流定义被发布
3. 工作流定义的导入导出
- 支持工作流定义的迁移
- 方便工作流的备份和恢复
4. 各个模块的数据导出
- 用于数据分析和备份
- 相对优先级较低
## 测试要求
1. 单元测试
- 测试各个Service层的核心方法
- 验证数据处理逻辑的正确性
2. 集成测试
- 测试API接口的功能
- 验证数据库操作的正确性
3. 性能测试
- 测试大数据量导出的性能
- 验证并发处理能力

View File

@ -1,347 +0,0 @@
# Deploy Ease Platform V2 设计文档
## 一、系统概述
### 1.1 系统目标
- 构建一个高度可扩展的自动化部署平台
- 支持灵活的工作流配置和审批流程
- 实现多环境、多租户的部署管理
- 提供完整的日志追踪和监控能力
### 1.2 核心功能
1. 项目管理
- 项目集管理
- 项目配置
- Git仓库集成
- Jenkins集成
2. 环境管理
- 环境配置
- 环境克隆
- 环境变量管理
- 权限控制
3. 工作流引擎
- 流程设计
- 节点执行
- 状态管理
- 日志追踪
4. 审批系统
- 审批流程定义
- 审批规则配置
- 审批节点管理
- 审批记录追踪
5. 配置中心
- Nacos配置管理
- 配置同步
- 版本控制
- 变更追踪
## 二、技术架构
### 2.1 技术选型
- 后端框架Spring Boot 3.2.0
- Java版本JDK 21
- ORM框架JPA + QueryDSL
- 安全框架Spring Security + JWT
- 数据库MySQL 8.0
- 缓存Redis
- 消息队列RabbitMQ
- 服务注册Nacos
### 2.2 系统架构
1. 微服务架构
- 网关服务
- 认证服务
- 工作流引擎
- 审批服务
- 配置中心
2. 分层架构
- 接口层Controller
- 业务层Service
- 数据访问层Repository
- 领域模型层Domain
3. 插件化架构
- 节点插件
- 审批插件
- 通知插件
## 三、数据库设计
### 3.1 核心表设计
{{ ... }}
### 3.2 工作流相关表
{{ ... }}
### 3.3 审批系统表
1. 审批定义表sys_approval_definition
- 基础字段id, tenant_id等
- 流程编码code
- 流程名称name
- 流程描述description
- 业务类型business_type
- 节点配置node_config
- 表单配置form_config
2. 审批流程表sys_approval_flow
- 基础字段
- 业务类型business_type
- 业务键business_key
- 模板编码template_code
- 当前节点current_node
- 流程状态status
- 流程变量variables
3. 审批节点表sys_approval_node
- 基础字段
- 流程IDflow_id
- 节点编码node_code
- 节点名称node_name
- 节点类型node_type
- 审批人类型approver_type
- 审批人列表approver_ids
- 节点状态status
- 执行时间start_time, end_time
4. 审批意见表sys_approval_comment
- 基础字段
- 流程IDflow_id
- 节点IDnode_id
- 审批人user_id
- 审批动作action
- 审批意见comment
- 附件信息attachments
## 四、审批系统设计
### 4.1 审批系统架构
1. 核心组件
- 审批引擎ApprovalEngine
- 审批规则解析器RuleParser
- 审批节点执行器NodeExecutor
- 审批事件总线EventBus
2. 扩展点
- 审批规则插件
- 审批节点类型
- 审批表单定义
- 审批通知方式
### 4.2 审批流程设计
1. 流程定义
```json
{
"nodes": [
{
"id": "start",
"type": "start",
"next": "approve1"
},
{
"id": "approve1",
"type": "approve",
"config": {
"approverType": "ROLE",
"approverIds": ["ROLE_LEADER"],
"counterSign": false
},
"next": "approve2"
},
{
"id": "approve2",
"type": "approve",
"config": {
"approverType": "USER",
"approverIds": ["user1", "user2"],
"counterSign": true
},
"next": "end"
},
{
"id": "end",
"type": "end"
}
]
}
```
2. 审批规则
```json
{
"rules": [
{
"condition": {
"field": "environment",
"operator": "equals",
"value": "PROD"
},
"template": "PROD_DEPLOY_APPROVAL"
},
{
"condition": {
"field": "changeType",
"operator": "in",
"value": ["CONFIG", "DATABASE"]
},
"template": "CHANGE_APPROVAL"
}
]
}
```
### 4.3 审批流程实现
1. 流程启动
- 规则匹配
- 创建流程实例
- 初始化节点
- 发送通知
2. 节点执行
- 权限校验
- 节点执行
- 状态更新
- 下一节点
3. 流程结束
- 结果处理
- 状态更新
- 通知相关人
### 4.4 与工作流集成
1. 工作流定义扩展
```json
{
"nodes": [
{
"id": "deploy",
"type": "deploy",
"config": {
"approval": {
"enabled": true,
"template": "DEPLOY_APPROVAL"
}
}
}
]
}
```
2. 节点执行器实现
```java
public class DeployNodeExecutor implements NodeExecutor {
@Override
public void execute(NodeInstance node) {
// 1. 检查是否需要审批
if (needApproval(node)) {
// 2. 创建审批流程
createApprovalFlow(node);
// 3. 暂停节点执行
node.suspend();
return;
}
// 4. 执行部署
executeDeploy(node);
}
}
```
## 五、系统特性
### 5.1 多租户支持
1. 数据隔离
- 租户ID字段
- 查询过滤
- 权限控制
2. 资源隔离
- 独立配置
- 独立存储
- 独立缓存
### 5.2 插件化设计
1. 节点插件
- Git操作
- Jenkins构建
- 配置同步
- 数据库变更
2. 审批插件
- 角色审批
- 用户审批
- 条件审批
- 自动审批
### 5.3 安全特性
1. 认证授权
- JWT认证
- RBAC权限
- 租户隔离
2. 数据安全
- 敏感信息加密
- 操作审计
- 数据备份
### 5.4 可观测性
1. 日志管理
- 操作日志
- 审计日志
- 性能日志
2. 监控告警
- 系统监控
- 业务监控
- 异常告警
## 六、部署架构
### 6.1 系统部署
1. 容器化部署
- Docker镜像
- K8s编排
- 服务网格
2. 高可用设计
- 多副本
- 负载均衡
- 故障转移
### 6.2 数据存储
1. 数据库
- 主从复制
- 数据备份
- 分库分表
2. 文件存储
- 分布式存储
- 文件备份
- 访问控制
## 七、后续规划
### 7.1 功能增强
1. 审批功能
- 移动端审批
- 审批委托
- 审批催办
2. 部署功能
- 灰度发布
- 回滚机制
- 批量部署
### 7.2 性能优化
1. 系统性能
- 缓存优化
- SQL优化
- 并发处理
2. 用户体验
- 响应速度
- 操作便捷
- 界面优化

View File

@ -1,887 +0,0 @@
# Deploy Ease 自动化部署平台设计文档
## 一、系统概述
### 1.1 功能模块
1. 项目集管理
- 项目集配置
- 项目管理
- Git仓库集成
- Jenkins集成
2. 环境管理
- 环境配置
- 环境克隆
- 环境变量管理
- 权限控制
3. 工作流引擎
- 流程设计
- 节点执行
- 状态管理
- 日志追踪
4. 审批流程
- 审批配置
- 审批执行
- 权限管理
- 日志记录
5. 配置中心
- Nacos配置管理
- 配置同步
- 版本控制
- 变更追踪
## 二、数据库设计
### 2.1 项目管理相关表
```sql
-- 项目集表
CREATE TABLE sys_project_group (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
code VARCHAR(50) NOT NULL COMMENT '项目集编码',
name VARCHAR(100) NOT NULL COMMENT '项目集名称',
description TEXT COMMENT '描述',
git_group_url VARCHAR(255) NOT NULL COMMENT 'Git组地址',
jenkins_folder VARCHAR(255) COMMENT 'Jenkins文件夹',
deploy_type VARCHAR(20) NOT NULL COMMENT '部署类型JENKINS/SELF_BUILD',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态ENABLED/DISABLED',
CONSTRAINT uk_group_code UNIQUE (tenant_id, code),
CONSTRAINT uk_group_name UNIQUE (tenant_id, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目集表';
-- 项目表
CREATE TABLE sys_project (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
group_id BIGINT NOT NULL COMMENT '项目集ID',
code VARCHAR(50) NOT NULL COMMENT '项目编码',
name VARCHAR(100) NOT NULL COMMENT '项目名称',
description TEXT COMMENT '描述',
git_url VARCHAR(255) NOT NULL COMMENT 'Git地址',
jenkins_job VARCHAR(255) COMMENT 'Jenkins任务名',
build_type VARCHAR(20) NOT NULL COMMENT '构建类型MAVEN/GRADLE/NPM',
enable_build BIT NOT NULL DEFAULT 1 COMMENT '是否启用构建',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态ENABLED/DISABLED',
CONSTRAINT fk_project_group FOREIGN KEY (group_id) REFERENCES sys_project_group(id),
CONSTRAINT uk_project_code UNIQUE (tenant_id, code),
CONSTRAINT uk_project_name UNIQUE (tenant_id, group_id, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目表';
```
### 2.2 环境管理相关表
```sql
-- 环境表
CREATE TABLE sys_environment (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
code VARCHAR(50) NOT NULL COMMENT '环境编码',
name VARCHAR(100) NOT NULL COMMENT '环境名称',
description TEXT COMMENT '描述',
type VARCHAR(20) NOT NULL COMMENT '环境类型DEV/TEST/UAT/PROD',
need_approval BIT NOT NULL DEFAULT 0 COMMENT '是否需要审批',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态ENABLED/DISABLED',
CONSTRAINT uk_env_code UNIQUE (tenant_id, code),
CONSTRAINT uk_env_name UNIQUE (tenant_id, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='环境表';
-- 项目环境配置表
CREATE TABLE sys_project_env (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
project_id BIGINT NOT NULL COMMENT '项目ID',
env_id BIGINT NOT NULL COMMENT '环境ID',
workflow_id BIGINT COMMENT '工作流定义ID',
config TEXT COMMENT '环境配置(JSON)',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态ENABLED/DISABLED',
CONSTRAINT fk_env_project FOREIGN KEY (project_id) REFERENCES sys_project(id),
CONSTRAINT fk_env_environment FOREIGN KEY (env_id) REFERENCES sys_environment(id),
CONSTRAINT uk_project_env UNIQUE (tenant_id, project_id, env_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目环境配置表';
```
### 2.3 工作流相关表
```sql
-- 工作流定义表
CREATE TABLE sys_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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
code VARCHAR(50) NOT NULL COMMENT '工作流编码',
name VARCHAR(100) NOT NULL COMMENT '工作流名称',
description TEXT COMMENT '描述',
content TEXT NOT NULL COMMENT '工作流定义(JSON)',
type VARCHAR(20) NOT NULL COMMENT '类型DEPLOY/CONFIG_SYNC',
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' COMMENT '状态DRAFT/PUBLISHED/DISABLED',
CONSTRAINT uk_workflow_code UNIQUE (tenant_id, code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流定义表';
-- 工作流实例表
CREATE TABLE sys_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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
project_env_id BIGINT NOT NULL COMMENT '项目环境ID',
status VARCHAR(20) NOT NULL COMMENT '状态RUNNING/COMPLETED/FAILED/CANCELED',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
variables TEXT COMMENT '工作流变量(JSON)',
error TEXT COMMENT '错误信息',
CONSTRAINT fk_instance_definition FOREIGN KEY (definition_id) REFERENCES sys_workflow_definition(id),
CONSTRAINT fk_instance_project_env FOREIGN KEY (project_env_id) REFERENCES sys_project_env(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流实例表';
-- 节点实例表
CREATE TABLE sys_node_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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL 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 '状态PENDING/RUNNING/COMPLETED/FAILED/CANCELED',
start_time DATETIME COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
input TEXT COMMENT '输入参数(JSON)',
output TEXT COMMENT '输出结果(JSON)',
error TEXT COMMENT '错误信息',
CONSTRAINT fk_node_workflow_instance FOREIGN KEY (workflow_instance_id) REFERENCES sys_workflow_instance(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点实例表';
```
### 2.4 审批相关表
```sql
-- 审批流程定义表
CREATE TABLE sys_approval_flow (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
code VARCHAR(50) NOT NULL COMMENT '流程编码',
name VARCHAR(100) NOT NULL COMMENT '流程名称',
description TEXT COMMENT '描述',
type VARCHAR(20) NOT NULL COMMENT '流程类型DEPLOY/LEAVE/NACOS_CONFIG',
content TEXT NOT NULL COMMENT '流程定义(JSON)',
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态ENABLED/DISABLED',
CONSTRAINT uk_flow_code UNIQUE (tenant_id, code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程定义表';
-- 审批任务表
CREATE TABLE sys_approval_task (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
flow_id BIGINT NOT NULL COMMENT '流程定义ID',
node_id BIGINT NOT NULL COMMENT '当前节点ID',
title VARCHAR(200) NOT NULL COMMENT '审批标题',
status VARCHAR(20) NOT NULL COMMENT '状态PENDING/APPROVED/REJECTED/CANCELED',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
variables TEXT COMMENT '审批变量(JSON)',
CONSTRAINT fk_task_flow FOREIGN KEY (flow_id) REFERENCES sys_approval_flow(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批任务表';
-- 审批记录表
CREATE TABLE sys_approval_record (
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 '是否删除',
update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
task_id BIGINT NOT NULL COMMENT '审批任务ID',
node_id BIGINT NOT NULL COMMENT '审批节点ID',
approver VARCHAR(100) NOT NULL COMMENT '审批人',
action VARCHAR(20) NOT NULL COMMENT '操作APPROVE/REJECT',
comment TEXT COMMENT '审批意见',
CONSTRAINT fk_record_task FOREIGN KEY (task_id) REFERENCES sys_approval_task(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表';
```
## 三、API接口设计
### 3.1 项目管理接口
#### 3.1.1 项目集管理
```yaml
/api/v1/projectGroup-group:
post:
summary: 创建项目集
description: |
创建新的项目集,需要提供以下信息:
1. 项目集基本信息(编码、名称、描述)
2. Git仓库信息
3. Jenkins配置可选
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectGroupDTO'
responses:
200:
description: 成功创建项目集
content:
application/json:
schema:
$ref: '#/components/schemas/Response'
get:
summary: 查询项目集列表
description: |
支持以下查询条件:
1. 名称模糊搜索
2. 状态过滤
3. 分页参数
parameters:
- name: name
in: query
description: 项目集名称
schema:
type: string
- name: status
in: query
description: 状态
schema:
type: string
enum: [ENABLED, DISABLED]
responses:
200:
description: 项目集列表
content:
application/json:
schema:
$ref: '#/components/schemas/PageResponse'
```
#### 3.1.2 项目管理
```yaml
/api/v1/projectGroup:
post:
summary: 创建项目
description: |
创建新项目,需要提供:
1. 项目基本信息
2. 所属项目集
3. Git仓库信息
4. 构建配置
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectDTO'
responses:
200:
description: 成功创建项目
get:
summary: 查询项目列表
description: |
支持条件:
1. 项目集ID
2. 名称搜索
3. 状态过滤
parameters:
- name: groupId
in: query
required: true
schema:
type: integer
- name: name
in: query
schema:
type: string
responses:
200:
description: 项目列表
```
### 3.2 工作流管理接口
#### 3.2.1 工作流设计
```yaml
/api/v1/workflow/definition:
post:
summary: 创建工作流
description: |
创建工作流定义,支持:
1. 节点配置
2. 连线规则
3. 变量定义
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowDefinitionDTO'
get:
summary: 获取工作流定义
description: |
查询工作流定义,支持:
1. 基本信息
2. 节点信息
3. 连线信息
parameters:
- name: id
in: path
required: true
schema:
type: integer
```
#### 3.2.2 工作流执行
```yaml
/api/v1/workflow/instance:
post:
summary: 启动工作流
description: |
启动工作流实例,需要:
1. 工作流定义ID
2. 项目环境ID
3. 执行参数
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowStartDTO'
get:
summary: 查询执行状态
description: |
查询工作流执行状态,包括:
1. 整体状态
2. 节点状态
3. 执行日志
parameters:
- name: id
in: path
required: true
schema:
type: integer
```
## 四、前端集成指南
### 4.1 工作流设计器
#### 4.1.1 组件结构
```typescript
// 工作流设计器组件
const WorkflowDesigner: React.FC = () => {
// 1. 画布配置
const graphConfig = {
container: 'workflow-container',
grid: true,
history: true,
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
highlight: true,
},
};
// 2. 节点配置
const nodeConfig = {
// Git同步节点
gitSync: {
width: 120,
height: 60,
attrs: {
body: {
fill: '#E7F7FF',
stroke: '#1890FF',
},
label: {
text: 'Git同步',
},
},
},
// 其他节点配置...
};
return (
<div className="workflow-designer">
<Toolbar>
<NodePalette nodes={nodeConfig} />
<div className="actions">
<Button onClick={handleSave}>保存</Button>
<Button onClick={handleDeploy}>部署</Button>
</div>
</Toolbar>
<div className="designer-container">
<div id="workflow-container" className="graph-container" />
<NodeConfig node={selectedNode} onChange={handleNodeChange} />
</div>
<LogViewer instance={currentInstance} />
</div>
);
};
```
#### 4.1.2 节点配置
```typescript
// 节点配置组件
const NodeConfig: React.FC<{node: Node}> = ({ node }) => {
// 1. 动态加载配置组件
const ConfigComponent = nodeConfigComponents[node.type];
// 2. 处理配置变更
const handleConfigChange = (config: any) => {
// 更新节点配置
node.updateConfig(config);
};
return (
<div className="node-config">
<ConfigComponent
value={node.config}
onChange={handleConfigChange}
/>
</div>
);
};
// Git同步节点配置
const GitSyncConfig: React.FC<{value: any, onChange: (config: any) => void}> = ({
value,
onChange,
}) => {
return (
<Form layout="vertical">
<Form.Item label="分支" required>
<Input
value={value.branch}
onChange={e => onChange({ ...value, branch: e.target.value })}
/>
</Form.Item>
<Form.Item label="本地路径" required>
<Input
value={value.localPath}
onChange={e => onChange({ ...value, localPath: e.target.value })}
/>
</Form.Item>
</Form>
);
};
```
#### 4.1.3 日志查看
```typescript
// 日志查看器组件
const LogViewer: React.FC<{instance: WorkflowInstance}> = ({ instance }) => {
const [logs, setLogs] = useState<LogMessage[]>([]);
// 1. 建立WebSocket连接
useEffect(() => {
const ws = new WebSocket(`ws://localhost:8080/api/logs/${instance.id}`);
ws.onmessage = (event) => {
const log = JSON.parse(event.data);
setLogs(prev => [...prev, log]);
};
return () => ws.close();
}, [instance]);
// 2. 渲染日志
return (
<div className="log-viewer">
<Tabs>
<TabPane tab="流程日志" key="workflow">
<LogList logs={logs.filter(log => log.type === 'workflow')} />
</TabPane>
<TabPane tab="节点日志" key="node">
<LogList logs={logs.filter(log => log.type === 'node')} />
</TabPane>
</Tabs>
</div>
);
};
```
### 4.2 状态管理
#### 4.2.1 工作流状态
```typescript
// 工作流状态管理
const useWorkflowStore = create<WorkflowStore>((set) => ({
definitions: [],
instances: [],
// 加载工作流定义
loadDefinitions: async () => {
const response = await api.get('/api/v1/workflow/definition');
set({ definitions: response.data });
},
// 创建工作流实例
createInstance: async (params) => {
const response = await api.post('/api/v1/workflow/instance', params);
set(state => ({
instances: [...state.instances, response.data],
}));
},
// 更新实例状态
updateInstanceStatus: (id, status) => {
set(state => ({
instances: state.instances.map(instance =>
instance.id === id
? { ...instance, status }
: instance
),
}));
},
}));
```
#### 4.2.2 审批状态
```typescript
// 审批状态管理
const useApprovalStore = create<ApprovalStore>((set) => ({
tasks: [],
records: [],
// 加载待办任务
loadTasks: async () => {
const response = await api.get('/api/v1/approval/task');
set({ tasks: response.data });
},
// 审批通过
approve: async (taskId, comment) => {
await api.post(`/api/v1/approval/task/${taskId}/approve`, { comment });
set(state => ({
tasks: state.tasks.filter(task => task.id !== taskId),
}));
},
// 审批拒绝
reject: async (taskId, comment) => {
await api.post(`/api/v1/approval/task/${taskId}/reject`, { comment });
set(state => ({
tasks: state.tasks.filter(task => task.id !== taskId),
}));
},
}));
```
## 五、部署指南
### 5.1 环境要求
1. Java 21
2. MySQL 8.0
3. Redis 7.x
4. Node.js 18+
### 5.2 配置说明
```yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/deploy_ease
username: root
password: root
redis:
host: localhost
port: 6379
rabbitmq:
host: localhost
port: 5672
workflow:
executor:
core-pool-size: 10
max-pool-size: 20
queue-capacity: 100
node:
timeout: 3600 # 节点执行超时时间(秒)
retry:
max-attempts: 3
delay: 5000
```
### 5.3 部署步骤
1. 数据库初始化
```bash
mysql -u root -p deploy_ease < schema.sql
mysql -u root -p deploy_ease < data.sql
```
2. 后端部署
```bash
mvn clean package
java -jar deploy-ease.jar
```
3. 前端部署
```bash
npm install
npm run build
```
### 5.4 监控配置
1. Prometheus监控
```yaml
scrape_configs:
- job_name: 'deploy-ease'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
```
2. Grafana面板
- JVM监控
- 接口监控
- 业务监控
## 六、系统优化增强
### 6.1 分布式工作流支持
```java
/**
* 分布式工作流引擎增强
*/
public interface DistributedWorkflowEngine extends WorkflowEngine {
// 分布式锁支持
void acquireWorkflowLock(String lockKey, Duration timeout);
void releaseWorkflowLock(String lockKey);
// 集群协调
void registerWorker(WorkerInfo workerInfo);
void updateWorkerStatus(String workerId, WorkerStatus status);
// 任务分发
void assignTask(String workerId, WorkflowTask task);
void reassignTask(String fromWorkerId, String toWorkerId, WorkflowTask task);
// 故障转移
void handleWorkerFailure(String workerId);
void recoverTasks(String failedWorkerId);
}
```
### 6.2 工作流版本控制
```java
/**
* 工作流版本管理
*/
public interface WorkflowVersionManager {
// 版本控制
String createVersion(WorkflowDefinition definition);
WorkflowDefinition getVersion(String versionId);
List<WorkflowVersion> getVersionHistory(String code);
// 版本迁移
void migrateInstances(String fromVersion, String toVersion);
void rollbackVersion(String versionId);
// 版本比较
WorkflowDiff compareVersions(String version1, String version2);
}
```
### 6.3 动态节点支持
```java
/**
* 动态节点管理器
*/
public interface DynamicNodeManager {
// 动态节点注册
void registerNodeType(String type, NodeExecutor executor);
void unregisterNodeType(String type);
// 动态配置
void updateNodeConfig(String type, NodeConfig config);
NodeConfig getNodeConfig(String type);
// 节点发现
List<NodeTypeInfo> discoverNodes();
boolean isNodeTypeAvailable(String type);
}
```
### 6.4 工作流监控增强
```java
/**
* 增强的工作流监控
*/
@Component
@Slf4j
public class EnhancedWorkflowMonitor extends PerformanceMonitor {
// 业务监控指标
private final Counter businessErrorCounter;
private final Counter approvalTimeoutCounter;
private final Timer approvalDurationTimer;
// 资源监控
private final Gauge databaseConnectionGauge;
private final Gauge cacheUsageGauge;
private final Gauge messageQueueDepthGauge;
// SLA监控
private final Timer workflowSLATimer;
private final Counter slaViolationCounter;
public void recordBusinessError(String errorType, String errorCode) {
businessErrorCounter.increment();
// 记录详细错误信息
}
public void checkSLA(WorkflowInstance instance) {
Duration duration = Duration.between(instance.getStartTime(), LocalDateTime.now());
workflowSLATimer.record(duration);
if (duration.compareTo(instance.getSLADuration()) > 0) {
slaViolationCounter.increment();
// 触发SLA违规告警
}
}
}
```
### 6.5 工作流数据分析
```java
/**
* 工作流分析服务
*/
@Service
@Slf4j
public class WorkflowAnalyticsService {
private final WorkflowInstanceRepository instanceRepository;
private final NodeInstanceRepository nodeRepository;
private final AnalyticsEngine analyticsEngine;
// 性能分析
public PerformanceReport analyzePerformance(String workflowType, DateRange range) {
// 分析工作流执行性能
return analyticsEngine.analyzePerformance(workflowType, range);
}
// 瓶颈分析
public List<BottleneckNode> findBottlenecks(String workflowType) {
// 识别执行瓶颈
return analyticsEngine.findBottlenecks(workflowType);
}
// 趋势分析
public TrendReport analyzeTrend(String metric, DateRange range) {
// 分析指标趋势
return analyticsEngine.analyzeTrend(metric, range);
}
}
```
### 6.6 工作流调度优化
```java
/**
* 优化的工作流调度器
*/
@Component
@Slf4j
public class OptimizedWorkflowScheduler {
private final WorkloadBalancer workloadBalancer;
private final PriorityQueue<WorkflowTask> taskQueue;
// 优先级调度
public void schedulePriorityTask(WorkflowTask task) {
task.setPriority(calculatePriority(task));
taskQueue.offer(task);
}
// 负载均衡
public void balanceWorkload() {
List<WorkerNode> workers = workloadBalancer.getActiveWorkers();
for (WorkerNode worker : workers) {
if (worker.isOverloaded()) {
redistributeTasks(worker);
}
}
}
// 资源预留
public void reserveResources(WorkflowTask task) {
ResourceRequirements requirements = task.getResourceRequirements();
resourceManager.reserve(requirements);
}
}
```

View File

@ -1 +0,0 @@

View File

@ -1,869 +0,0 @@
# 工作流前端对接文档
## 一、系统架构
### 1.1 整体架构
```typescript
// 系统分层
interface SystemArchitecture {
presentation: {
views: '页面组件', // 负责页面展示和用户交互
components: '通用组件', // 可复用的UI组件
hooks: '业务钩子', // 封装业务逻辑的Hooks
};
domain: {
models: '数据模型', // 核心业务模型定义
services: '领域服务', // 业务逻辑封装
stores: '状态管理', // 全局状态管理
};
infrastructure: {
api: 'API封装', // 后端接口调用
utils: '工具函数', // 通用工具方法
constants: '常量定义', // 系统常量
};
}
```
### 1.2 核心模块
1. **工作流设计器**
- 负责工作流的可视化设计
- 节点拖拽和连线
- 属性配置面板
- 数据验证和保存
2. **表单设计器**
- 工作流表单的可视化设计
- 字段配置和验证规则
- 布局设计
- 表单预览
3. **工作流引擎**
- 工作流实例管理
- 节点状态控制
- 变量管理
- 日志记录
## 二、数据模型
### 2.1 工作流定义
```typescript
interface WorkflowDefinition {
// 基本信息
id: number; // 工作流定义ID
code: string; // 工作流编码(唯一标识)
name: string; // 工作流名称
description: string; // 工作流描述
version: number; // 版本号
status: WorkflowStatus; // 状态DRAFT/PUBLISHED/DISABLED
enabled: boolean; // 是否启用
// 节点定义
nodes: Array<{
id: number; // 节点定义ID
nodeId: string; // 节点标识(在图中的唯一标识)
name: string; // 节点名称
type: NodeType; // 节点类型START/END/TASK/GATEWAY
config: string; // 节点配置JSON字符串
description: string; // 节点描述
orderNum: number; // 排序号
}>;
// 配置数据
nodeConfig: string; // 节点关系配置JSON字符串
transitionConfig: string; // 流转配置JSON字符串
formDefinition: string; // 表单定义JSON字符串
graphDefinition: string; // 图形布局JSON字符串
}
// 节点配置示例JSON格式
interface NodeConfig {
// 开始节点配置
startNode: {
type: 'START';
name: string;
config?: Record<string, any>;
};
// 任务节点配置
taskNodes: Array<{
id: string;
type: 'TASK';
name: string;
executor?: string; // 执行器类型
config: {
// Shell执行器配置
script?: string; // 脚本内容
timeout?: number; // 超时时间
workingDir?: string; // 工作目录
// Jenkins执行器配置
jenkinsJob?: string; // Jenkins任务名
parameters?: Record<string, any>; // 构建参数
};
}>;
// 网关节点配置
gatewayNodes: Array<{
id: string;
type: 'GATEWAY';
name: string;
gatewayType: 'EXCLUSIVE' | 'PARALLEL' | 'INCLUSIVE'; // 网关类型
conditions?: Array<{
expression: string; // 条件表达式
description: string; // 条件描述
}>;
}>;
// 结束节点配置
endNode: {
type: 'END';
name: string;
config?: Record<string, any>;
};
}
// 流转配置示例
interface TransitionConfig {
transitions: Array<{
from: string; // 源节点ID
to: string; // 目标节点ID
condition?: string; // 流转条件(网关节点使用)
priority?: number; // 优先级(条件分支使用)
}>;
}
// 图形布局示例
interface GraphDefinition {
nodes: Array<{
id: string; // 节点ID
type: string; // 节点类型
x: number; // X坐标
y: number; // Y坐标
properties?: { // 节点属性
width?: number;
height?: number;
color?: string;
icon?: string;
};
}>;
edges: Array<{
source: string; // 源节点ID
target: string; // 目标节点ID
points?: Array<{ // 连线控制点
x: number;
y: number;
}>;
properties?: { // 连线属性
style?: string;
arrow?: string;
label?: string;
};
}>;
}
```
### 2.2 工作流实例
```typescript
interface WorkflowInstance {
id: number; // 实例ID
definitionId: number; // 工作流定义ID
businessKey: string; // 业务标识
status: InstanceStatus; // 状态
startTime: string; // 开始时间
endTime: string; // 结束时间
variables: string; // 工作流变量JSON字符串
error: string; // 错误信息
}
interface NodeInstance {
id: number; // 实例ID
workflowInstanceId: number; // 工作流实例ID
nodeId: string; // 节点ID
nodeType: NodeType; // 节点类型
status: NodeStatus; // 节点状态
startTime: string; // 开始时间
endTime: string; // 结束时间
input: string; // 输入参数JSON字符串
output: string; // 输出结果JSON字符串
error: string; // 错误信息
}
```
## 三、接口定义
### 3.1 工作流定义接口
```typescript
interface WorkflowDefinitionAPI {
// 基础CRUD
create: {
url: '/api/v1/workflow-definitions',
method: 'POST',
data: WorkflowDefinitionDTO,
response: Response<WorkflowDefinitionDTO>
};
update: {
url: '/api/v1/workflow-definitions/{id}',
method: 'PUT',
data: WorkflowDefinitionDTO,
response: Response<WorkflowDefinitionDTO>
};
delete: {
url: '/api/v1/workflow-definitions/{id}',
method: 'DELETE',
response: Response<void>
};
getById: {
url: '/api/v1/workflow-definitions/{id}',
method: 'GET',
response: Response<WorkflowDefinitionDTO>
};
page: {
url: '/api/v1/workflow-definitions/page',
method: 'GET',
params: PageQuery,
response: Response<Page<WorkflowDefinitionDTO>>
};
// 特殊操作
publish: {
url: '/api/v1/workflow-definitions/{id}/publish',
method: 'POST',
response: Response<WorkflowDefinitionDTO>
};
disable: {
url: '/api/v1/workflow-definitions/{id}/disable',
method: 'POST',
response: Response<WorkflowDefinitionDTO>
};
enable: {
url: '/api/v1/workflow-definitions/{id}/enable',
method: 'POST',
response: Response<WorkflowDefinitionDTO>
};
}
```
### 3.2 工作流实例接口
```typescript
interface WorkflowInstanceAPI {
// 实例操作
start: {
url: '/api/v1/workflow-instances/start',
method: 'POST',
data: {
workflowCode: string; // 工作流编码
businessKey: string; // 业务标识
variables?: Record<string, any>; // 工作流变量
},
response: Response<WorkflowInstanceDTO>
};
terminate: {
url: '/api/v1/workflow-instances/{id}/terminate',
method: 'POST',
data: {
reason?: string; // 终止原因
},
response: Response<void>
};
pause: {
url: '/api/v1/workflow-instances/{id}/pause',
method: 'POST',
response: Response<void>
};
resume: {
url: '/api/v1/workflow-instances/{id}/resume',
method: 'POST',
response: Response<void>
};
// 查询接口
getById: {
url: '/api/v1/workflow-instances/{id}',
method: 'GET',
response: Response<WorkflowInstanceDTO>
};
page: {
url: '/api/v1/workflow-instances/page',
method: 'GET',
params: PageQuery & {
status?: InstanceStatus;
startTime?: string;
endTime?: string;
keyword?: string;
},
response: Response<Page<WorkflowInstanceDTO>>
};
}
```
## 四、组件设计
### 4.1 工作流设计器
```typescript
// 1. 设计器组件
interface WorkflowDesigner {
// 属性定义
props: {
value?: WorkflowDefinition; // 工作流定义数据
readOnly?: boolean; // 是否只读
onChange?: (value: WorkflowDefinition) => void; // 数据变更回调
};
// 子组件
components: {
ToolBar: '工具栏', // 撤销、重做、缩放等操作
NodePanel: '节点面板', // 可用节点列表
Canvas: '画布', // 节点拖拽和连线
ConfigPanel: '配置面板', // 节点和连线配置
MiniMap: '小地图' // 导航缩略图
};
// 核心方法
methods: {
// 初始化设计器
initialize(definition: WorkflowDefinition): void;
// 添加节点
addNode(nodeData: NodeData): void;
// 连接节点
connect(source: string, target: string): void;
// 更新节点配置
updateNodeConfig(nodeId: string, config: any): void;
// 更新连线配置
updateEdgeConfig(edgeId: string, config: any): void;
// 验证数据
validate(): ValidationResult;
// 导出数据
exportData(): WorkflowDefinition;
};
}
// 2. 节点配置面板
interface NodeConfigPanel {
props: {
node: NodeData; // 节点数据
nodeType: NodeType; // 节点类型
onChange: (config: any) => void; // 配置变更回调
};
// 配置表单定义
forms: {
// 基础配置(所有节点都有)
BaseConfig: {
name: string; // 节点名称
description?: string; // 节点描述
};
// 任务节点配置
TaskConfig: {
executor: string; // 执行器类型
config: Record<string, any>; // 执行器配置
};
// 网关节点配置
GatewayConfig: {
type: 'EXCLUSIVE' | 'PARALLEL' | 'INCLUSIVE';
conditions?: Array<{
expression: string;
description: string;
}>;
};
};
}
// 3. 连线配置面板
interface EdgeConfigPanel {
props: {
edge: EdgeData; // 连线数据
sourceNode: NodeData; // 源节点
targetNode: NodeData; // 目标节点
onChange: (config: any) => void; // 配置变更回调
};
// 配置表单定义
forms: {
condition?: string; // 流转条件
priority?: number; // 优先级
description?: string; // 说明
};
}
```
### 4.2 表单设计器
```typescript
interface FormDesigner {
props: {
value?: FormDefinition; // 表单定义数据
onChange?: (value: FormDefinition) => void; // 数据变更回调
};
// 可用的字段类型
fieldTypes: {
input: '单行文本',
textarea: '多行文本',
number: '数字',
select: '下拉选择',
radio: '单选',
checkbox: '多选',
date: '日期',
datetime: '日期时间',
file: '文件上传'
};
// 字段属性定义
fieldProperties: {
name: string; // 字段名
label: string; // 字段标签
type: string; // 字段类型
required?: boolean; // 是否必填
defaultValue?: any; // 默认值
placeholder?: string; // 占位提示
description?: string; // 字段描述
options?: Array<{ // 选项用于select/radio/checkbox
label: string;
value: any;
}>;
validation?: Array<{ // 验证规则
type: string; // 规则类型
message: string; // 错误消息
params?: any; // 规则参数
}>;
};
}
```
## 五、状态管理
### 5.1 工作流设计器状态
```typescript
interface DesignerState {
// 画布状态
canvas: {
scale: number; // 缩放比例
position: { // 画布位置
x: number;
y: number;
};
selectedElements: { // 选中的元素
nodes: string[]; // 节点ID列表
edges: string[]; // 连线ID列表
};
};
// 历史记录
history: {
undoStack: HistoryItem[]; // 撤销栈
redoStack: HistoryItem[]; // 重做栈
canUndo: boolean; // 是否可撤销
canRedo: boolean; // 是否可重做
};
// 节点数据
nodes: Record<string, NodeData>; // 节点数据映射
edges: Record<string, EdgeData>; // 连线数据映射
// 配置面板
configPanel: {
visible: boolean; // 是否显示
type: 'node' | 'edge'; // 配置类型
elementId?: string; // 当前配置的元素ID
};
}
```
### 5.2 工作流实例状态
```typescript
interface InstanceState {
// 实例数据
instance: {
current?: WorkflowInstance; // 当前实例
nodes: NodeInstance[]; // 节点实例列表
variables: Record<string, any>; // 工作流变量
};
// 操作权限
permissions: {
canTerminate: boolean; // 是否可终止
canPause: boolean; // 是否可暂停
canResume: boolean; // 是否可恢复
};
// 日志数据
logs: {
workflow: WorkflowLog[]; // 工作流日志
nodes: Record<string, NodeLog[]>; // 节点日志
};
}
```
## 六、工作流设计器使用示例
### 6.1 基础使用
```typescript
// 1. 创建工作流
const createWorkflow = async (definition: WorkflowDefinition) => {
try {
// 验证数据
const validationResult = workflowDesigner.validate();
if (!validationResult.valid) {
message.error('工作流数据验证失败:' + validationResult.errors.join(', '));
return;
}
// 提交数据
const response = await WorkflowDefinitionAPI.create(definition);
if (response.code === 200) {
message.success('工作流创建成功');
return response.data;
} else {
message.error('工作流创建失败:' + response.message);
}
} catch (error) {
message.error('系统错误:' + error.message);
}
};
// 2. 加载工作流
const loadWorkflow = async (id: number) => {
try {
const response = await WorkflowDefinitionAPI.getById(id);
if (response.code === 200) {
// 初始化设计器
workflowDesigner.initialize(response.data);
return response.data;
} else {
message.error('加载工作流失败:' + response.message);
}
} catch (error) {
message.error('系统错误:' + error.message);
}
};
// 3. 保存工作流
const saveWorkflow = async (id: number) => {
try {
// 获取设计器数据
const definition = workflowDesigner.exportData();
// 验证数据
const validationResult = workflowDesigner.validate();
if (!validationResult.valid) {
message.error('工作流数据验证失败:' + validationResult.errors.join(', '));
return;
}
// 提交数据
const response = await WorkflowDefinitionAPI.update(id, definition);
if (response.code === 200) {
message.success('工作流保存成功');
return response.data;
} else {
message.error('工作流保存失败:' + response.message);
}
} catch (error) {
message.error('系统错误:' + error.message);
}
};
```
### 6.2 节点配置示例
```typescript
// 1. Shell节点配置
const ShellNodeConfig = {
type: 'TASK',
name: 'Shell脚本',
executor: 'SHELL',
config: {
script: '#!/bin/bash\necho "Hello World"',
timeout: 300,
workingDir: '/tmp'
}
};
// 2. Jenkins节点配置
const JenkinsNodeConfig = {
type: 'TASK',
name: 'Jenkins构建',
executor: 'JENKINS',
config: {
jenkinsJob: 'my-projectGroup-build',
parameters: {
BRANCH: 'master',
ENV: 'prod'
}
}
};
// 3. 网关节点配置
const GatewayNodeConfig = {
type: 'GATEWAY',
name: '条件分支',
gatewayType: 'EXCLUSIVE',
conditions: [
{
expression: '${status} == "SUCCESS"',
description: '执行成功'
},
{
expression: '${status} == "FAILED"',
description: '执行失败'
}
]
};
```
### 6.3 数据验证示例
```typescript
// 1. 节点配置验证
const validateNodeConfig = (node: NodeData): ValidationResult => {
const errors: string[] = [];
// 基础验证
if (!node.name) {
errors.push('节点名称不能为空');
}
// 任务节点特殊验证
if (node.type === 'TASK') {
if (!node.executor) {
errors.push('请选择执行器');
}
// Shell执行器验证
if (node.executor === 'SHELL') {
if (!node.config.script) {
errors.push('脚本内容不能为空');
}
}
// Jenkins执行器验证
if (node.executor === 'JENKINS') {
if (!node.config.jenkinsJob) {
errors.push('Jenkins任务名不能为空');
}
}
}
// 网关节点特殊验证
if (node.type === 'GATEWAY') {
if (!node.gatewayType) {
errors.push('请选择网关类型');
}
if (node.gatewayType === 'EXCLUSIVE' && (!node.conditions || node.conditions.length === 0)) {
errors.push('请配置分支条件');
}
}
return {
valid: errors.length === 0,
errors
};
};
// 2. 流程验证
const validateWorkflow = (definition: WorkflowDefinition): ValidationResult => {
const errors: string[] = [];
// 1. 基础信息验证
if (!definition.name) {
errors.push('工作流名称不能为空');
}
if (!definition.code) {
errors.push('工作流编码不能为空');
}
// 2. 节点验证
const nodes = JSON.parse(definition.nodeConfig);
// 验证开始节点
if (!nodes.startNode) {
errors.push('必须有一个开始节点');
}
// 验证结束节点
if (!nodes.endNode) {
errors.push('必须有一个结束节点');
}
// 验证任务节点
if (!nodes.taskNodes || nodes.taskNodes.length === 0) {
errors.push('至少需要一个任务节点');
}
// 3. 连线验证
const transitions = JSON.parse(definition.transitionConfig).transitions;
// 验证是否有孤立节点
const connectedNodes = new Set<string>();
transitions.forEach(t => {
connectedNodes.add(t.from);
connectedNodes.add(t.to);
});
const allNodes = [
nodes.startNode,
...nodes.taskNodes,
...(nodes.gatewayNodes || []),
nodes.endNode
];
allNodes.forEach(node => {
if (!connectedNodes.has(node.id)) {
errors.push(`节点 ${node.name}(${node.id}) 未连接`);
}
});
// 4. 验证是否有环
if (hasCircle(transitions)) {
errors.push('流程中存在循环');
}
return {
valid: errors.length === 0,
errors
};
};
```
## 七、最佳实践
### 7.1 性能优化
1. **大数据量处理**
- 使用虚拟滚动
- 分页加载数据
- 按需渲染节点
2. **状态管理**
- 合理使用缓存
- 避免频繁更新
- 使用不可变数据
3. **渲染优化**
- 组件懒加载
- 合理使用memo
- 避免不必要的重渲染
### 7.2 错误处理
1. **前端验证**
- 实时验证
- 提供错误提示
- 防止无效操作
2. **异常捕获**
- 全局错误处理
- 友好的错误提示
- 错误日志记录
3. **数据恢复**
- 自动保存
- 本地缓存
- 操作回滚
### 7.3 用户体验
1. **交互设计**
- 拖拽操作流畅
- 实时预览效果
- 快捷键支持
2. **反馈机制**
- 操作提示
- 加载状态
- 成功/失败反馈
3. **辅助功能**
- 自动布局
- 查找/替换
- 导入/导出
## 八、常见问题
### 8.1 配置相关
1. **节点配置问题**
- Q: 如何处理不同类型节点的配置?
- A: 使用统一的配置接口,通过类型区分不同的配置表单
2. **数据同步问题**
- Q: 如何保持前端展示数据与后端数据一致?
- A: 实现定期自动保存和加载机制
3. **验证问题**
- Q: 如何确保工作流数据的正确性?
- A: 实现多层次的验证机制,包括实时验证和提交验证
### 8.2 性能相关
1. **大型工作流**
- Q: 如何处理节点数量很多的工作流?
- A: 实现分区渲染和虚拟滚动
2. **频繁操作**
- Q: 如何处理频繁的拖拽和连线操作?
- A: 使用节流和防抖优化性能
3. **数据量大**
- Q: 如何处理大量的历史数据?
- A: 实现分页加载和懒加载机制
## 九、开发规范
### 9.1 代码规范
1. **命名规范**
- 组件使用大驼峰命名
- 方法使用小驼峰命名
- 常量使用大写下划线
2. **注释规范**
- 组件必须有文档注释
- 复杂逻辑需要注释
- 保持注释的及时更新
3. **类型规范**
- 使用TypeScript
- 定义清晰的接口
- 避免any类型
### 9.2 提交规范
1. **提交信息**
- feat: 新功能
- fix: 修复bug
- docs: 文档更新
- style: 代码格式
- refactor: 重构
- test: 测试
- chore: 构建过程或辅助工具的变动
2. **分支管理**
- master: 主分支
- develop: 开发分支
- feature: 功能分支
- hotfix: 紧急修复分支
### 9.3 测试规范
1. **单元测试**
- 组件测试
- 方法测试
- 工具函数测试
2. **集成测试**
- 流程测试
- 接口测试
- 兼容性测试
3. **E2E测试**
- 用户操作测试
- 场景测试
- 性能测试

View File

@ -1,332 +0,0 @@
# 工作流数据格式说明
## 1. 节点类型数据示例
```json
{
"id": 2003,
"createTime": "2024-12-05 12:40:03",
"createBy": "system",
"updateTime": "2024-12-05 12:40:03",
"updateBy": "system",
"version": 1,
"deleted": false,
"extraData": null,
"code": "SHELL",
"name": "Shell脚本节点",
"description": "执行Shell脚本的任务节点",
"category": "TASK",
"icon": "code",
"color": "#1890ff",
"executors": [
{
"code": "SHELL",
"name": "Shell脚本执行器",
"description": "执行Shell脚本支持配置超时时间和工作目录",
"configSchema": "{\"type\":\"object\",\"required\":[\"script\"],\"properties\":{\"script\":{\"type\":\"string\",\"title\":\"脚本内容\",\"format\":\"shell\",\"description\":\"需要执行的Shell脚本内容\"},\"timeout\":{\"type\":\"number\",\"title\":\"超时时间\",\"description\":\"脚本执行的最大时间(秒)\",\"minimum\":1,\"maximum\":3600,\"default\":300},\"workingDir\":{\"type\":\"string\",\"title\":\"工作目录\",\"description\":\"脚本执行的工作目录\",\"default\":\"/tmp\"}}}",
"defaultConfig": null
}
],
"configSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"title\": \"节点名称\",\n \"minLength\": 1,\n \"maxLength\": 50\n },\n \"description\": {\n \"type\": \"string\",\n \"title\": \"节点描述\",\n \"maxLength\": 200\n },\n \"executor\": {\n \"type\": \"string\",\n \"title\": \"执行器\",\n \"enum\": [\"SHELL\"],\n \"enumNames\": [\"Shell脚本执行器\"]\n },\n \"retryTimes\": {\n \"type\": \"number\",\n \"title\": \"重试次数\",\n \"minimum\": 0,\n \"maximum\": 3,\n \"default\": 0\n },\n \"retryInterval\": {\n \"type\": \"number\",\n \"title\": \"重试间隔(秒)\",\n \"minimum\": 1,\n \"maximum\": 300,\n \"default\": 60\n }\n },\n \"required\": [\"name\", \"executor\"]\n}",
"defaultConfig": "{\"name\": \"Shell脚本\", \"executor\": \"SHELL\", \"retryTimes\": 0, \"retryInterval\": 60}",
"enabled": true
}
```
## 2. 流程设计数据示例
```json
{
"nodes": [
{
"id": "node_1",
"type": "START",
"position": { "x": 100, "y": 100 },
"data": {
"name": "开始",
"description": "流程开始节点",
"config": {
"name": "开始",
"description": "这是一个开始节点"
}
}
}
],
"edges": [
{
"id": "edge_1",
"source": "node_1",
"target": "node_2",
"type": "default",
"data": {
"condition": null
}
}
]
}
```
## 3. 关键字段说明
### configSchema节点配置模式
**用途**
1. 前端动态生成配置表单
2. 后端验证配置数据的合法性
3. 提供配置项的约束和验证规则
### defaultConfig默认配置
**用途**
1. 新建节点时的默认值
2. 重置配置时的参考值
3. 必须符合configSchema定义的格式
### executors执行器定义
**用途**
1. 定义节点支持的执行器类型
2. 每个执行器的配置要求
3. 用于任务节点的具体执行逻辑
## 4. 字段关系说明
1. configSchema定义节点的整体配置结构
2. defaultConfig提供符合configSchema的默认值
3. executors中的configSchema定义具体执行器的配置结构
4. 实际节点配置时executorConfig需要符合选定执行器的configSchema
## 5. 前端实现指南
### 5.1 工作流设计器组件架构
推荐使用组件化设计,主要包含以下组件:
1. **WorkflowDesigner工作流设计器**
- 整体容器组件
- 负责状态管理
- 处理快捷键
- 工具栏集成
2. **NodePanel节点面板**
- 显示可用节点类型
- 支持拖拽创建节点
- 节点分类展示
3. **Canvas画布**
- 节点和连线的可视化
- 处理拖拽和连线
- 网格背景
- 缩放和平移
4. **NodeConfig节点配置**
- 动态表单生成
- 配置验证
- 实时预览
### 5.2 接口说明
#### 5.2.1 节点类型接口
```typescript
// 获取节点类型列表
GET /api/v1/node-types
Response: {
code: number;
data: NodeType[];
message: string;
}
// 获取单个节点类型详情
GET /api/v1/node-types/{id}
Response: {
code: number;
data: NodeType;
message: string;
}
```
#### 5.2.2 工作流设计接口
```typescript
// 保存工作流设计
POST /api/v1/workflows/{id}/design
Request: {
nodes: Node[];
edges: Edge[];
}
Response: {
code: number;
data: boolean;
message: string;
}
// 获取工作流设计
GET /api/v1/workflows/{id}/design
Response: {
code: number;
data: {
nodes: Node[];
edges: Edge[];
};
message: string;
}
```
### 5.3 节点连线实现
#### 5.3.1 连接点Anchors
每种类型节点的连接点定义:
```typescript
interface NodeAnchor {
id: string;
type: 'input' | 'output';
position: 'top' | 'right' | 'bottom' | 'left';
allowMultiple?: boolean; // 是否允许多条连线
}
const nodeAnchors = {
START: [
{ id: 'output', type: 'output', position: 'bottom', allowMultiple: true }
],
END: [
{ id: 'input', type: 'input', position: 'top', allowMultiple: false }
],
TASK: [
{ id: 'input', type: 'input', position: 'top', allowMultiple: false },
{ id: 'output', type: 'output', position: 'bottom', allowMultiple: true }
],
GATEWAY: [
{ id: 'input', type: 'input', position: 'top', allowMultiple: false },
{ id: 'output', type: 'output', position: 'bottom', allowMultiple: true }
]
};
```
#### 5.3.2 连线验证规则
```typescript
interface ConnectionValidation {
sourceNode: Node;
targetNode: Node;
sourceAnchor: NodeAnchor;
targetAnchor: NodeAnchor;
}
function validateConnection({
sourceNode,
targetNode,
sourceAnchor,
targetAnchor
}: ConnectionValidation): boolean {
// 1. 检查源节点和目标节点是否有效
if (!sourceNode || !targetNode) return false;
// 2. 检查是否形成循环
if (wouldCreateCycle(sourceNode, targetNode)) return false;
// 3. 检查锚点类型匹配
if (sourceAnchor.type !== 'output' || targetAnchor.type !== 'input') return false;
// 4. 检查目标锚点是否已被占用(如果不允许多重连接)
if (!targetAnchor.allowMultiple && hasExistingConnection(targetNode, targetAnchor)) {
return false;
}
return true;
}
```
#### 5.3.3 连线样式配置
```typescript
const edgeStyles = {
default: {
type: 'smoothstep', // 平滑阶梯线
animated: false,
style: {
stroke: '#b1b1b7',
strokeWidth: 2
}
},
selected: {
style: {
stroke: '#1890ff',
strokeWidth: 2
}
},
conditional: {
type: 'smoothstep',
animated: true,
style: {
stroke: '#722ed1',
strokeWidth: 2,
strokeDasharray: '5,5'
}
}
};
```
### 5.4 状态管理建议
推荐使用状态管理库如Redux或MobX管理以下状态
1. **全局状态**
- 当前工作流设计
- 可用节点类型
- 画布缩放级别
- 选中的节点/连线
2. **节点状态**
- 位置信息
- 配置数据
- 验证状态
3. **连线状态**
- 连接关系
- 条件配置
- 样式信息
### 5.5 性能优化建议
1. **渲染优化**
- 使用React.memo()优化节点渲染
- 实现虚拟滚动
- 大量节点时使用分层渲染
2. **状态更新优化**
- 批量更新状态
- 使用不可变数据结构
- 实现节点位置的防抖
3. **交互优化**
- 拖拽时使用节点预览
- 连线时显示对齐参考线
- 支持快捷键操作
### 5.6 错误处理
1. **前端验证**
- 节点配置验证
- 连线规则验证
- 数据完整性检查
2. **错误提示**
- 友好的错误信息
- 错误定位高亮
- 操作建议提示
3. **异常恢复**
- 自动保存
- 操作撤销/重做
- 状态恢复机制
## 6. 最佳实践建议
1. **代码组织**
- 使用TypeScript确保类型安全
- 遵循组件设计原则
- 实现完整的测试覆盖
2. **用户体验**
- 实现撤销/重做功能
- 支持键盘快捷键
- 添加操作引导
3. **可扩展性**
- 支持自定义节点
- 支持自定义连线样式
- 预留扩展接口

View File

@ -1,494 +0,0 @@
# 工作流引擎开发文档
## 一、已完成工作
### 1. 核心实体设计
- WorkflowDefinition: 工作流定义
- 包含基本信息(编码、名称、描述等)
- 包含流程定义内容(JSON格式)
- 支持版本控制和状态管理
- WorkflowInstance: 工作流实例
- 关联工作流定义
- 记录执行状态和进度
- 支持暂停、恢复、取消等操作
- NodeInstance: 节点实例
- 关联工作流实例
- 记录节点执行状态和结果
- 支持重试和跳过等操作
### 2. 节点类型体系
- 基础节点类型:
- START: 开始节点
- END: 结束节点
- CONDITION: 条件节点
- PARALLEL: 并行节点
- 功能节点类型:
- APPROVAL: 审批节点
- JENKINS: Jenkins构建节点
- SCRIPT: 脚本执行节点
- GIT: Git操作节点
- NACOS: 配置中心节点
- HTTP: HTTP请求节点
- NOTIFY: 通知节点
### 3. 工作流引擎实现
- 核心接口设计
- WorkflowEngine: 工作流引擎接口
- NodeExecutor: 节点执行器接口
- 默认实现
- DefaultWorkflowEngine: 默认工作流引擎实现
- AbstractNodeExecutor: 抽象节点执行器
- ShellNodeExecutor等具体执行器
### 4. 变量与日志管理
- WorkflowVariable: 工作流变量
- 支持全局变量和节点变量
- 支持变量引用和替换
- WorkflowLog: 工作流日志
- 支持不同日志类型和级别
- 支持详细的执行记录
### 5. 权限管理
- WorkflowPermission: 工作流权限
- 支持角色和用户级别的权限控制
- 支持操作级别的权限控制
### 6. API接口设计
- 工作流定义管理
- 工作流实例操作
- 节点实例管理
- 日志查询等功能
### 7. 错误码和消息
- 工作流相关错误码(2700-2799)
- 详细的错误消息定义
## 二、待完成工作
### 1. 高级功能实现
#### 1.1 节点执行引擎增强
```java
public interface NodeExecutor {
// 新增预执行检查
boolean preCheck(NodeInstance node, WorkflowContext context);
// 新增补偿操作
void compensate(NodeInstance node, WorkflowContext context);
// 新增超时处理
void handleTimeout(NodeInstance node, WorkflowContext context);
}
// 节点执行状态追踪
public class NodeExecutionTracker {
private Long nodeInstanceId;
private Date startTime;
private Date endTime;
private String status;
private Map<String, Object> metrics;
private List<String> logs;
}
```
#### 1.2 工作流调度管理
```java
@Service
public class WorkflowScheduleService {
// 创建调度任务
public void createSchedule(WorkflowSchedule schedule);
// 修改调度配置
public void updateSchedule(WorkflowSchedule schedule);
// 启用/禁用调度
public void toggleSchedule(Long scheduleId, boolean enabled);
// 手动触发
public void triggerSchedule(Long scheduleId);
}
// 调度配置表
CREATE TABLE wf_workflow_schedule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
name VARCHAR(100) NOT NULL COMMENT '调度名称',
cron VARCHAR(100) NOT NULL COMMENT 'cron表达式',
variables JSON COMMENT '工作流变量',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
last_fire_time DATETIME COMMENT '上次触发时间',
next_fire_time DATETIME COMMENT '下次触发时间',
-- 其他基础字段
);
```
#### 1.3 工作流监控告警
```java
public interface WorkflowMonitor {
// 收集性能指标
void collectMetrics(WorkflowInstance instance);
// 检查健康状态
HealthStatus checkHealth();
// 触发告警
void triggerAlert(AlertType type, String message);
}
// 监控指标表
CREATE TABLE wf_workflow_metrics (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_instance_id BIGINT NOT NULL,
metric_name VARCHAR(100) NOT NULL,
metric_value DECIMAL(19,2) NOT NULL,
collect_time DATETIME NOT NULL,
-- 其他基础字段
);
// 告警记录表
CREATE TABLE wf_workflow_alert (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
alert_type VARCHAR(50) NOT NULL,
target_type VARCHAR(50) NOT NULL,
target_id BIGINT NOT NULL,
level VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
status VARCHAR(20) NOT NULL,
-- 其他基础字段
);
```
#### 1.4 工作流分析统计
```java
public interface WorkflowAnalytics {
// 执行时长分析
Map<String, Duration> analyzeExecutionTime(Long workflowId);
// 成功率分析
Map<String, Double> analyzeSuccessRate(Long workflowId);
// 节点耗时分析
List<NodeTimeAnalysis> analyzeNodeTime(Long workflowId);
}
// 分析结果表
CREATE TABLE wf_workflow_analytics (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_definition_id BIGINT NOT NULL,
analysis_type VARCHAR(50) NOT NULL,
time_range VARCHAR(50) NOT NULL,
metrics JSON NOT NULL,
-- 其他基础字段
);
```
### 2. 功能优化计划
#### 2.1 性能优化
- 引入本地缓存和分布式缓存
- 实现工作流实例分片执行
- 优化日志存储和查询
```java
@Configuration
public class WorkflowCacheConfig {
@Bean
public Cache<String, WorkflowDefinition> definitionCache() {
return CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
}
public class ShardingWorkflowEngine implements WorkflowEngine {
// 分片执行实现
public void executeWithSharding(WorkflowInstance instance) {
String shardingKey = calculateShardingKey(instance);
ShardingContext context = createShardingContext(shardingKey);
executeInShard(instance, context);
}
}
```
#### 2.2 可靠性增强
- 实现节点执行幂等性
- 增加全局事务控制
- 完善补偿机制
```java
public abstract class IdempotentNodeExecutor implements NodeExecutor {
// 幂等性检查
protected boolean checkIdempotent(String executionId) {
return redisTemplate.opsForValue()
.setIfAbsent("node:execution:" + executionId, "1", 24, TimeUnit.HOURS);
}
}
@Service
public class CompensationService {
// 注册补偿操作
public void registerCompensation(NodeInstance node, Runnable compensation);
// 执行补偿
public void executeCompensation(WorkflowInstance instance);
}
```
#### 2.3 扩展性优化
- 支持自定义节点类型
- 支持插件化扩展
- 提供更多扩展点
```java
public interface WorkflowPlugin {
// 插件初始化
void init(WorkflowEngine engine);
// 注册扩展点
void registerExtensions();
// 清理资源
void destroy();
}
public class PluginManager {
// 加载插件
public void loadPlugins();
// 启用插件
public void enablePlugin(String pluginId);
// 禁用插件
public void disablePlugin(String pluginId);
}
```
### 3. 新特性规划
#### 3.1 工作流模板功能
```java
// 模板定义表
CREATE TABLE wf_workflow_template (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
content JSON NOT NULL,
category VARCHAR(50),
tags JSON,
-- 其他基础字段
);
// 模板参数表
CREATE TABLE wf_template_parameter (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
template_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(50) NOT NULL,
required BIT NOT NULL DEFAULT 0,
default_value VARCHAR(255),
validation_rule VARCHAR(255),
-- 其他基础字段
);
```
#### 3.2 工作流版本管理
```java
// 版本管理表
CREATE TABLE wf_workflow_version (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_definition_id BIGINT NOT NULL,
version VARCHAR(20) NOT NULL,
content JSON NOT NULL,
change_log TEXT,
status VARCHAR(20) NOT NULL,
-- 其他基础字段
);
public interface VersionManager {
// 创建新版本
String createVersion(Long workflowId, String content);
// 发布版本
void publishVersion(Long workflowId, String version);
// 回滚版本
void rollbackVersion(Long workflowId, String version);
}
```
#### 3.3 工作流迁移功能
```java
public interface WorkflowMigration {
// 导出工作流
byte[] exportWorkflow(Long workflowId);
// 导入工作流
Long importWorkflow(byte[] content);
// 迁移实例到新版本
void migrateInstance(Long instanceId, String targetVersion);
}
// 迁移记录表
CREATE TABLE wf_workflow_migration (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
source_workflow_id BIGINT NOT NULL,
target_workflow_id BIGINT NOT NULL,
migration_type VARCHAR(50) NOT NULL,
status VARCHAR(20) NOT NULL,
error_message TEXT,
-- 其他基础字段
);
```
#### 3.4 工作流测试功能
```java
public interface WorkflowTesting {
// 模拟执行
TestResult simulateExecution(Long workflowId, Map<String, Object> variables);
// 节点单元测试
TestResult testNode(NodeConfig config, Map<String, Object> inputs);
// 生成测试报告
TestReport generateReport(Long testExecutionId);
}
// 测试用例表
CREATE TABLE wf_workflow_test_case (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_definition_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
variables JSON,
expected_result JSON,
-- 其他基础字段
);
// 测试执行记录表
CREATE TABLE wf_workflow_test_execution (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
test_case_id BIGINT NOT NULL,
execution_time DATETIME NOT NULL,
status VARCHAR(20) NOT NULL,
actual_result JSON,
error_message TEXT,
-- 其他基础字段
);
```
## 三、技术架构
### 1. 整体架构
```
+------------------+
| API Layer |
+------------------+
| Service Layer |
+------------------+
| Engine Core |
+------------------+
| Storage Layer |
+------------------+
```
### 2. 关键组件
- 工作流引擎核心
- 节点执行引擎
- 变量管理器
- 日志管理器
- 权限管理器
- 调度管理器
- 监控告警组件
- 分析统计组件
### 3. 存储设计
- 核心业务表
- 执行记录表
- 监控指标表
- 分析统计表
- 日志记录表
### 4. 缓存设计
- 本地缓存
- 工作流定义缓存
- 节点配置缓存
- 分布式缓存
- 执行状态缓存
- 变量数据缓存
## 四、部署运维
### 1. 部署架构
```
+---------------+ +---------------+
| API Server | | API Server |
+---------------+ +---------------+
| |
+---------------+ +---------------+
| Engine Worker | | Engine Worker |
+---------------+ +---------------+
| |
+---------------+ +---------------+
| Node Worker | | Node Worker |
+---------------+ +---------------+
```
### 2. 监控方案
- 系统监控
- JVM指标
- 线程池状态
- 数据库连接池
- 业务监控
- 工作流执行状态
- 节点执行性能
- 错误率统计
### 3. 告警方案
- 系统告警
- 资源使用率
- 错误率阈值
- 响应时间
- 业务告警
- 执行超时
- 节点失败
- 异常终止
### 4. 运维工具
- 管理控制台
- 监控面板
- 运维脚本
- 诊断工具
## 五、项目管理
### 1. 开发计划
- Phase 1: 核心功能实现 (已完成)
- Phase 2: 高级特性开发 (进行中)
- Phase 3: 性能优化和稳定性提升
- Phase 4: 运维工具和监控体系建设
### 2. 测试策略
- 单元测试
- 集成测试
- 性能测试
- 稳定性测试
### 3. 文档规划
- 设计文档
- API文档
- 使用手册
- 运维手册
### 4. 版本规划
- v1.0: 基础功能版本
- v1.1: 高级特性版本
- v1.2: 性能优化版本
- v2.0: 企业版本

View File

@ -1,670 +0,0 @@
# 工作流引擎改造方案设计文档
## 1. 现状分析
### 1.1 现有组件
1. 工作流引擎核心组件
- `AbstractNodeExecutor`: 节点执行器抽象类,提供基础的节点执行逻辑
- `WorkflowGraph`: 工作流图模型,管理节点和转换关系
- `INodeExecutor`: 节点执行器接口
- `WorkflowInstance`: 工作流实例实体
- `NodeInstance`: 节点实例实体
2. 数据模型
```sql
-- 工作流定义表
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 VARCHAR(255) NULL COMMENT '工作流描述',
status TINYINT NOT NULL COMMENT '工作流状态0草稿1已发布2已禁用',
version_no INT NOT NULL DEFAULT 1 COMMENT '版本号',
node_config TEXT NULL COMMENT '节点配置(JSON)',
transition_config TEXT NULL COMMENT '流转配置(JSON)',
form_definition TEXT NULL COMMENT '表单定义(JSON)',
graph_definition TEXT NULL COMMENT '图形信息(JSON)',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
CONSTRAINT UK_workflow_definition_code_version UNIQUE (code, version_no)
) 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',
business_key VARCHAR(100) NOT NULL COMMENT '业务标识',
status TINYINT NOT NULL COMMENT '状态0已创建1等待执行2执行中3已完成4执行失败5已取消6已暂停7已终止',
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_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_instance_id BIGINT NOT NULL COMMENT '工作流实例ID',
node_id VARCHAR(100) NOT NULL COMMENT '节点ID',
node_type TINYINT NOT NULL COMMENT '节点类型0开始节点1结束节点2任务节点3网关节点4子流程节点5Shell脚本节点6审批节点7Jenkins任务节点8Git操作节点',
name VARCHAR(100) NOT NULL COMMENT '节点名称',
status TINYINT NOT NULL COMMENT '状态0已创建1等待执行2执行中3已完成4执行失败5已取消6已暂停7已终止',
start_time DATETIME(6) NULL COMMENT '开始时间',
end_time DATETIME(6) NULL COMMENT '结束时间',
config TEXT NULL COMMENT '节点配置(JSON)',
description TEXT NULL COMMENT '节点描述',
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_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(20) NOT NULL COMMENT '变量类型',
CONSTRAINT FK_workflow_variable_instance FOREIGN KEY (workflow_instance_id) REFERENCES wf_workflow_instance (id),
CONSTRAINT UK_workflow_variable_instance_name UNIQUE (workflow_instance_id, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作流变量表';
-- 工作流日志表
CREATE TABLE wf_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workflow_instance_id BIGINT NOT NULL COMMENT '工作流实例ID',
node_id VARCHAR(50) NULL COMMENT '节点ID',
level VARCHAR(10) NOT NULL COMMENT '日志级别',
message TEXT NOT NULL COMMENT '日志内容',
detail TEXT,
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 FK_workflow_log_instance FOREIGN KEY (workflow_instance_id) REFERENCES wf_workflow_instance (id)
);
-- 工作流权限表
CREATE TABLE wf_workflow_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
workflow_definition_id BIGINT NOT NULL COMMENT '工作流定义ID',
type VARCHAR(50) NOT NULL COMMENT '权限类型',
user_id BIGINT COMMENT '用户ID',
role_id BIGINT COMMENT '角色ID',
department_id BIGINT COMMENT '部门ID',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
create_by BIGINT NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
update_by BIGINT NOT NULL COMMENT '更新人',
version INT NOT NULL DEFAULT 0 COMMENT '版本号',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
INDEX idx_workflow_definition_id (workflow_definition_id),
INDEX idx_user_id (user_id),
INDEX idx_role_id (role_id),
INDEX idx_department_id (department_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流权限';
-- 节点类型表
CREATE TABLE wf_node_type (
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 '节点类型描述',
category VARCHAR(50) NOT NULL COMMENT '节点类型分类',
icon VARCHAR(100) NULL COMMENT '节点图标',
color VARCHAR(20) NULL COMMENT '节点颜色',
executors TEXT NULL COMMENT '执行器列表(JSON)',
config_schema TEXT NULL COMMENT '节点配置模式(JSON)',
default_config TEXT NULL COMMENT '默认配置(JSON)',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
CONSTRAINT UK_node_type_code UNIQUE (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点类型表';
## 2. 改造目标
### 2.1 核心需求
1. 支持审批节点
2. 实现实时日志
3. 工作流状态持久化
4. 支持程序重启后恢复执行
### 2.2 技术要求
1. 遵循现有代码规范
2. 保持向后兼容
3. 确保高可用性
4. 支持并发执行
## 3. 详细设计
### 3.1 数据库改造
#### 3.1.1 修改现有表
```sql
-- 修改工作流实例表,添加执行状态字段
ALTER TABLE wf_workflow_instance
ADD COLUMN current_node_id varchar(100) COMMENT '当前执行的节点ID',
ADD COLUMN execution_status varchar(32) COMMENT '执行状态';
-- 修改节点实例表,添加重试相关字段
ALTER TABLE wf_node_instance
ADD COLUMN retry_count INT DEFAULT 0 COMMENT '重试次数',
ADD COLUMN max_retry_count INT DEFAULT 0 COMMENT '最大重试次数',
ADD COLUMN retry_interval INT DEFAULT 0 COMMENT '重试间隔(秒)',
ADD COLUMN last_retry_time TIMESTAMP COMMENT '最后重试时间';
```
#### 3.1.2 新增审批相关表
```sql
-- 审批配置表
CREATE TABLE wf_approval_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 '乐观锁版本号',
project_id VARCHAR(64) NOT NULL COMMENT '项目ID',
env_type VARCHAR(20) NOT NULL COMMENT '环境类型',
approval_type VARCHAR(20) NOT NULL COMMENT '审批类型SINGLE/MULTI/ANY',
approver_ids TEXT NOT NULL COMMENT '审批人ID列表(JSON)',
required_count INT NOT NULL COMMENT '需要的审批人数',
timeout_hours INT NOT NULL COMMENT '审批超时时间(小时)',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用',
CONSTRAINT UK_approval_config_project_env UNIQUE (project_id, env_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批配置表';
-- 审批实例表
CREATE TABLE wf_approval_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 '乐观锁版本号',
node_instance_id VARCHAR(64) NOT NULL COMMENT '节点实例ID',
workflow_instance_id VARCHAR(64) NOT NULL COMMENT '工作流实例ID',
project_id VARCHAR(64) NOT NULL COMMENT '项目ID',
env_type VARCHAR(20) NOT NULL COMMENT '环境类型',
status VARCHAR(20) NOT NULL COMMENT '状态PENDING/APPROVED/REJECTED/TIMEOUT',
initiator_id VARCHAR(64) NOT NULL COMMENT '发起人ID',
approved_time DATETIME(6) NULL COMMENT '审批通过时间',
approver_id VARCHAR(64) NULL COMMENT '审批人ID',
comment TEXT NULL COMMENT '审批意见',
INDEX idx_workflow_instance (workflow_instance_id),
INDEX idx_node_instance (node_instance_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批实例表';
-- 审批记录表
CREATE TABLE wf_approval_record (
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 '乐观锁版本号',
approval_instance_id BIGINT NOT NULL COMMENT '审批实例ID',
approver_id VARCHAR(64) NOT NULL COMMENT '审批人ID',
action VARCHAR(20) NOT NULL COMMENT '审批动作APPROVE/REJECT',
comment TEXT NULL COMMENT '审批意见',
CONSTRAINT FK_approval_record_instance FOREIGN KEY (approval_instance_id)
REFERENCES wf_approval_instance (id),
INDEX idx_approval_instance (approval_instance_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表';
### 3.2 代码改造
#### 3.2.1 新增枚举
```java
/**
* 节点类型枚举扩展
*/
public enum NodeTypeEnum {
SCRIPT, // 脚本节点
HTTP, // HTTP请求节点
APPROVAL, // 审批节点
// ... 其他节点类型
}
/**
* 审批类型枚举
*/
public enum ApprovalTypeEnum {
SINGLE, // 单人审批
MULTI, // 多人会签
ANY // 任意人审批
}
/**
* 审批状态枚举
*/
public enum ApprovalStatusEnum {
PENDING, // 待审批
APPROVED, // 已通过
REJECTED, // 已拒绝
TIMEOUT // 已超时
}
```
#### 3.2.2 实体类
```java
/**
* 审批配置实体
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "wf_approval_config")
@LogicDelete
public class ApprovalConfig extends BaseEntity<Long> {
@Column(nullable = false)
private String projectId;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private String envType;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private ApprovalTypeEnum approvalType;
@Column(nullable = false)
private String approverIds;
@Column(nullable = false)
private Integer requiredCount;
@Column(nullable = false)
private Integer timeoutHours;
}
/**
* 审批实例实体
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "wf_approval_instance")
@LogicDelete
public class ApprovalInstance extends BaseEntity<Long> {
@Column(nullable = false)
private String nodeInstanceId;
@Column(nullable = false)
private String workflowInstanceId;
@Column(nullable = false)
private String projectId;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private String envType;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private ApprovalStatusEnum status;
@Column(nullable = false)
private String initiatorId;
private LocalDateTime approvedTime;
private String approverId;
private String comment;
}
```
#### 3.2.3 Repository接口
```java
/**
* 审批配置Repository
*/
@Repository
public interface IApprovalConfigRepository extends IBaseRepository<ApprovalConfig, Long> {
ApprovalConfig findByProjectIdAndEnvType(String projectId, String envType);
}
/**
* 审批实例Repository
*/
@Repository
public interface IApprovalInstanceRepository extends IBaseRepository<ApprovalInstance, Long> {
List<ApprovalInstance> findByStatus(ApprovalStatusEnum status);
ApprovalInstance findByWorkflowInstanceIdAndNodeInstanceId(String workflowInstanceId, String nodeInstanceId);
}
```
#### 3.2.4 Service接口
```java
/**
* 审批服务接口
*/
public interface IApprovalService {
/**
* 创建审批实例
*/
ApprovalInstance createApprovalInstance(String nodeInstanceId, String workflowInstanceId,
String projectId, ApprovalConfig config);
/**
* 处理审批结果
*/
void handleApproval(Long approvalInstanceId, String approverId,
boolean approved, String comment);
/**
* 检查审批状态
*/
ApprovalStatusEnum checkApprovalStatus(Long approvalInstanceId);
/**
* 发送审批通知
*/
void sendApprovalNotifications(ApprovalInstance instance);
}
```
#### 3.2.5 工作流执行管理器
```java
/**
* 工作流执行管理器
*/
@Component
@Slf4j
public class WorkflowExecutionManager {
@Resource
private ThreadPoolExecutor workflowExecutor;
@Resource
private WorkflowInstanceRepository instanceRepository;
@PostConstruct
public void init() {
// 初始化线程池
workflowExecutor = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("workflow-executor-%d").build()
);
// 恢复运行中的工作流
resumeRunningWorkflows();
}
// ... 其他方法实现
}
```
#### 3.2.6 审批节点执行器
```java
/**
* 审批节点执行器
*/
@Component
@Slf4j
public class ApprovalNodeExecutor extends AbstractNodeExecutor {
@Resource
private IApprovalService approvalService;
@Override
public NodeTypeEnum getNodeType() {
return NodeTypeEnum.APPROVAL;
}
@Override
protected void doExecute(WorkflowInstance workflowInstance, NodeInstance nodeInstance) {
// ... 实现审批节点执行逻辑
}
}
```
### 3.3 实时日志实现
#### 3.3.1 日志收集器
```java
/**
* 日志收集器
*/
@Component
@Slf4j
public class ProcessLogManager {
@Resource
private WorkflowLogService workflowLogService;
private final Map<String, CircularBuffer> logBuffers = new ConcurrentHashMap<>();
private final Map<String, List<Consumer<String>>> subscribers = new ConcurrentHashMap<>();
// ... 实现日志收集和推送逻辑
}
```
#### 3.3.2 SSE控制器
```java
/**
* 日志SSE控制器
*/
@RestController
@RequestMapping("/api/v1/logs")
@Tag(name = "工作流日志", description = "工作流日志相关接口")
public class LogController {
@GetMapping(value = "/{nodeInstanceId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamLogs(@PathVariable String nodeInstanceId) {
// ... 实现SSE日志推送
}
}
```
## 4. 开发计划
### 4.1 核心功能改造
#### 4.1.1 工作流引擎核心
1. **状态管理优化**
- 实现工作流实例状态机,支持更细粒度的状态转换
- 增加节点实例的状态追踪和历史记录
- 添加工作流暂停/恢复机制
2. **并发控制增强**
- 实现基于数据库的分布式锁
- 添加节点级别的并发控制
- 优化多实例并行执行的性能
3. **错误处理机制**
- 完善节点执行失败的重试机制
- 实现节点回滚功能
- 添加全局异常处理和日志记录
- 支持自定义错误处理策略
#### 4.1.2 审批功能实现
1. **审批节点开发**
- 实现审批节点执行器
- 支持多种审批模式(单人、多人、任意人)
- 添加审批超时处理机制
- 实现审批回调通知
2. **审批流程管理**
- 开发审批配置管理接口
- 实现审批人动态配置
- 支持审批规则的版本控制
- 添加审批历史查询功能
3. **审批UI组件**
- 开发审批任务列表
- 实现审批详情页面
- 添加审批操作接口
- 支持审批意见和附件上传
### 4.2 新增功能
#### 4.2.1 工作流监控
1. **实时监控**
- 实现工作流执行状态实时展示
- 添加节点执行时间统计
- 支持执行瓶颈分析
- 实现资源使用监控
2. **报警机制**
- 添加执行超时告警
- 实现错误率监控告警
- 支持自定义告警规则
- 集成多种告警通道邮件、短信、webhook等
#### 4.2.2 工作流分析
1. **统计分析**
- 实现工作流执行趋势分析
- 添加节点执行效率分析
- 支持审批效率统计
- 开发自定义报表功能
2. **可视化展示**
- 实现工作流执行路径可视化
- 添加状态转换图表展示
- 支持审批流程可视化
- 开发数据大屏功能
### 4.3 性能优化
1. **数据库优化**
- 优化表结构和索引
- 实现分库分表方案
- 添加缓存机制
- 优化大数据量查询性能
2. **执行引擎优化**
- 优化线程池管理
- 实现节点执行预热
- 添加资源限流机制
- 优化内存使用
### 4.4 开发排期
#### Phase 1: 核心功能改造4周
- Week 1-2: 工作流引擎核心改造
- Week 3-4: 审批功能实现
#### Phase 2: 新增功能开发3周
- Week 1-2: 工作流监控功能
- Week 3: 工作流分析功能
#### Phase 3: 性能优化2周
- Week 1: 数据库优化
- Week 2: 执行引擎优化
#### Phase 4: 测试与上线1周
- Day 1-3: 功能测试与bug修复
- Day 4-5: 性能测试与优化
- Day 6-7: 准备上线与部署
### 4.5 风险评估
1. **技术风险**
- 分布式环境下的状态一致性
- 大规模并发下的性能表现
- 复杂工作流的稳定性保证
2. **项目风险**
- 开发周期可能超出预期
- 新旧版本兼容性问题
- 现有业务的平滑迁移
### 4.6 后续规划
1. **功能扩展**
- 支持更多类型的节点
- 添加工作流模板功能
- 实现工作流版本管理
- 支持跨系统工作流
2. **生态建设**
- 开发SDK工具包
- 提供更多开箱即用的组件
- 完善开发文档
- 建设示例中心
## 5. 注意事项
1. 数据迁移
- 需要处理已有的运行中工作流
- 确保数据一致性
2. 性能考虑
- 控制线程池大小
- 合理设置日志缓冲区
- 定期清理历史数据
3. 监控告警
- 添加工作流执行监控
- 配置审批超时告警
- 监控系统资源使用
## 6. 后续优化
1. 功能优化
- 支持更多审批策略
- 优化日志查询性能
- 添加工作流统计分析
2. 性能优化
- 引入分布式锁
- 优化数据库查询
- 添加缓存机制
3. 运维优化
- 完善监控指标
- 优化日志清理
- 添加性能诊断工具

View File

@ -1,380 +0,0 @@
# 工作流实例同步机制设计文档
## 目录
1. [整体数据流向](#整体数据流向)
2. [Flowable与系统映射关系](#Flowable与系统映射关系)
3. [数据同步实现](#数据同步实现)
4. [事件监听机制](#事件监听机制)
5. [状态管理](#状态管理)
6. [最佳实践](#最佳实践)
## 整体数据流向
```
[前端流程设计器]
|
| 1. 用户拖拽设计流程
| - 选择节点类型从workflow_node_definition获取可用节点
| - 配置节点属性根据节点的form_config渲染表单
| - 连接节点、设置条件
[前端数据模型]
| - 图形数据nodes、edges
| - 表单数据(各节点的配置信息)
| - 流程基本信息(名称、描述等)
|
| 2. 保存流程设计
[后端 WorkflowDefinitionController]
|
| 3. 接收流程设计数据
[后端 WorkflowDefinitionService]
|
| 4. 处理流程数据
| - 保存流程定义基本信息到 workflow_definition
| - 转换图形数据为BPMN XML
| - 解析节点配置更新workflow_node_definition
| * 对于新的节点类型:创建新记录
| * 对于已有节点:更新配置
|
| 5. 部署到Flowable
[Flowable引擎]
|
| 6. 部署流程
| - 保存BPMN XML
| - 生成流程定义ID
|
| 7. 启动流程实例
[后端 WorkflowInstanceService]
|
| 8. 创建流程实例
| - 创建workflow_instance记录
| - 关联workflow_definition
|
| 9. 节点实例化
| - 创建workflow_node_instance记录
| - 关联workflow_node_definition
[数据库]
|
| 实时同步的表:
| - workflow_definition流程定义
| - workflow_node_definition节点定义
| - workflow_instance流程实例
| - workflow_node_instance节点实例
|
| Flowable表
| - ACT_RE_*(流程定义相关表)
| - ACT_RU_*(运行时数据表)
| - ACT_HI_*(历史数据表)
[前端任务列表/监控页面]
|
| 10. 展示流程实例
| - 查询实例状态
| - 显示节点执行情况
| - 处理用户任务
| - 查看历史记录
```
## Flowable与系统映射关系
### 表结构映射
#### Flowable核心表
```
[流程定义相关]
ACT_RE_DEPLOYMENT: 流程部署表
ACT_RE_PROCDEF: 流程定义表
[流程实例相关]
ACT_RU_EXECUTION: 运行时流程实例表
ACT_RU_TASK: 运行时任务表
ACT_RU_VARIABLE: 运行时变量表
[历史数据]
ACT_HI_PROCINST: 历史流程实例表
ACT_HI_TASKINST: 历史任务实例表
ACT_HI_ACTINST: 历史活动实例表
```
#### 系统表与Flowable映射关系
```
我们的表 Flowable的表
--------------------------------------------------
workflow_definition <-> ACT_RE_PROCDEF
workflow_instance <-> ACT_RU_EXECUTION/ACT_HI_PROCINST
workflow_node_instance <-> ACT_RU_TASK/ACT_HI_TASKINST
```
## 数据同步实现
### 流程实例服务实现
```java
@Service
@Transactional
public class WorkflowInstanceService {
@Resource
private RuntimeService runtimeService; // Flowable的运行时服务
@Resource
private TaskService taskService; // Flowable的任务服务
public WorkflowInstance startProcess(WorkflowInstanceCreateDTO createDTO) {
// 1. 启动Flowable流程实例
ProcessInstance processInstance = runtimeService.startProcessInstanceById(
createDTO.getProcessDefinitionId(),
createDTO.getBusinessKey(),
createDTO.getVariables()
);
// 2. 创建我们自己的流程实例记录
WorkflowInstance workflowInstance = new WorkflowInstance();
workflowInstance.setProcessInstanceId(processInstance.getId());
workflowInstance.setProcessDefinitionId(createDTO.getProcessDefinitionId());
workflowInstance.setBusinessKey(createDTO.getBusinessKey());
workflowInstance.setStatus(WorkflowInstanceStatusEnums.RUNNING);
workflowInstance.setVariables(JsonUtils.toJsonString(createDTO.getVariables()));
workflowInstance.setStartTime(LocalDateTime.now());
workflowInstanceRepository.save(workflowInstance);
// 3. 获取当前活动的任务
List<Task> tasks = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.list();
// 4. 为每个活动的任务创建节点实例
for (Task task : tasks) {
WorkflowNodeInstance nodeInstance = new WorkflowNodeInstance();
nodeInstance.setWorkflowInstanceId(workflowInstance.getId());
nodeInstance.setNodeId(task.getTaskDefinitionKey());
nodeInstance.setNodeName(task.getName());
nodeInstance.setNodeType("userTask"); // 或者从定义中获取
nodeInstance.setStatus("ACTIVE");
nodeInstance.setStartTime(LocalDateTime.now());
workflowNodeInstanceRepository.save(nodeInstance);
}
return workflowInstance;
}
}
```
### 数据同步服务实现
```java
@Service
public class WorkflowSyncService {
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void syncWorkflowStatus() {
// 1. 查找所有运行中的实例
List<WorkflowInstance> runningInstances = workflowInstanceRepository
.findByStatus(WorkflowInstanceStatusEnums.RUNNING);
for (WorkflowInstance instance : runningInstances) {
// 2. 检查Flowable中的状态
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(instance.getProcessInstanceId())
.singleResult();
if (processInstance == null) {
// Flowable中实例已结束更新我们的状态
HistoricProcessInstance historicInstance = historyService
.createHistoricProcessInstanceQuery()
.processInstanceId(instance.getProcessInstanceId())
.singleResult();
instance.setStatus(convertFlowableStatus(historicInstance.getEndActivityId()));
instance.setEndTime(LocalDateTime.now());
workflowInstanceRepository.save(instance);
}
// 3. 同步节点实例状态
syncNodeInstances(instance.getId(), instance.getProcessInstanceId());
}
}
private void syncNodeInstances(Long workflowInstanceId, String processInstanceId) {
// 1. 获取当前活动的任务
List<Task> activeTasks = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.list();
// 2. 获取历史任务
List<HistoricTaskInstance> historicTasks = historyService
.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.finished()
.list();
// 3. 更新节点实例状态
Set<String> processedTaskIds = new HashSet<>();
// 处理活动任务
for (Task task : activeTasks) {
WorkflowNodeInstance nodeInstance = getOrCreateNodeInstance(
workflowInstanceId, task.getTaskDefinitionKey());
updateNodeInstance(nodeInstance, task, null);
processedTaskIds.add(task.getTaskDefinitionKey());
}
// 处理历史任务
for (HistoricTaskInstance historicTask : historicTasks) {
if (!processedTaskIds.contains(historicTask.getTaskDefinitionKey())) {
WorkflowNodeInstance nodeInstance = getOrCreateNodeInstance(
workflowInstanceId, historicTask.getTaskDefinitionKey());
updateNodeInstance(nodeInstance, null, historicTask);
}
}
}
}
```
## 事件监听机制
### Flowable事件监听器
```java
@Component
public class FlowableEventListener implements TaskListener, ExecutionListener {
@Resource
private WorkflowInstanceService workflowInstanceService;
@Override
public void notify(DelegateTask task) {
// 任务事件处理
String eventName = task.getEventName();
switch (eventName) {
case "create":
workflowInstanceService.onTaskCreate(task);
break;
case "complete":
workflowInstanceService.onTaskComplete(task);
break;
case "delete":
workflowInstanceService.onTaskDelete(task);
break;
}
}
@Override
public void notify(DelegateExecution execution) {
// 流程执行事件处理
String eventName = execution.getEventName();
switch (eventName) {
case "start":
workflowInstanceService.onProcessStart(execution);
break;
case "end":
workflowInstanceService.onProcessEnd(execution);
break;
}
}
}
```
## 状态管理
### 工作流实例状态枚举
```java
public enum WorkflowInstanceStatusEnums {
NOT_STARTED, // 未开始
RUNNING, // 运行中
SUSPENDED, // 已暂停
COMPLETED, // 已完成
TERMINATED, // 已终止
FAILED; // 执行失败
public static WorkflowInstanceStatusEnums fromFlowableStatus(String flowableStatus) {
switch (flowableStatus) {
case "active":
return RUNNING;
case "suspended":
return SUSPENDED;
case "completed":
return COMPLETED;
case "terminated":
return TERMINATED;
default:
return FAILED;
}
}
}
```
## 最佳实践
### 为什么需要自己维护实例表
1. **业务扩展性**
- 可以添加更多业务相关的字段
- 可以实现自定义的状态管理
- 可以关联更多业务数据
2. **性能优化**
- 避免频繁查询Flowable表
- 可以建立更适合业务查询的索引
- 可以实现更好的缓存策略
3. **数据完整性**
- 保存完整的业务上下文
- 记录更详细的审计信息
- 支持自定义的数据分析
### 数据一致性保证
1. **事务管理**
```java
@Transactional
public void startProcess() {
// 1. 启动Flowable流程
// 2. 创建系统流程实例
// 3. 创建节点实例
}
```
2. **定时同步**
- 定期检查运行中的实例状态
- 自动修复不一致的数据
- 记录同步日志
3. **事件驱动**
- 监听Flowable事件
- 实时更新系统状态
- 保证数据实时性
### 性能优化建议
1. **索引优化**
- 为常用查询字段建立索引
- 使用复合索引优化多字段查询
- 避免过多索引影响写入性能
2. **缓存策略**
- 缓存活动的流程实例
- 缓存常用的节点定义
- 使用分布式缓存提高性能
3. **批量处理**
- 批量同步数据
- 批量更新状态
- 使用队列处理异步任务
## 总结
1. Flowable负责流程的实际执行和调度
2. 系统维护业务层面的状态和数据
3. 通过事件监听和定时同步保证数据一致性
4. 使用状态映射处理不同系统间的状态转换
5. 通过事务确保关键操作的原子性
这样的设计可以:
- 保持与Flowable的数据同步
- 支持业务扩展
- 提供更好的性能
- 确保数据一致性
- 便于问题追踪和修复

View File

@ -1,322 +0,0 @@
# 工作流动态表单设计方案
## 1. 整体架构
### 1.1 数据流向
```
1. 流程设计阶段
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 表单设计器 │ │ 流程设计器 │ │ 后端存储 │
│ Form Designer │ ──> │ Flow Designer │ ──> │ Database │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ 设计表单 │ 配置节点表单 │ 保存定义
│ JSON Schema │ 配置数据映射 │ - 表单定义
│ UI Schema │ 配置权限 │ - 流程定义
▼ ▼ ▼
2. 流程运行阶段
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 流程发起 │ │ 节点激活 │ │ 表单实例化 │
│ Start Flow │ ──> │ Node Activated │ ──> │ Form Instance │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ 触发监听器 │ 创建表单实例
│ │ 初始化数据
▼ ▼
3. 表单处理阶段
┌─────────────<EFBFBD><EFBFBD>───┐ ┌─────────────────┐ ┌─────────────────┐
│ 表单渲染 │ │ 表单交互 │ │ 数据处理 │
│ Form Render │ ──> │ Form Interact │ ──> │ Data Process │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ 加载表单定义 │ 用户输入 │ 数据验证
│ 加载实例数据 │ 数据提交 │ 数据转换
│ 应用权限 │ │ 变量映射
▼ ▼ ▼
4. 节点完成阶段
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 表单提交 │ │ 节点完成 │ │ 流程推进 │
│ Form Submit │ ──> │ Node Complete │ ──> │ Flow Progress │
└─────────────────┘ └─────────────<E29480><E29480><EFBFBD>───┘ └─────────────────┘
│ │
│ 触发监听器 │ 流转到下一节点
│ 更新流程变量 │
▼ ▼
```
### 1.2 数据模型设计
1. **表单定义(FormDefinition)**
```java
@Data
@Table(name = "workflow_form_definition")
@Entity
@LogicDelete
public class FormDefinition extends Entity<Long> {
@Column(nullable = false)
private String name; // 表单名称
@Column(nullable = false, unique = true)
private String code; // 表单编码
@Column(columnDefinition = "TEXT")
private String description; // 表单描述
@Type(JsonType.class)
@Column(columnDefinition = "json", nullable = false)
private JsonNode schema; // 表单JSON Schema
@Type(JsonType.class)
@Column(columnDefinition = "json")
private JsonNode uiSchema; // UI渲染Schema
@Column(nullable = false)
private Boolean enabled = true; // 是否启用
@Column(nullable = false)
private Integer version = 1; // 版本号
}
```
2. **表单实例(FormInstance)**
```java
@Data
@Table(name = "workflow_form_instance")
@Entity
@LogicDelete
public class FormInstance extends Entity<Long> {
@Column(nullable = false)
private Long formDefinitionId; // 表单定义ID
@Column(nullable = false)
private String processInstanceId; // 流程实例ID
@Column(nullable = false)
private String taskId; // 任务ID
@Type(JsonType.class)
@Column(columnDefinition = "json", nullable = false)
private JsonNode formData; // 表单数据
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private FormInstanceStatus status; // 状态
}
```
## 2. 节点表单集成
### 2.1 工作流上下文设计
```java
@Data
public class WorkflowContext {
// 系统上下文
private SystemContext systemContext;
// 表单上下文
private FormContext formContext;
// 流程变量上下文
private ProcessContext processContext;
@Data
public static class SystemContext {
private JsonNode systemConfig; // 系统配置
private String executionId; // 执行ID
private String processInstanceId; // 流程实例ID
private String nodeId; // 节点ID
}
@Data
public static class FormContext {
private Long formInstanceId; // 表单实例ID
private JsonNode formData; // 表单数据
private JsonNode formConfig; // 表单配置
private Map<String, Object> mappedData; // 映射后的数据
}
@Data
public static class ProcessContext {
private Map<String, Object> variables; // 流程变量
private Map<String, Object> localVariables; // 节点本地变量
}
}
```
### 2.2 节点定义中的表单配置
```java
@Data
@Table(name = "workflow_node_definition")
@Entity
@LogicDelete
public class WorkflowNodeDefinition extends Entity<Long> {
// ... 现有字段 ...
/**
* 表单配置
*/
@Type(JsonType.class)
@Column(columnDefinition = "json")
private JsonNode formConfig; // 节点默认的表单配置
/**
* 表单Schema
*/
@Type(JsonType.class)
@Column(columnDefinition = "json")
private JsonNode formSchema; // 节点默认的表单结构
/**
* 表单UI Schema
*/
@Type(JsonType.class)
@Column(columnDefinition = "json")
private JsonNode formUiSchema; // 节点默认的UI渲染配置
}
```
## 3. 动态接口支持
### 3.1 表单配置中的接口定义
```json
{
"formSchema": {
"type": "object",
"properties": {
"department": {
"type": "string",
"title": "部门",
"enum": "@api:getDepartments",
"enumNames": "@api:getDepartmentNames"
},
"employee": {
"type": "string",
"title": "员工",
"enum": "@api:getEmployeesByDepartment(department)",
"enumNames": "@api:getEmployeeNamesByDepartment(department)"
}
}
}
}
```
### 3.2 动态接口注册
```java
@Service
public class DynamicApiRegistry {
private final Map<String, DynamicApi> apiRegistry = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 注册系统内置API
registerSystemApis();
}
// 注册API
public void register(String apiKey, DynamicApi api) {
apiRegistry.put(apiKey, api);
}
// 获取API
public DynamicApi getApi(String apiKey) {
return apiRegistry.get(apiKey);
}
}
```
### 3.3 动态API定义
```java
@Data
@Builder
public class DynamicApi {
private String url; // API地址
private HttpMethod method; // 请求方法
private Function<Map<String, Object>, Map<String, Object>> paramsBuilder; // 参数构建器
private String transform; // 数据转换路径
private Map<String, String> mapping; // 数据映射
private List<String> dependencies; // 依赖字段
}
```
## 4. 使用示例
### 4.1 定义节点表单
```json
{
"systemConfig": {
"timeout": 3600,
"retryTimes": 3,
"notifyEmail": "admin@example.com"
},
"formConfig": {
"formSchema": { /* 表单结构 */ },
"formUiSchema": { /* UI渲染配置 */ },
"permissions": { /* 权限配置 */ },
"dataMapping": {
"input": { /* 输入映射 */ },
"output": { /* 输出映射 */ }
}
}
}
```
### 4.2 注册动态接口
```java
@Component
public class CustomApiConfig {
@Resource
private DynamicApiRegistry apiRegistry;
@PostConstruct
public void init() {
// 注册项目查询API
apiRegistry.register("getProjects", new DynamicApi.builder()
.url("/api/v1/projects")
.method(HttpMethod.GET)
.paramsBuilder(context -> Map.of(
"departmentId", context.get("department"),
"employeeId", context.get("employee")
))
.transform("data.list")
.mapping(Map.of("value", "id", "label", "name"))
.dependencies(List.of("department", "employee"))
.build()
);
}
}
```
### 4.3 实现委派类
```java
@Component
@Slf4j
public class ApprovalTaskDelegate extends BaseWorkflowDelegate {
@Override
protected void doExecute(WorkflowContext context) throws Exception {
// 1. 获取表单数据
FormContext formContext = context.getFormContext();
Map<String, Object> mappedData = formContext.getMappedData();
String approvalResult = (String) mappedData.get("approvalResult");
String comments = (String) mappedData.get("comments");
// 2. 获取系统配置
SystemContext systemContext = context.getSystemContext();
JsonNode systemConfig = systemContext.getSystemConfig();
String notifyEmail = systemConfig.get("notifyEmail").asText();
// 3. 处理审批结果
ProcessContext processContext = context.getProcessContext();
processContext.getVariables().put("approved", "APPROVED".equals(approvalResult));
processContext.getVariables().put("approvalComments", comments);
// 4. 发送通知
if (notifyEmail != null) {
notificationService.sendApprovalNotification(notifyEmail, approvalResult, comments);
}
}
}
```

View File

@ -1,94 +0,0 @@
package com.qqchen.deploy.backend.deploy.api;
import com.qqchen.deploy.backend.deploy.dto.TeamConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamConfig;
import com.qqchen.deploy.backend.deploy.query.TeamConfigQuery;
import com.qqchen.deploy.backend.deploy.service.ITeamConfigService;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 团队配置API控制器
*
* @author qqchen
* @since 2025-10-31
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/team-configs")
@Tag(name = "团队配置管理", description = "团队配置的增删改查接口")
public class TeamConfigApiController extends BaseController<TeamConfig, TeamConfigDTO, Long, TeamConfigQuery> {
@Resource
private ITeamConfigService teamConfigService;
@Override
public Response<TeamConfigDTO> create(@Validated @RequestBody TeamConfigDTO dto) {
return super.create(dto);
}
@Override
public Response<TeamConfigDTO> update(@PathVariable Long id, @Validated @RequestBody TeamConfigDTO dto) {
return super.update(id, dto);
}
@Override
public Response<Void> delete(@PathVariable Long id) {
return super.delete(id);
}
@Override
public Response<TeamConfigDTO> findById(@PathVariable Long id) {
return super.findById(id);
}
@Override
public Response<List<TeamConfigDTO>> findAll() {
return super.findAll();
}
@Override
public Response<Page<TeamConfigDTO>> page(TeamConfigQuery query) {
return super.page(query);
}
@Override
public Response<List<TeamConfigDTO>> findAll(TeamConfigQuery query) {
return super.findAll(query);
}
@Override
public CompletableFuture<Response<Void>> batchProcess(List<TeamConfigDTO> dtos) {
return super.batchProcess(dtos);
}
@Operation(summary = "根据团队ID获取配置", description = "获取指定团队的配置信息")
@GetMapping("/team/{teamId}")
public Response<TeamConfigDTO> getByTeamId(
@Parameter(description = "团队ID", required = true) @PathVariable Long teamId
) {
return Response.success(teamConfigService.getByTeamId(teamId));
}
@Override
protected void exportData(HttpServletResponse response, List<TeamConfigDTO> data) {
// TODO: 实现导出功能
}
}

View File

@ -0,0 +1,66 @@
package com.qqchen.deploy.backend.deploy.api;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.deploy.query.TeamEnvironmentConfigQuery;
import com.qqchen.deploy.backend.deploy.service.ITeamEnvironmentConfigService;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 团队环境配置Controller
*
* @author qqchen
* @since 2025-11-04
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/team-environment-config")
@Tag(name = "团队环境配置管理", description = "团队环境配置相关接口")
public class TeamEnvironmentConfigApiController extends BaseController<TeamEnvironmentConfig, TeamEnvironmentConfigDTO, Long, TeamEnvironmentConfigQuery> {
private final ITeamEnvironmentConfigService teamEnvironmentConfigService;
public TeamEnvironmentConfigApiController(ITeamEnvironmentConfigService service) {
this.teamEnvironmentConfigService = service;
}
@Operation(summary = "根据团队ID查询环境配置列表")
@GetMapping("/team/{teamId}")
public Response<List<TeamEnvironmentConfigDTO>> getByTeamId(
@Parameter(description = "团队ID", required = true) @PathVariable Long teamId
) {
return Response.success(teamEnvironmentConfigService.findByTeamId(teamId));
}
@Operation(summary = "根据团队ID和环境ID查询配置")
@GetMapping("/team/{teamId}/environment/{environmentId}")
public Response<TeamEnvironmentConfigDTO> getByTeamIdAndEnvironmentId(
@Parameter(description = "团队ID", required = true) @PathVariable Long teamId,
@Parameter(description = "环境ID", required = true) @PathVariable Long environmentId
) {
return Response.success(teamEnvironmentConfigService.findByTeamIdAndEnvironmentId(teamId, environmentId));
}
@Operation(summary = "检查配置是否存在")
@GetMapping("/exists")
public Response<Boolean> exists(
@Parameter(description = "团队ID", required = true) @RequestParam Long teamId,
@Parameter(description = "环境ID", required = true) @RequestParam Long environmentId
) {
return Response.success(teamEnvironmentConfigService.exists(teamId, environmentId));
}
@Override
protected void exportData(jakarta.servlet.http.HttpServletResponse response, List<TeamEnvironmentConfigDTO> data) {
// TODO: 实现导出功能
}
}

View File

@ -1,17 +0,0 @@
package com.qqchen.deploy.backend.deploy.converter;
import com.qqchen.deploy.backend.deploy.dto.TeamConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamConfig;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import org.mapstruct.Mapper;
/**
* 团队配置转换器
*
* @author qqchen
* @since 2025-10-31
*/
@Mapper(config = BaseConverter.class)
public interface TeamConfigConverter extends BaseConverter<TeamConfig, TeamConfigDTO> {
}

View File

@ -0,0 +1,17 @@
package com.qqchen.deploy.backend.deploy.converter;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.framework.converter.BaseConverter;
import org.mapstruct.Mapper;
/**
* 团队环境配置转换器
*
* @author qqchen
* @since 2025-11-04
*/
@Mapper(config = BaseConverter.class)
public interface TeamEnvironmentConfigConverter extends BaseConverter<TeamEnvironmentConfig, TeamEnvironmentConfigDTO> {
}

View File

@ -39,6 +39,12 @@ public class DeployableEnvironmentDTO {
@Schema(description = "审批人列表") @Schema(description = "审批人列表")
private List<ApproverDTO> approvers; private List<ApproverDTO> approvers;
@Schema(description = "是否要求代码审查")
private Boolean requireCodeReview;
@Schema(description = "备注信息")
private String remark;
@Schema(description = "可部署应用列表") @Schema(description = "可部署应用列表")
private List<DeployableApplicationDTO> applications; private List<DeployableApplicationDTO> applications;
} }

View File

@ -1,35 +0,0 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 团队配置DTO
*
* @author qqchen
* @since 2025-10-31
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "团队配置信息")
public class TeamConfigDTO extends BaseDTO {
@Schema(description = "团队ID", required = true)
@NotNull(message = "团队ID不能为空")
private Long teamId;
@Schema(description = "允许访问的环境ID列表", example = "[1, 2, 3]")
private List<Long> allowedEnvironmentIds;
@Schema(description = "环境是否需要审批与allowedEnvironmentIds位置对应", example = "[false, false, true]")
private List<Boolean> environmentApprovalRequired;
@Schema(description = "各环境的审批人列表与allowedEnvironmentIds位置对应无审批人用null", example = "[null, null, [1, 4]]")
private List<Object> approverUserIds;
}

View File

@ -0,0 +1,76 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 团队环境配置DTO
*
* @author qqchen
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamEnvironmentConfigDTO extends BaseDTO {
/**
* 团队ID
*/
private Long teamId;
/**
* 环境ID
*/
private Long environmentId;
/**
* 是否需要审批
*/
private Boolean approvalRequired;
/**
* 审批人用户ID列表
*/
private List<Long> approverUserIds;
/**
* 通知渠道ID
*/
private Long notificationChannelId;
/**
* 是否启用部署通知
*/
private Boolean notificationEnabled;
/**
* 是否要求代码审查通过
*/
private Boolean requireCodeReview;
/**
* 备注信息
*/
private String remark;
// ===== 扩展字段非数据库字段 =====
/**
* 环境名称扩展字段
*/
private String environmentName;
/**
* 通知渠道名称扩展字段
*/
private String notificationChannelName;
/**
* 该团队在该环境下关联的应用数量扩展字段
*/
private Long applicationCount;
}

View File

@ -1,59 +0,0 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.vladmihalcea.hibernate.type.json.JsonType;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import java.util.List;
/**
* 团队配置实体
*
* @author qqchen
* @since 2025-10-31
*/
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "deploy_team_config")
@LogicDelete
public class TeamConfig extends Entity<Long> {
/**
* 团队ID
*/
@Column(name = "team_id", nullable = false)
private Long teamId;
/**
* 允许访问的环境ID列表
*/
@Type(JsonType.class)
@Column(name = "allowed_environment_ids", columnDefinition = "JSON")
private List<Long> allowedEnvironmentIds;
/**
* 环境是否需要审批boolean数组与allowedEnvironmentIds位置对应
* 例如allowedEnvironmentIds=[1,2,3], environmentApprovalRequired=[false,false,true]
* 表示环境1和2不需要审批环境3需要审批
*/
@Type(JsonType.class)
@Column(name = "environment_approval_required", columnDefinition = "JSON")
private List<Boolean> environmentApprovalRequired;
/**
* 各环境的审批人列表数组与allowedEnvironmentIds位置对应
* 元素可以是null无审批人或审批人ID数组
* 例如allowedEnvironmentIds=[1,2,3], approverUserIds=[null,null,[1,4]]
* 表示环境1和2无审批人环境3的审批人是用户1和4
*/
@Type(JsonType.class)
@Column(name = "approver_user_ids", columnDefinition = "JSON")
private List<Object> approverUserIds;
}

View File

@ -0,0 +1,104 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
import com.vladmihalcea.hibernate.type.json.JsonType;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import java.util.List;
/**
* 团队环境配置实体
*
* <p>定义团队对特定环境的配置
* <ul>
* <li>审批配置是否需要审批审批人列表</li>
* <li>通知配置通知渠道是否启用</li>
* <li>安全策略代码审查要求</li>
* </ul>
*
* <p>数据迁移说明
* 本表由 deploy_team_config 重构而来将原来的 JSON 数组结构展开为标准表结构
* <pre>
* 原结构1条记录 = 1个团队的多个环境配置JSON数组
* 新结构1条记录 = 1个团队在1个环境的配置
* </pre>
*
* @author qqchen
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "deploy_team_environment_config")
@LogicDelete(false)
public class TeamEnvironmentConfig extends Entity<Long> {
// ===== 关联关系 =====
/**
* 团队ID
*/
@Column(name = "team_id", nullable = false)
private Long teamId;
/**
* 环境ID
*/
@Column(name = "environment_id", nullable = false)
private Long environmentId;
// ===== 审批配置 =====
/**
* 是否需要审批
* <p>对应原 TeamConfig.environmentApprovalRequired[i]
*/
@Column(name = "approval_required", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
private Boolean approvalRequired = false;
/**
* 审批人用户ID列表
* <p>对应原 TeamConfig.approverUserIds[i]
* <p>示例[1, 4, 7]
*/
@Type(JsonType.class)
@Column(name = "approver_user_ids", columnDefinition = "JSON")
private List<Long> approverUserIds;
// ===== 通知配置 =====
/**
* 通知渠道ID
* <p>关联 sys_notification_channel
*/
@Column(name = "notification_channel_id")
private Long notificationChannelId;
/**
* 是否启用部署通知
*/
@Column(name = "notification_enabled", nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE")
private Boolean notificationEnabled = true;
// ===== 安全策略 =====
/**
* 是否要求代码审查通过才能部署
*/
@Column(name = "require_code_review", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
private Boolean requireCodeReview = false;
// ===== 备注 =====
/**
* 备注信息
*/
@Column(name = "remark", length = 500)
private String remark;
}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.deploy.query;
import com.qqchen.deploy.backend.framework.annotation.QueryField;
import com.qqchen.deploy.backend.framework.query.BaseQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 团队配置查询条件
*
* @author qqchen
* @since 2025-10-31
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "团队配置查询条件")
public class TeamConfigQuery extends BaseQuery {
@QueryField(field = "teamId")
@Schema(description = "团队ID")
private Long teamId;
}

View File

@ -0,0 +1,42 @@
package com.qqchen.deploy.backend.deploy.query;
import com.qqchen.deploy.backend.framework.annotation.QueryField;
import com.qqchen.deploy.backend.framework.query.BaseQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 团队环境配置查询条件
*
* @author qqchen
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeamEnvironmentConfigQuery extends BaseQuery {
/**
* 团队ID
*/
@QueryField(field = "teamId")
private Long teamId;
/**
* 环境ID
*/
@QueryField(field = "environmentId")
private Long environmentId;
/**
* 是否需要审批
*/
@QueryField(field = "approvalRequired")
private Boolean approvalRequired;
/**
* 是否启用通知
*/
@QueryField(field = "notificationEnabled")
private Boolean notificationEnabled;
}

View File

@ -110,5 +110,11 @@ public interface IDeployRecordRepository extends IBaseRepository<DeployRecord, L
List<DeployRecord> findRecentDeployRecordsByTeamApplicationIds( List<DeployRecord> findRecentDeployRecordsByTeamApplicationIds(
@Param("teamApplicationIds") List<Long> teamApplicationIds, @Param("teamApplicationIds") List<Long> teamApplicationIds,
@Param("limit") int limit); @Param("limit") int limit);
/**
* 根据团队应用ID查询所有部署记录包括已删除的
* 用于级联删除
*/
List<DeployRecord> findByTeamApplicationId(Long teamApplicationId);
} }

View File

@ -89,5 +89,24 @@ public interface ITeamApplicationRepository extends IBaseRepository<TeamApplicat
* @return 团队应用配置列表 * @return 团队应用配置列表
*/ */
List<TeamApplication> findByTeamIdIn(Collection<Long> teamIds); List<TeamApplication> findByTeamIdIn(Collection<Long> teamIds);
/**
* 统计指定团队在指定环境下的应用数量
*
* @param teamId 团队ID
* @param environmentId 环境ID
* @return 应用数量
*/
Long countByTeamIdAndEnvironmentId(Long teamId, Long environmentId);
/**
* 统计指定团队列表在指定环境下的不同应用数量去重 application_id
*
* @param teamIds 团队ID列表
* @param environmentId 环境ID
* @return 应用数量
*/
@Query("SELECT COUNT(DISTINCT ta.applicationId) FROM TeamApplication ta WHERE ta.teamId IN :teamIds AND ta.environmentId = :environmentId")
Long countDistinctApplicationIdByTeamIdsAndEnvironmentId(@Param("teamIds") Collection<Long> teamIds, @Param("environmentId") Long environmentId);
} }

View File

@ -1,44 +0,0 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.TeamConfig;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* 团队配置Repository
*
* @author qqchen
* @since 2025-10-31
*/
@Repository
public interface ITeamConfigRepository extends IBaseRepository<TeamConfig, Long> {
/**
* 根据团队ID查询配置
*
* @param teamId 团队ID
* @return 团队配置
*/
Optional<TeamConfig> findByTeamIdAndDeletedFalse(Long teamId);
/**
* 检查团队配置是否存在
*
* @param teamId 团队ID
* @return 是否存在
*/
boolean existsByTeamIdAndDeletedFalse(Long teamId);
/**
* 批量根据团队ID查询配置
*
* @param teamIds 团队ID集合
* @return 团队配置列表
*/
List<TeamConfig> findByTeamIdIn(Collection<Long> teamIds);
}

View File

@ -0,0 +1,49 @@
package com.qqchen.deploy.backend.deploy.repository;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 团队环境配置Repository
*
* @author qqchen
* @since 2025-11-04
*/
@Repository
public interface ITeamEnvironmentConfigRepository extends IBaseRepository<TeamEnvironmentConfig, Long> {
/**
* 根据团队ID查询所有环境配置
*/
List<TeamEnvironmentConfig> findByTeamId(Long teamId);
/**
* 根据团队ID列表批量查询
*/
List<TeamEnvironmentConfig> findByTeamIdIn(List<Long> teamIds);
/**
* 根据团队ID和环境ID查询
*/
Optional<TeamEnvironmentConfig> findByTeamIdAndEnvironmentId(Long teamId, Long environmentId);
/**
* 根据环境ID查询所有团队配置
*/
List<TeamEnvironmentConfig> findByEnvironmentId(Long environmentId);
/**
* 检查团队环境配置是否存在
*/
boolean existsByTeamIdAndEnvironmentId(Long teamId, Long environmentId);
/**
* 统计配置了指定环境的团队数量
*/
Long countByEnvironmentId(Long environmentId);
}

View File

@ -1,24 +0,0 @@
package com.qqchen.deploy.backend.deploy.service;
import com.qqchen.deploy.backend.deploy.dto.TeamConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamConfig;
import com.qqchen.deploy.backend.deploy.query.TeamConfigQuery;
import com.qqchen.deploy.backend.framework.service.IBaseService;
/**
* 团队配置Service接口
*
* @author qqchen
* @since 2025-10-31
*/
public interface ITeamConfigService extends IBaseService<TeamConfig, TeamConfigDTO, TeamConfigQuery, Long> {
/**
* 根据团队ID获取配置
*
* @param teamId 团队ID
* @return 团队配置
*/
TeamConfigDTO getByTeamId(Long teamId);
}

View File

@ -0,0 +1,33 @@
package com.qqchen.deploy.backend.deploy.service;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.deploy.query.TeamEnvironmentConfigQuery;
import com.qqchen.deploy.backend.framework.service.IBaseService;
import java.util.List;
/**
* 团队环境配置Service接口
*
* @author qqchen
* @since 2025-11-04
*/
public interface ITeamEnvironmentConfigService extends IBaseService<TeamEnvironmentConfig, TeamEnvironmentConfigDTO, TeamEnvironmentConfigQuery, Long> {
/**
* 根据团队ID查询所有环境配置
*/
List<TeamEnvironmentConfigDTO> findByTeamId(Long teamId);
/**
* 根据团队ID和环境ID查询配置
*/
TeamEnvironmentConfigDTO findByTeamIdAndEnvironmentId(Long teamId, Long environmentId);
/**
* 检查团队环境配置是否存在
*/
boolean exists(Long teamId, Long environmentId);
}

View File

@ -53,7 +53,7 @@ public class DeployServiceImpl implements IDeployService {
private ITeamRepository teamRepository; private ITeamRepository teamRepository;
@Resource @Resource
private ITeamConfigRepository teamConfigRepository; private ITeamEnvironmentConfigRepository teamEnvironmentConfigRepository;
@Resource @Resource
private IEnvironmentRepository environmentRepository; private IEnvironmentRepository environmentRepository;
@ -82,6 +82,24 @@ public class DeployServiceImpl implements IDeployService {
@Resource @Resource
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
/**
* 数据组装上下文封装所有需要的Map避免方法参数过多
*/
private static class BuildContext {
Map<Long, Team> teamMap;
Map<Long, TeamMember> teamMemberMap;
Map<String, TeamEnvironmentConfig> teamEnvConfigMap;
Map<Long, Environment> envMap;
Map<Long, List<TeamApplication>> teamAppsMap;
Map<Long, Application> appMap;
Map<Long, ExternalSystem> systemMap;
Map<Long, WorkflowDefinition> workflowMap;
Map<Long, User> approverMap;
Map<Long, DeployStatisticsDTO> statisticsMap;
Map<Long, DeployRecord> latestRecordMap;
Map<Long, List<DeployRecord>> recentRecordsMap;
}
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public UserDeployableDTO getDeployableEnvironments() { public UserDeployableDTO getDeployableEnvironments() {
@ -95,68 +113,119 @@ public class DeployServiceImpl implements IDeployService {
// 3. 查询用户作为负责人的团队 // 3. 查询用户作为负责人的团队
List<Team> ownedTeams = teamRepository.findByOwnerIdAndDeletedFalse(currentUserId); List<Team> ownedTeams = teamRepository.findByOwnerIdAndDeletedFalse(currentUserId);
// 4. 合并团队ID去重 // 4. 合并团队ID并构建团队Map优化直接复用ownedTeams数据
Set<Long> teamIdSet = new HashSet<>(); Set<Long> teamIdSet = new HashSet<>();
teamIdSet.addAll(teamMembers.stream().map(TeamMember::getTeamId).toList()); Map<Long, Team> teamMap = new HashMap<>();
teamIdSet.addAll(ownedTeams.stream().map(Team::getId).toList());
// 4.1 从负责人团队中提取并直接放入teamMap避免重复查询
for (Team team : ownedTeams) {
teamIdSet.add(team.getId());
teamMap.put(team.getId(), team);
}
// 4.2 从成员关系中提取teamId
Set<Long> memberTeamIds = teamMembers.stream()
.map(TeamMember::getTeamId)
.collect(Collectors.toSet());
teamIdSet.addAll(memberTeamIds);
if (teamIdSet.isEmpty()) { if (teamIdSet.isEmpty()) {
log.info("用户 {} 未加入任何团队且不是任何团队的负责人", user.getUsername()); log.info("用户 {} 未加入任何团队且不是任何团队的负责人", user.getUsername());
return buildEmptyResult(user); return buildEmptyResult(user);
} }
// 4.3 只查询成员团队中不在teamMap里的团队减少查询量
Set<Long> needQueryTeamIds = memberTeamIds.stream()
.filter(id -> !teamMap.containsKey(id))
.collect(Collectors.toSet());
if (!needQueryTeamIds.isEmpty()) {
teamRepository.findAllById(needQueryTeamIds).forEach(team ->
teamMap.put(team.getId(), team)
);
}
List<Long> teamIds = new ArrayList<>(teamIdSet); List<Long> teamIds = new ArrayList<>(teamIdSet);
// 3. 批量查询团队信息 // 5. 批量查询所有团队的应用配置
Map<Long, Team> teamMap = teamRepository.findAllById(teamIds).stream().collect(toMap(Team::getId, t -> t)); List<TeamApplication> allTeamApps = teamApplicationRepository.findByTeamIdIn(teamIds);
// 4. 批量查询团队配置 // 5.1 提前判断如果没有应用配置直接返回
Map<Long, TeamConfig> configMap = teamConfigRepository.findByTeamIdIn(teamIds).stream().collect(toMap(TeamConfig::getTeamId, c -> c)); if (allTeamApps.isEmpty()) {
log.info("用户 {} 所属团队未配置任何应用", user.getUsername());
return buildEmptyResult(user);
}
// 5. 收集所有环境ID // 6. 一次遍历提取所有需要的ID集合优化减少4次额外遍历
Set<Long> allEnvIds = configMap.values().stream().filter(c -> c.getAllowedEnvironmentIds() != null).flatMap(c -> c.getAllowedEnvironmentIds().stream()).collect(Collectors.toSet()); Set<Long> allEnvIds = new HashSet<>();
Set<Long> appIds = new HashSet<>();
Set<Long> systemIds = new HashSet<>();
Set<Long> workflowIds = new HashSet<>();
List<Long> teamApplicationIds = new ArrayList<>(allTeamApps.size());
for (TeamApplication ta : allTeamApps) {
teamApplicationIds.add(ta.getId());
allEnvIds.add(ta.getEnvironmentId());
appIds.add(ta.getApplicationId());
if (ta.getDeploySystemId() != null) {
systemIds.add(ta.getDeploySystemId());
}
if (ta.getWorkflowDefinitionId() != null) {
workflowIds.add(ta.getWorkflowDefinitionId());
}
}
if (allEnvIds.isEmpty()) { if (allEnvIds.isEmpty()) {
log.info("用户 {} 所属团队未配置任何环境", user.getUsername()); log.info("用户 {} 所属团队未配置任何环境", user.getUsername());
return buildEmptyResult(user); return buildEmptyResult(user);
} }
// 6. 批量查询环境信息 // 7. 应用配置按团队分组
Map<Long, Environment> envMap = environmentRepository.findAllById(allEnvIds).stream().collect(toMap(Environment::getId, e -> e)); Map<Long, List<TeamApplication>> teamAppsMap = allTeamApps.stream()
.collect(groupingBy(TeamApplication::getTeamId));
// 7. 批量查询所有团队的应用配置 // 8. 批量查询环境信息
List<TeamApplication> allTeamApps = teamApplicationRepository.findByTeamIdIn(teamIds); Map<Long, Environment> envMap = environmentRepository.findAllById(allEnvIds).stream()
Map<Long, List<TeamApplication>> teamAppsMap = allTeamApps.stream().collect(groupingBy(TeamApplication::getTeamId)); .collect(toMap(Environment::getId, e -> e));
// 8. 批量查询应用信息 // 9. 批量查询应用信息
Set<Long> appIds = allTeamApps.stream().map(TeamApplication::getApplicationId).collect(Collectors.toSet());
final Map<Long, Application> appMap; final Map<Long, Application> appMap;
if (!appIds.isEmpty()) { if (!appIds.isEmpty()) {
appMap = applicationRepository.findAllById(appIds).stream().collect(toMap(Application::getId, a -> a)); appMap = applicationRepository.findAllById(appIds).stream()
.collect(toMap(Application::getId, a -> a));
} else { } else {
appMap = Collections.emptyMap(); appMap = Collections.emptyMap();
} }
// 9. 批量查询部署系统 // 10. 批量查询部署系统
Set<Long> systemIds = allTeamApps.stream().map(TeamApplication::getDeploySystemId).filter(Objects::nonNull).collect(Collectors.toSet());
final Map<Long, ExternalSystem> systemMap; final Map<Long, ExternalSystem> systemMap;
if (!systemIds.isEmpty()) { if (!systemIds.isEmpty()) {
systemMap = externalSystemRepository.findAllById(systemIds).stream().collect(toMap(ExternalSystem::getId, s -> s)); systemMap = externalSystemRepository.findAllById(systemIds).stream()
.collect(toMap(ExternalSystem::getId, s -> s));
} else { } else {
systemMap = Collections.emptyMap(); systemMap = Collections.emptyMap();
} }
// 10. 批量查询工作流定义 // 11. 批量查询工作流定义
Set<Long> workflowIds = allTeamApps.stream().map(TeamApplication::getWorkflowDefinitionId).filter(Objects::nonNull).collect(Collectors.toSet());
final Map<Long, WorkflowDefinition> workflowMap; final Map<Long, WorkflowDefinition> workflowMap;
if (!workflowIds.isEmpty()) { if (!workflowIds.isEmpty()) {
workflowMap = workflowDefinitionRepository.findAllById(workflowIds).stream().collect(toMap(WorkflowDefinition::getId, w -> w)); workflowMap = workflowDefinitionRepository.findAllById(workflowIds).stream()
.collect(toMap(WorkflowDefinition::getId, w -> w));
} else { } else {
workflowMap = Collections.emptyMap(); workflowMap = Collections.emptyMap();
} }
// 11. 批量查询审批人信息 // 11. 批量查询团队环境配置
Set<Long> approverUserIds = configMap.values().stream().filter(c -> c.getApproverUserIds() != null).flatMap(c -> c.getApproverUserIds().stream()).filter(obj -> obj instanceof List).flatMap(obj -> ((List<?>) obj).stream()).filter(id -> id instanceof Number).map(id -> ((Number) id).longValue()).collect(Collectors.toSet()); List<TeamEnvironmentConfig> teamEnvConfigs = teamEnvironmentConfigRepository.findByTeamIdIn(teamIds);
// (teamId, environmentId) 组织配置: key = teamId + "_" + environmentId
Map<String, TeamEnvironmentConfig> teamEnvConfigMap = teamEnvConfigs.stream()
.collect(toMap(c -> c.getTeamId() + "_" + c.getEnvironmentId(), c -> c));
// 12. 批量查询审批人信息
Set<Long> approverUserIds = teamEnvConfigs.stream()
.filter(c -> c.getApproverUserIds() != null)
.flatMap(c -> c.getApproverUserIds().stream())
.collect(Collectors.toSet());
final Map<Long, User> approverMap; final Map<Long, User> approverMap;
if (!approverUserIds.isEmpty()) { if (!approverUserIds.isEmpty()) {
approverMap = userRepository.findAllById(approverUserIds).stream().collect(toMap(User::getId, u -> u)); approverMap = userRepository.findAllById(approverUserIds).stream().collect(toMap(User::getId, u -> u));
@ -164,9 +233,7 @@ public class DeployServiceImpl implements IDeployService {
approverMap = Collections.emptyMap(); approverMap = Collections.emptyMap();
} }
// 12. 批量查询部署记录信息 // 13. 批量查询部署记录信息teamApplicationIds 已在第6步提取
List<Long> teamApplicationIds = allTeamApps.stream().map(TeamApplication::getId).collect(toList());
// 12.1 批量查询部署统计信息 // 12.1 批量查询部署统计信息
final Map<Long, DeployStatisticsDTO> statisticsMap = new HashMap<>(); final Map<Long, DeployStatisticsDTO> statisticsMap = new HashMap<>();
final Map<Long, DeployRecord> latestRecordMap = new HashMap<>(); final Map<Long, DeployRecord> latestRecordMap = new HashMap<>();
@ -220,10 +287,33 @@ public class DeployServiceImpl implements IDeployService {
recentRecordsMap.putAll(recentRecords.stream().collect(groupingBy(DeployRecord::getTeamApplicationId))); recentRecordsMap.putAll(recentRecords.stream().collect(groupingBy(DeployRecord::getTeamApplicationId)));
} }
// 13. 组装团队数据 // 13. 组装团队数据优化处理潜在的Key冲突保留第一个成员记录
Map<Long, TeamMember> teamMemberMap = teamMembers.stream().collect(toMap(TeamMember::getTeamId, tm -> tm)); Map<Long, TeamMember> teamMemberMap = teamMembers.stream()
.collect(toMap(
TeamMember::getTeamId,
tm -> tm,
(existing, replacement) -> existing // 如果有重复保留第一个
));
List<TeamDeployableDTO> teamDTOs = teamIds.stream().map(teamId -> buildTeamDTO(teamId, currentUserId, teamMap, teamMemberMap, configMap, envMap, teamAppsMap, appMap, systemMap, workflowMap, approverMap, statisticsMap, latestRecordMap, recentRecordsMap)).filter(Objects::nonNull).collect(toList()); // 14. 构建上下文对象避免方法参数过多
BuildContext context = new BuildContext();
context.teamMap = teamMap;
context.teamMemberMap = teamMemberMap;
context.teamEnvConfigMap = teamEnvConfigMap;
context.envMap = envMap;
context.teamAppsMap = teamAppsMap;
context.appMap = appMap;
context.systemMap = systemMap;
context.workflowMap = workflowMap;
context.approverMap = approverMap;
context.statisticsMap = statisticsMap;
context.latestRecordMap = latestRecordMap;
context.recentRecordsMap = recentRecordsMap;
List<TeamDeployableDTO> teamDTOs = teamIds.stream()
.map(teamId -> buildTeamDTO(teamId, currentUserId, context))
.filter(Objects::nonNull)
.collect(toList());
// 14. 组装最终结果 // 14. 组装最终结果
UserDeployableDTO result = new UserDeployableDTO(); UserDeployableDTO result = new UserDeployableDTO();
@ -251,14 +341,13 @@ public class DeployServiceImpl implements IDeployService {
/** /**
* 构建团队DTO * 构建团队DTO
*/ */
private TeamDeployableDTO buildTeamDTO(Long teamId, Long currentUserId, Map<Long, Team> teamMap, Map<Long, TeamMember> teamMemberMap, Map<Long, TeamConfig> configMap, Map<Long, Environment> envMap, Map<Long, List<TeamApplication>> teamAppsMap, Map<Long, Application> appMap, Map<Long, ExternalSystem> systemMap, Map<Long, WorkflowDefinition> workflowMap, Map<Long, User> approverMap, Map<Long, DeployStatisticsDTO> statisticsMap, Map<Long, DeployRecord> latestRecordMap, Map<Long, List<DeployRecord>> recentRecordsMap) { private TeamDeployableDTO buildTeamDTO(Long teamId, Long currentUserId, BuildContext ctx) {
Team team = teamMap.get(teamId); Team team = ctx.teamMap.get(teamId);
if (team == null) { if (team == null) {
return null; return null;
} }
TeamMember member = teamMemberMap.get(teamId); TeamMember member = ctx.teamMemberMap.get(teamId);
TeamConfig config = configMap.get(teamId);
TeamDeployableDTO teamDTO = new TeamDeployableDTO(); TeamDeployableDTO teamDTO = new TeamDeployableDTO();
teamDTO.setTeamId(team.getId()); teamDTO.setTeamId(team.getId());
@ -275,32 +364,37 @@ public class DeployServiceImpl implements IDeployService {
teamDTO.setTeamRole(null); teamDTO.setTeamRole(null);
} }
// 组装环境数据 // 组装环境数据 TeamApplication 中提取该团队的所有环境
if (config != null && config.getAllowedEnvironmentIds() != null && !config.getAllowedEnvironmentIds().isEmpty()) { List<TeamApplication> teamApps = ctx.teamAppsMap.getOrDefault(teamId, Collections.emptyList());
List<Long> allowedEnvIds = config.getAllowedEnvironmentIds();
List<TeamApplication> teamApps = teamAppsMap.getOrDefault(teamId, Collections.emptyList());
// 按环境分组应用 // 按环境分组应用
Map<Long, List<TeamApplication>> appsByEnv = teamApps.stream().collect(groupingBy(TeamApplication::getEnvironmentId)); Map<Long, List<TeamApplication>> appsByEnv = teamApps.stream()
.collect(groupingBy(TeamApplication::getEnvironmentId));
List<DeployableEnvironmentDTO> envDTOs = new ArrayList<>(); List<DeployableEnvironmentDTO> envDTOs = new ArrayList<>();
for (int i = 0; i < allowedEnvIds.size(); i++) { for (Long envId : appsByEnv.keySet()) {
Long envId = allowedEnvIds.get(i); Environment env = ctx.envMap.get(envId);
Environment env = envMap.get(envId);
if (env == null) { if (env == null) {
continue; continue;
} }
DeployableEnvironmentDTO envDTO = buildEnvironmentDTO(env, config, i, appsByEnv.getOrDefault(envId, Collections.emptyList()), appMap, systemMap, workflowMap, approverMap, statisticsMap, latestRecordMap, recentRecordsMap); // 查找该团队+环境的配置
String configKey = teamId + "_" + envId;
TeamEnvironmentConfig envConfig = ctx.teamEnvConfigMap.get(configKey);
// 只显示在 deploy_team_environment_config 表中配置过的环境
if (envConfig == null) {
log.debug("团队 {} 的环境 {} 未在 team_environment_config 中配置,跳过", teamId, envId);
continue;
}
DeployableEnvironmentDTO envDTO = buildEnvironmentDTO(env, envConfig, appsByEnv.get(envId), ctx);
envDTOs.add(envDTO); envDTOs.add(envDTO);
} }
envDTOs.sort(Comparator.comparing(DeployableEnvironmentDTO::getSort)); envDTOs.sort(Comparator.comparing(DeployableEnvironmentDTO::getSort));
teamDTO.setEnvironments(envDTOs); teamDTO.setEnvironments(envDTOs);
} else {
teamDTO.setEnvironments(Collections.emptyList());
}
return teamDTO; return teamDTO;
} }
@ -308,7 +402,7 @@ public class DeployServiceImpl implements IDeployService {
/** /**
* 构建环境DTO * 构建环境DTO
*/ */
private DeployableEnvironmentDTO buildEnvironmentDTO(Environment env, TeamConfig config, int envIndex, List<TeamApplication> teamApps, Map<Long, Application> appMap, Map<Long, ExternalSystem> systemMap, Map<Long, WorkflowDefinition> workflowMap, Map<Long, User> approverMap, Map<Long, DeployStatisticsDTO> statisticsMap, Map<Long, DeployRecord> latestRecordMap, Map<Long, List<DeployRecord>> recentRecordsMap) { private DeployableEnvironmentDTO buildEnvironmentDTO(Environment env, TeamEnvironmentConfig envConfig, List<TeamApplication> teamApps, BuildContext ctx) {
DeployableEnvironmentDTO envDTO = new DeployableEnvironmentDTO(); DeployableEnvironmentDTO envDTO = new DeployableEnvironmentDTO();
envDTO.setEnvironmentId(env.getId()); envDTO.setEnvironmentId(env.getId());
envDTO.setEnvironmentCode(env.getEnvCode()); envDTO.setEnvironmentCode(env.getEnvCode());
@ -317,29 +411,27 @@ public class DeployServiceImpl implements IDeployService {
envDTO.setEnabled(env.getEnabled()); envDTO.setEnabled(env.getEnabled());
envDTO.setSort(env.getSort()); envDTO.setSort(env.getSort());
// 审批配置 // TeamEnvironmentConfig 读取配置信息
if (config.getEnvironmentApprovalRequired() != null && envIndex < config.getEnvironmentApprovalRequired().size()) { envDTO.setRequiresApproval(envConfig != null ? envConfig.getApprovalRequired() : false);
envDTO.setRequiresApproval(config.getEnvironmentApprovalRequired().get(envIndex)); envDTO.setRequireCodeReview(envConfig != null ? envConfig.getRequireCodeReview() : false);
} else { envDTO.setRemark(envConfig != null ? envConfig.getRemark() : null);
envDTO.setRequiresApproval(false);
}
// 审批人 // 审批人列表需要从 List<Long> 转换为 List<ApproverDTO> 以包含用户详细信息
if (config.getApproverUserIds() != null && envIndex < config.getApproverUserIds().size()) { if (envConfig != null && envConfig.getApproverUserIds() != null && !envConfig.getApproverUserIds().isEmpty()) {
Object approverObj = config.getApproverUserIds().get(envIndex); List<ApproverDTO> approverDTOs = envConfig.getApproverUserIds().stream()
if (approverObj instanceof List) { .map(id -> buildApproverDTO(id, ctx.approverMap))
List<?> approverList = (List<?>) approverObj; .filter(Objects::nonNull)
List<ApproverDTO> approverDTOs = approverList.stream().filter(id -> id instanceof Number).map(id -> buildApproverDTO(((Number) id).longValue(), approverMap)).filter(Objects::nonNull).collect(toList()); .collect(toList());
envDTO.setApprovers(approverDTOs); envDTO.setApprovers(approverDTOs);
} else { } else {
envDTO.setApprovers(Collections.emptyList()); envDTO.setApprovers(Collections.emptyList());
} }
} else {
envDTO.setApprovers(Collections.emptyList());
}
// 应用列表 // 应用列表
List<DeployableApplicationDTO> appDTOs = teamApps.stream().map(ta -> buildApplicationDTO(ta, appMap, systemMap, workflowMap, statisticsMap, latestRecordMap, recentRecordsMap)).filter(Objects::nonNull).collect(toList()); List<DeployableApplicationDTO> appDTOs = teamApps.stream()
.map(ta -> buildApplicationDTO(ta, ctx))
.filter(Objects::nonNull)
.collect(toList());
envDTO.setApplications(appDTOs); envDTO.setApplications(appDTOs);
@ -365,8 +457,8 @@ public class DeployServiceImpl implements IDeployService {
/** /**
* 构建应用DTO * 构建应用DTO
*/ */
private DeployableApplicationDTO buildApplicationDTO(TeamApplication ta, Map<Long, Application> appMap, Map<Long, ExternalSystem> systemMap, Map<Long, WorkflowDefinition> workflowMap, Map<Long, DeployStatisticsDTO> statisticsMap, Map<Long, DeployRecord> latestRecordMap, Map<Long, List<DeployRecord>> recentRecordsMap) { private DeployableApplicationDTO buildApplicationDTO(TeamApplication ta, BuildContext ctx) {
Application app = appMap.get(ta.getApplicationId()); Application app = ctx.appMap.get(ta.getApplicationId());
if (app == null) { if (app == null) {
return null; return null;
} }
@ -382,7 +474,7 @@ public class DeployServiceImpl implements IDeployService {
// 部署系统 // 部署系统
dto.setDeploySystemId(ta.getDeploySystemId()); dto.setDeploySystemId(ta.getDeploySystemId());
if (ta.getDeploySystemId() != null) { if (ta.getDeploySystemId() != null) {
ExternalSystem system = systemMap.get(ta.getDeploySystemId()); ExternalSystem system = ctx.systemMap.get(ta.getDeploySystemId());
dto.setDeploySystemName(system != null ? system.getName() : null); dto.setDeploySystemName(system != null ? system.getName() : null);
} }
@ -392,7 +484,7 @@ public class DeployServiceImpl implements IDeployService {
// 工作流定义 // 工作流定义
dto.setWorkflowDefinitionId(ta.getWorkflowDefinitionId()); dto.setWorkflowDefinitionId(ta.getWorkflowDefinitionId());
if (ta.getWorkflowDefinitionId() != null) { if (ta.getWorkflowDefinitionId() != null) {
WorkflowDefinition workflow = workflowMap.get(ta.getWorkflowDefinitionId()); WorkflowDefinition workflow = ctx.workflowMap.get(ta.getWorkflowDefinitionId());
if (workflow != null) { if (workflow != null) {
dto.setWorkflowDefinitionName(workflow.getName()); dto.setWorkflowDefinitionName(workflow.getName());
dto.setWorkflowDefinitionKey(workflow.getKey()); dto.setWorkflowDefinitionKey(workflow.getKey());
@ -400,12 +492,12 @@ public class DeployServiceImpl implements IDeployService {
} }
// 部署统计信息和记录 // 部署统计信息和记录
DeployStatisticsDTO statistics = statisticsMap.get(ta.getId()); DeployStatisticsDTO statistics = ctx.statisticsMap.get(ta.getId());
if (statistics != null) { if (statistics != null) {
dto.setDeployStatistics(statistics); dto.setDeployStatistics(statistics);
// 判断是否正在部署中 // 判断是否正在部署中
DeployRecord latestRecord = latestRecordMap.get(ta.getId()); DeployRecord latestRecord = ctx.latestRecordMap.get(ta.getId());
if (latestRecord != null) { if (latestRecord != null) {
DeployRecordStatusEnums status = latestRecord.getStatus(); DeployRecordStatusEnums status = latestRecord.getStatus();
dto.setIsDeploying(status == DeployRecordStatusEnums.CREATED || status == DeployRecordStatusEnums.RUNNING); dto.setIsDeploying(status == DeployRecordStatusEnums.CREATED || status == DeployRecordStatusEnums.RUNNING);
@ -424,7 +516,7 @@ public class DeployServiceImpl implements IDeployService {
} }
// 最近部署记录列表 // 最近部署记录列表
List<DeployRecord> recentRecords = recentRecordsMap.getOrDefault(ta.getId(), Collections.emptyList()); List<DeployRecord> recentRecords = ctx.recentRecordsMap.getOrDefault(ta.getId(), Collections.emptyList());
List<DeployRecordSummaryDTO> recordSummaryList = recentRecords.stream().map(this::buildDeployRecordSummary).collect(toList()); List<DeployRecordSummaryDTO> recordSummaryList = recentRecords.stream().map(this::buildDeployRecordSummary).collect(toList());
dto.setRecentDeployRecords(recordSummaryList); dto.setRecentDeployRecords(recordSummaryList);

View File

@ -4,9 +4,11 @@ import com.qqchen.deploy.backend.deploy.converter.EnvironmentConverter;
import com.qqchen.deploy.backend.deploy.dto.EnvironmentDTO; import com.qqchen.deploy.backend.deploy.dto.EnvironmentDTO;
import com.qqchen.deploy.backend.deploy.entity.Environment; import com.qqchen.deploy.backend.deploy.entity.Environment;
import com.qqchen.deploy.backend.deploy.entity.QEnvironment; import com.qqchen.deploy.backend.deploy.entity.QEnvironment;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.deploy.query.EnvironmentQuery; import com.qqchen.deploy.backend.deploy.query.EnvironmentQuery;
import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository; import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository; import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamEnvironmentConfigRepository;
import com.qqchen.deploy.backend.deploy.service.IEnvironmentService; import com.qqchen.deploy.backend.deploy.service.IEnvironmentService;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
@ -38,21 +40,16 @@ public class EnvironmentServiceImpl extends BaseServiceImpl<Environment, Environ
@Resource @Resource
private ITeamApplicationRepository teamApplicationRepository; private ITeamApplicationRepository teamApplicationRepository;
@Resource
private ITeamEnvironmentConfigRepository teamEnvironmentConfigRepository;
@Override @Override
public Page<EnvironmentDTO> page(EnvironmentQuery query) { public Page<EnvironmentDTO> page(EnvironmentQuery query) {
Page<EnvironmentDTO> page = super.page(query); Page<EnvironmentDTO> page = super.page(query);
// 填充统计数据 // 填充统计数据
List<EnvironmentDTO> content = page.getContent().stream() List<EnvironmentDTO> content = page.getContent().stream()
.peek(env -> { .peek(env -> fillEnvironmentStatistics(env))
// 统计使用该环境的团队数量去重
Long teamCount = teamApplicationRepository.countDistinctTeamIdByEnvironmentId(env.getId());
env.setTeamCount(teamCount != null ? teamCount : 0L);
// 统计该环境关联的应用数量去重
Long appCount = teamApplicationRepository.countDistinctApplicationIdByEnvironmentId(env.getId());
env.setApplicationCount(appCount != null ? appCount : 0L);
})
.collect(Collectors.toList()); .collect(Collectors.toList());
return new PageImpl<>(content, page.getPageable(), page.getTotalElements()); return new PageImpl<>(content, page.getPageable(), page.getTotalElements());
@ -64,16 +61,36 @@ public class EnvironmentServiceImpl extends BaseServiceImpl<Environment, Environ
// 填充统计数据 // 填充统计数据
return list.stream() return list.stream()
.peek(env -> { .peek(env -> fillEnvironmentStatistics(env))
// 统计使用该环境的团队数量去重 .collect(Collectors.toList());
Long teamCount = teamApplicationRepository.countDistinctTeamIdByEnvironmentId(env.getId()); }
/**
* 填充环境统计数据
* <p>统计逻辑基于 deploy_team_environment_config 只统计已配置该环境的团队</p>
*/
private void fillEnvironmentStatistics(EnvironmentDTO env) {
// 1. 统计配置了该环境的团队数量
Long teamCount = teamEnvironmentConfigRepository.countByEnvironmentId(env.getId());
env.setTeamCount(teamCount != null ? teamCount : 0L); env.setTeamCount(teamCount != null ? teamCount : 0L);
// 统计该环境关联的应用数量去重 // 2. 统计这些团队在该环境下的应用数量去重
Long appCount = teamApplicationRepository.countDistinctApplicationIdByEnvironmentId(env.getId()); if (teamCount != null && teamCount > 0) {
env.setApplicationCount(appCount != null ? appCount : 0L); // 查询配置了该环境的所有团队ID
}) List<Long> teamIds = teamEnvironmentConfigRepository.findByEnvironmentId(env.getId())
.stream()
.map(config -> config.getTeamId())
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!teamIds.isEmpty()) {
Long appCount = teamApplicationRepository.countDistinctApplicationIdByTeamIdsAndEnvironmentId(teamIds, env.getId());
env.setApplicationCount(appCount != null ? appCount : 0L);
} else {
env.setApplicationCount(0L);
}
} else {
env.setApplicationCount(0L);
}
} }
@Override @Override

View File

@ -3,10 +3,14 @@ package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.converter.TeamApplicationConverter; import com.qqchen.deploy.backend.deploy.converter.TeamApplicationConverter;
import com.qqchen.deploy.backend.deploy.dto.TeamApplicationDTO; import com.qqchen.deploy.backend.deploy.dto.TeamApplicationDTO;
import com.qqchen.deploy.backend.deploy.entity.Application; import com.qqchen.deploy.backend.deploy.entity.Application;
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
import com.qqchen.deploy.backend.deploy.entity.Environment;
import com.qqchen.deploy.backend.deploy.entity.Team; import com.qqchen.deploy.backend.deploy.entity.Team;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication; import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.query.TeamApplicationQuery; import com.qqchen.deploy.backend.deploy.query.TeamApplicationQuery;
import com.qqchen.deploy.backend.deploy.repository.IApplicationRepository; import com.qqchen.deploy.backend.deploy.repository.IApplicationRepository;
import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository;
import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository; import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamRepository; import com.qqchen.deploy.backend.deploy.repository.ITeamRepository;
import com.qqchen.deploy.backend.deploy.service.ITeamApplicationService; import com.qqchen.deploy.backend.deploy.service.ITeamApplicationService;
@ -20,14 +24,12 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.*;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication, TeamApplicationDTO, TeamApplicationQuery, Long> public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication, TeamApplicationDTO, TeamApplicationQuery, Long> implements ITeamApplicationService {
implements ITeamApplicationService {
@Resource @Resource
private ITeamApplicationRepository teamApplicationRepository; private ITeamApplicationRepository teamApplicationRepository;
@ -38,6 +40,12 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
@Resource @Resource
private IApplicationRepository applicationRepository; private IApplicationRepository applicationRepository;
@Resource
private IDeployRecordRepository deployRecordRepository;
@Resource
private IEnvironmentRepository environmentRepository;
@Resource @Resource
private TeamApplicationConverter teamApplicationConverter; private TeamApplicationConverter teamApplicationConverter;
@ -45,8 +53,7 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
@Transactional @Transactional
public TeamApplicationDTO create(TeamApplicationDTO dto) { public TeamApplicationDTO create(TeamApplicationDTO dto) {
// 检查团队+应用+环境组合是否已存在 // 检查团队+应用+环境组合是否已存在
if (teamApplicationRepository.existsByTeamIdAndApplicationIdAndEnvironmentIdAndDeletedFalse( if (teamApplicationRepository.existsByTeamIdAndApplicationIdAndEnvironmentIdAndDeletedFalse(dto.getTeamId(), dto.getApplicationId(), dto.getEnvironmentId())) {
dto.getTeamId(), dto.getApplicationId(), dto.getEnvironmentId())) {
throw new BusinessException(ResponseCode.TEAM_APPLICATION_ALREADY_EXISTS); throw new BusinessException(ResponseCode.TEAM_APPLICATION_ALREADY_EXISTS);
} }
@ -56,22 +63,137 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
@Override @Override
public Page<TeamApplicationDTO> page(TeamApplicationQuery query) { public Page<TeamApplicationDTO> page(TeamApplicationQuery query) {
Page<TeamApplicationDTO> page = super.page(query); Page<TeamApplicationDTO> page = super.page(query);
fillExtendedFields(page.getContent());
return new PageImpl<>(page.getContent(), page.getPageable(), page.getTotalElements());
}
// 填充团队名称和应用信息 @Override
List<TeamApplicationDTO> content = page.getContent().stream() public List<TeamApplicationDTO> findAll(TeamApplicationQuery query) {
.peek(teamApp -> { List<TeamApplicationDTO> list = super.findAll(query);
Optional<Team> teamOptional = teamRepository.findById(teamApp.getTeamId()); fillExtendedFields(list);
teamOptional.ifPresent(team -> teamApp.setTeamName(team.getTeamName())); return list;
}
Optional<Application> appOptional = applicationRepository.findById(teamApp.getApplicationId()); /**
appOptional.ifPresent(app -> { * 批量填充扩展字段teamNameapplicationNameapplicationCodeenvironmentName
*
* <p>使用批量查询避免N+1问题
*/
private void fillExtendedFields(List<TeamApplicationDTO> teamApps) {
if (teamApps.isEmpty()) {
return;
}
// 1. 收集所有需要查询的ID
Set<Long> teamIds = teamApps.stream()
.map(TeamApplicationDTO::getTeamId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> applicationIds = teamApps.stream()
.map(TeamApplicationDTO::getApplicationId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> environmentIds = teamApps.stream()
.map(TeamApplicationDTO::getEnvironmentId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 2. 批量查询团队信息
Map<Long, Team> teamMap = new HashMap<>();
if (!teamIds.isEmpty()) {
teamRepository.findAllById(teamIds).forEach(team ->
teamMap.put(team.getId(), team)
);
}
// 3. 批量查询应用信息
Map<Long, Application> appMap = new HashMap<>();
if (!applicationIds.isEmpty()) {
applicationRepository.findAllById(applicationIds).forEach(app ->
appMap.put(app.getId(), app)
);
}
// 4. 批量查询环境信息
Map<Long, Environment> envMap = new HashMap<>();
if (!environmentIds.isEmpty()) {
environmentRepository.findAllById(environmentIds).forEach(env ->
envMap.put(env.getId(), env)
);
}
// 5. 填充扩展字段
teamApps.forEach(teamApp -> {
// 填充团队名称
if (teamApp.getTeamId() != null) {
Team team = teamMap.get(teamApp.getTeamId());
if (team != null) {
teamApp.setTeamName(team.getTeamName());
}
}
// 填充应用信息
if (teamApp.getApplicationId() != null) {
Application app = appMap.get(teamApp.getApplicationId());
if (app != null) {
teamApp.setApplicationName(app.getAppName()); teamApp.setApplicationName(app.getAppName());
teamApp.setApplicationCode(app.getAppCode()); teamApp.setApplicationCode(app.getAppCode());
}
}
// 填充环境名称
if (teamApp.getEnvironmentId() != null) {
Environment env = envMap.get(teamApp.getEnvironmentId());
if (env != null) {
teamApp.setEnvironmentName(env.getEnvName());
}
}
}); });
}) }
.collect(Collectors.toList());
return new PageImpl<>(content, page.getPageable(), page.getTotalElements()); /**
* 删除团队应用及其关联的部署记录
*
* <p>删除逻辑
* <ul>
* <li>1. 先逻辑删除所有关联的部署记录DeployRecord</li>
* <li>2. 再物理删除团队应用TeamApplication</li>
* </ul>
*
* <p>注意
* <ul>
* <li>TeamApplication 使用物理删除@LogicDelete(false)</li>
* <li>DeployRecord 使用逻辑删除@LogicDelete</li>
* </ul>
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
log.info("开始删除团队应用ID: {}", id);
// 1. 查询关联的部署记录
List<DeployRecord> deployRecords = deployRecordRepository.findByTeamApplicationId(id);
if (!deployRecords.isEmpty()) {
log.info("团队应用 {} 关联了 {} 条部署记录,开始级联删除", id, deployRecords.size());
// 2. 逻辑删除所有关联的部署记录
deployRecords.forEach(record -> {
record.setDeleted(true);
deployRecordRepository.save(record);
});
log.info("成功逻辑删除 {} 条部署记录", deployRecords.size());
} else {
log.info("团队应用 {} 没有关联的部署记录", id);
}
// 3. 物理删除团队应用
super.delete(id);
log.info("成功删除团队应用ID: {}", id);
} }
} }

View File

@ -1,42 +0,0 @@
package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.converter.TeamConfigConverter;
import com.qqchen.deploy.backend.deploy.dto.TeamConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.TeamConfig;
import com.qqchen.deploy.backend.deploy.query.TeamConfigQuery;
import com.qqchen.deploy.backend.deploy.repository.ITeamConfigRepository;
import com.qqchen.deploy.backend.deploy.service.ITeamConfigService;
import com.qqchen.deploy.backend.framework.annotation.ServiceType;
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 jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 团队配置Service实现
*
* @author qqchen
* @since 2025-10-31
*/
@Slf4j
@Service
@ServiceType(ServiceType.Type.DATABASE)
public class TeamConfigServiceImpl extends BaseServiceImpl<TeamConfig, TeamConfigDTO, TeamConfigQuery, Long>
implements ITeamConfigService {
@Resource
private ITeamConfigRepository teamConfigRepository;
@Resource
private TeamConfigConverter teamConfigConverter;
@Override
public TeamConfigDTO getByTeamId(Long teamId) {
TeamConfig config = teamConfigRepository.findByTeamIdAndDeletedFalse(teamId)
.orElseThrow(() -> new BusinessException(ResponseCode.TEAM_CONFIG_NOT_FOUND, new Object[]{teamId}));
return teamConfigConverter.toDto(config);
}
}

View File

@ -0,0 +1,152 @@
package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.dto.TeamEnvironmentConfigDTO;
import com.qqchen.deploy.backend.deploy.entity.Environment;
import com.qqchen.deploy.backend.deploy.entity.TeamEnvironmentConfig;
import com.qqchen.deploy.backend.deploy.query.TeamEnvironmentConfigQuery;
import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamEnvironmentConfigRepository;
import com.qqchen.deploy.backend.deploy.service.ITeamEnvironmentConfigService;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 团队环境配置Service实现
*
* @author qqchen
* @since 2025-11-04
*/
@Slf4j
@Service
public class TeamEnvironmentConfigServiceImpl
extends BaseServiceImpl<TeamEnvironmentConfig, TeamEnvironmentConfigDTO, TeamEnvironmentConfigQuery, Long>
implements ITeamEnvironmentConfigService {
@Resource
private ITeamEnvironmentConfigRepository teamEnvironmentConfigRepository;
@Resource
private IEnvironmentRepository environmentRepository;
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Resource
private ITeamApplicationRepository teamApplicationRepository;
@Override
public List<TeamEnvironmentConfigDTO> findByTeamId(Long teamId) {
List<TeamEnvironmentConfigDTO> list = teamEnvironmentConfigRepository.findByTeamId(teamId).stream()
.map(converter::toDto)
.collect(Collectors.toList());
fillExtendedFields(list);
return list;
}
@Override
public TeamEnvironmentConfigDTO findByTeamIdAndEnvironmentId(Long teamId, Long environmentId) {
TeamEnvironmentConfigDTO dto = teamEnvironmentConfigRepository.findByTeamIdAndEnvironmentId(teamId, environmentId)
.map(converter::toDto)
.orElse(null);
if (dto != null) {
fillExtendedFields(Collections.singletonList(dto));
}
return dto;
}
@Override
public boolean exists(Long teamId, Long environmentId) {
return teamEnvironmentConfigRepository.existsByTeamIdAndEnvironmentId(teamId, environmentId);
}
@Override
public Page<TeamEnvironmentConfigDTO> page(TeamEnvironmentConfigQuery query) {
Page<TeamEnvironmentConfigDTO> page = super.page(query);
fillExtendedFields(page.getContent());
return new PageImpl<>(page.getContent(), page.getPageable(), page.getTotalElements());
}
@Override
public List<TeamEnvironmentConfigDTO> findAll(TeamEnvironmentConfigQuery query) {
List<TeamEnvironmentConfigDTO> list = super.findAll(query);
fillExtendedFields(list);
return list;
}
/**
* 批量填充扩展字段environmentNamenotificationChannelNameapplicationCount
*
* <p>使用批量查询避免N+1问题
*/
private void fillExtendedFields(List<TeamEnvironmentConfigDTO> configs) {
if (configs.isEmpty()) {
return;
}
// 1. 收集所有环境ID和通知渠道ID
Set<Long> environmentIds = configs.stream()
.map(TeamEnvironmentConfigDTO::getEnvironmentId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> channelIds = configs.stream()
.map(TeamEnvironmentConfigDTO::getNotificationChannelId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 2. 批量查询环境信息
Map<Long, Environment> environmentMap = new HashMap<>();
if (!environmentIds.isEmpty()) {
environmentRepository.findAllById(environmentIds).forEach(env ->
environmentMap.put(env.getId(), env)
);
}
// 3. 批量查询通知渠道信息
Map<Long, NotificationChannel> channelMap = new HashMap<>();
if (!channelIds.isEmpty()) {
notificationChannelRepository.findAllById(channelIds).forEach(channel ->
channelMap.put(channel.getId(), channel)
);
}
// 4. 填充扩展字段
configs.forEach(config -> {
// 填充环境名称
if (config.getEnvironmentId() != null) {
Environment env = environmentMap.get(config.getEnvironmentId());
if (env != null) {
config.setEnvironmentName(env.getEnvName());
}
}
// 填充通知渠道名称
if (config.getNotificationChannelId() != null) {
NotificationChannel channel = channelMap.get(config.getNotificationChannelId());
if (channel != null) {
config.setNotificationChannelName(channel.getName());
}
}
// 填充应用数量统计
if (config.getTeamId() != null && config.getEnvironmentId() != null) {
Long appCount = teamApplicationRepository.countByTeamIdAndEnvironmentId(
config.getTeamId(),
config.getEnvironmentId()
);
config.setApplicationCount(appCount != null ? appCount : 0L);
}
});
}
}

View File

@ -344,5 +344,198 @@ public class NestedMapUtils {
return getValueFromExecution(execution, variablePath); return getValueFromExecution(execution, variablePath);
} }
// ==================== 统一解析入口推荐使用 ====================
/**
* 统一解析方法 - 智能处理所有类型的输入
*
* <p>自动识别并处理三种情况
* <ul>
* <li>纯文本: "hello" "hello"</li>
* <li>纯表达式: "${user.name}" Object (原始类型可能是 String/Integer/Map )</li>
* <li>模板字符串: "Hello ${user.name}, age: ${user.age}" "Hello John, age: 30"</li>
* </ul>
*
* <p>使用示例
* <pre>
* // 纯表达式 - 返回原始类型
* Object status = NestedMapUtils.resolve(execution, "${jenkins.status}");
* // 返回: NodeExecutionStatusEnum.SUCCESS (枚举类型)
*
* // 模板字符串 - 返回替换后的字符串
* String message = (String) NestedMapUtils.resolve(execution, "构建${jenkins.status}, 编号: ${jenkins.buildNumber}");
* // 返回: "构建SUCCESS, 编号: 123"
*
* // 纯文本 - 直接返回
* String text = (String) NestedMapUtils.resolve(execution, "纯文本");
* // 返回: "纯文本"
* </pre>
*
* @param execution Flowable 执行上下文
* @param input 输入字符串可能包含表达式
* @return 解析后的值可能是任意类型
*/
public static Object resolve(DelegateExecution execution, String input) {
if (input == null || !input.contains("${")) {
return input;
}
// 判断是否是纯表达式整个字符串就是一个 ${xxx}
if (isPureExpression(input)) {
// 纯表达式返回原始类型可能是 String/Integer/Enum/Map
return resolveExpression(execution, input);
} else {
// 模板字符串返回替换后的字符串
return resolveTemplateString(execution, input);
}
}
/**
* 统一解析方法 - DelegateTask 版本
*
* @param task Flowable 任务上下文
* @param input 输入字符串
* @return 解析后的值
*/
public static Object resolve(DelegateTask task, String input) {
if (input == null || !input.contains("${")) {
return input;
}
if (isPureExpression(input)) {
return resolveExpression(task, input);
} else {
return resolveTemplateString(task, input);
}
}
/**
* 判断是否是纯表达式
* 纯表达式定义整个字符串就是一个 ${xxx}前后没有其他内容
*
* @param input 输入字符串
* @return true 如果是纯表达式
*/
private static boolean isPureExpression(String input) {
if (!input.startsWith("${") || !input.endsWith("}")) {
return false;
}
// 检查是否只有一个表达式没有第二个 ${
int secondExprStart = input.indexOf("${", 2);
return secondExprStart == -1;
}
// ==================== 内部实现方法 ====================
/**
* 解析模板字符串支持多个嵌入式表达式
*
* <p>内部方法推荐使用 {@link #resolve(DelegateExecution, String)} 代替
*
* @param execution Flowable 执行上下文
* @param template 模板字符串
* @return 替换后的字符串
*/
private static String resolveTemplateString(DelegateExecution execution, String template) {
if (template == null || !template.contains("${")) {
return template;
}
StringBuilder result = new StringBuilder();
int currentIndex = 0;
while (currentIndex < template.length()) {
int startIndex = template.indexOf("${", currentIndex);
// 没有更多的表达式了
if (startIndex == -1) {
result.append(template.substring(currentIndex));
break;
}
// 添加表达式前的普通文本
if (startIndex > currentIndex) {
result.append(template.substring(currentIndex, startIndex));
}
// 查找表达式的结束位置
int endIndex = template.indexOf("}", startIndex);
if (endIndex == -1) {
// 没有找到闭合的 }说明格式错误保留原样
log.warn("未找到闭合的 }} 符号,保留原样: {}", template.substring(startIndex));
result.append(template.substring(startIndex));
break;
}
// 提取并解析表达式
String expression = template.substring(startIndex, endIndex + 1);
try {
Object value = resolveExpression(execution, expression);
result.append(value != null ? value.toString() : "");
log.debug(" 替换表达式: {} -> {}", expression, value);
} catch (Exception e) {
log.warn(" 表达式解析失败: {},保留原样", expression, e);
result.append(expression);
}
currentIndex = endIndex + 1;
}
return result.toString();
}
/**
* 解析模板字符串支持多个嵌入式表达式- DelegateTask 版本
*
* <p>内部方法推荐使用 {@link #resolve(DelegateTask, String)} 代替
*
* @param task Flowable 任务上下文
* @param template 模板字符串
* @return 替换后的字符串
*/
private static String resolveTemplateString(DelegateTask task, String template) {
if (template == null || !template.contains("${")) {
return template;
}
StringBuilder result = new StringBuilder();
int currentIndex = 0;
while (currentIndex < template.length()) {
int startIndex = template.indexOf("${", currentIndex);
if (startIndex == -1) {
result.append(template.substring(currentIndex));
break;
}
if (startIndex > currentIndex) {
result.append(template.substring(currentIndex, startIndex));
}
int endIndex = template.indexOf("}", startIndex);
if (endIndex == -1) {
log.warn("未找到闭合的 }} 符号,保留原样: {}", template.substring(startIndex));
result.append(template.substring(startIndex));
break;
}
String expression = template.substring(startIndex, endIndex + 1);
try {
Object value = resolveExpression(task, expression);
result.append(value != null ? value.toString() : "");
log.debug(" 替换表达式: {} -> {}", expression, value);
} catch (Exception e) {
log.warn(" 表达式解析失败: {},保留原样", expression, e);
result.append(expression);
}
currentIndex = endIndex + 1;
}
return result.toString();
}
} }

View File

@ -50,8 +50,19 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
// 4. 构建企业微信消息体 // 4. 构建企业微信消息体
Map<String, Object> messageBody = new HashMap<>(); Map<String, Object> messageBody = new HashMap<>();
messageBody.put("msgtype", "text");
// 智能判断消息类型如果包含 Markdown 标签使用 markdown 类型
boolean isMarkdown = isMarkdownMessage(message);
String msgType = isMarkdown ? "markdown" : "text";
messageBody.put("msgtype", msgType);
if (isMarkdown) {
// Markdown 格式消息
Map<String, Object> markdownContent = new HashMap<>();
markdownContent.put("content", message);
messageBody.put("markdown", markdownContent);
} else {
// 纯文本消息
Map<String, Object> textContent = new HashMap<>(); Map<String, Object> textContent = new HashMap<>();
textContent.put("content", message); textContent.put("content", message);
@ -64,6 +75,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
} }
messageBody.put("text", textContent); messageBody.put("text", textContent);
}
// 5. 发送请求 // 5. 发送请求
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
@ -72,7 +84,7 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
String jsonBody = objectMapper.writeValueAsString(messageBody); String jsonBody = objectMapper.writeValueAsString(messageBody);
HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers); HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
log.info("发送企业微信通知 - URL: {}, 消息: {}", weworkConfig.getWebhookUrl(), message); log.info("发送企业微信通知 - URL: {}, 类型: {}, 消息: {}", weworkConfig.getWebhookUrl(), msgType, message);
String response = restTemplate.exchange( String response = restTemplate.exchange(
weworkConfig.getWebhookUrl(), weworkConfig.getWebhookUrl(),
@ -148,5 +160,28 @@ public class WeworkChannelAdapter implements INotificationChannelAdapter {
return mentionedMobileList; return mentionedMobileList;
} }
/**
* 判断是否是 Markdown 格式消息
* 企业微信支持的 Markdown 标签包括
* - <font color="xxx">文本</font>
* - **加粗**
* - `代码`
* - [链接](url)
*
* @param message 消息内容
* @return true 如果包含 Markdown 标签
*/
private boolean isMarkdownMessage(String message) {
if (message == null || message.isEmpty()) {
return false;
}
// 检测企业微信 Markdown 特征标签
return message.contains("<font") ||
message.contains("**") ||
message.contains("`") ||
message.matches(".*\\[.*\\]\\(.*\\).*"); // [text](url) 格式
}
} }

View File

@ -16,6 +16,8 @@ import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.security.core.parameters.P;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Arrays; import java.util.Arrays;
@ -35,8 +37,7 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/v1/notification-channel") @RequestMapping("/api/v1/notification-channel")
@Tag(name = "通知渠道管理", description = "通知渠道管理相关接口") @Tag(name = "通知渠道管理", description = "通知渠道管理相关接口")
public class NotificationChannelApiController public class NotificationChannelApiController extends BaseController<NotificationChannel, NotificationChannelDTO, Long, NotificationChannelQuery> {
extends BaseController<NotificationChannel, NotificationChannelDTO, Long, NotificationChannelQuery> {
@Resource @Resource
private INotificationChannelService notificationChannelService; private INotificationChannelService notificationChannelService;
@ -45,23 +46,23 @@ public class NotificationChannelApiController
private INotificationSendService notificationSendService; private INotificationSendService notificationSendService;
@Override @Override
public Response<NotificationChannelDTO> create(NotificationChannelDTO dto) { public Response<NotificationChannelDTO> create(@Validated @RequestBody NotificationChannelDTO dto) {
return super.create(dto); return super.create(dto);
} }
@Override @Override
public Response<NotificationChannelDTO> update(Long aLong, NotificationChannelDTO dto) { public Response<NotificationChannelDTO> update(@PathVariable Long id,@Validated @RequestBody NotificationChannelDTO dto) {
return super.update(aLong, dto); return super.update(id, dto);
} }
@Override @Override
public Response<Void> delete(Long aLong) { public Response<Void> delete(@PathVariable Long id) {
return super.delete(aLong); return super.delete(id);
} }
@Override @Override
public Response<NotificationChannelDTO> findById(Long aLong) { public Response<NotificationChannelDTO> findById(@PathVariable Long id) {
return super.findById(aLong); return super.findById(id);
} }
@Override @Override

View File

@ -2,6 +2,7 @@ package com.qqchen.deploy.backend.workflow.delegate;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.utils.NestedMapUtils;
import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs; import com.qqchen.deploy.backend.workflow.dto.outputs.BaseNodeOutputs;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum; import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import com.qqchen.deploy.backend.workflow.model.NodeContext; import com.qqchen.deploy.backend.workflow.model.NodeContext;
@ -174,7 +175,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
/** /**
* 解析Map中的表达式 * 解析Map中的表达式
* 使用简单的字符串替换方式从execution变量中获取值 * 自动处理纯表达式模板字符串纯文本三种情况
*/ */
protected Map<String, Object> resolveExpressions(Map<String, Object> inputMap, DelegateExecution execution) { protected Map<String, Object> resolveExpressions(Map<String, Object> inputMap, DelegateExecution execution) {
Map<String, Object> resolvedMap = new HashMap<>(); Map<String, Object> resolvedMap = new HashMap<>();
@ -186,18 +187,13 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
if (value instanceof String) { if (value instanceof String) {
String strValue = (String) value; String strValue = (String) value;
if (strValue.contains("${")) {
try { try {
// 使用工具类直接解析表达式 // 使用统一的 resolve 方法自动智能处理所有情况
Object resolvedValue = com.qqchen.deploy.backend.framework.utils.NestedMapUtils Object resolvedValue = NestedMapUtils.resolve(execution, strValue);
.resolveExpression(execution, strValue); log.debug("解析字段: {} = {} -> {}", entry.getKey(), strValue, resolvedValue);
log.debug("解析表达式: {} = {} -> {}", entry.getKey(), strValue, resolvedValue);
resolvedMap.put(entry.getKey(), resolvedValue); resolvedMap.put(entry.getKey(), resolvedValue);
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to resolve expression: {}, using original value", strValue, e); log.warn("解析失败: {},使用原始值", strValue, e);
resolvedMap.put(entry.getKey(), value);
}
} else {
resolvedMap.put(entry.getKey(), value); resolvedMap.put(entry.getKey(), value);
} }
} else { } else {

View File

@ -25,11 +25,7 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
private INotificationSendService notificationSendService; private INotificationSendService notificationSendService;
@Override @Override
protected NotificationOutputs executeInternal( protected NotificationOutputs executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
DelegateExecution execution,
Map<String, Object> configs,
NotificationInputMapping input
) {
log.info("Sending notification - channel: {}, title: {}, content: {}", input.getChannelId(), input.getTitle(), input.getContent()); log.info("Sending notification - channel: {}, title: {}, content: {}", input.getChannelId(), input.getTitle(), input.getContent());
try { try {

View File

@ -1160,13 +1160,6 @@ VALUES
(2, 4, 'ops_manager', '负责人', NOW(), 'admin', NOW(), 'admin', NOW(), 1, 0), (2, 4, 'ops_manager', '负责人', NOW(), 'admin', NOW(), 'admin', NOW(), 1, 0),
(2, 2, 'it_manager', '运维', NOW(), 'admin', NOW(), 'admin', NOW(), 1, 0); (2, 2, 'it_manager', '运维', NOW(), 'admin', NOW(), 'admin', NOW(), 1, 0);
-- 初始化团队配置数据
INSERT INTO deploy_team_config (team_id, allowed_environment_ids, environment_approval_required, approver_user_ids, create_by, create_time, update_by, update_time, version, deleted)
VALUES
-- 平台研发团队配置(可访问开发和测试环境,都不需要审批)
(1, '[1, 2]', '[false, false]', '[null, null]', 'admin', NOW(), 'admin', NOW(), 1, 0),
-- DevOps团队配置可访问所有环境生产环境需要审批审批人是用户1和4
(2, '[1, 2, 3]', '[false, false, true]', '[null, null, [1, 4]]', 'admin', NOW(), 'admin', NOW(), 1, 0);
-- ==================================================================== -- ====================================================================
-- 定时任务初始化数据 -- 定时任务初始化数据

View File

@ -846,7 +846,7 @@ CREATE TABLE deploy_team_application
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队应用关联表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队应用关联表';
-- 团队配置表 -- 团队配置表
CREATE TABLE deploy_team_config CREATE TABLE deploy_team_environment_config
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人', create_by VARCHAR(100) NULL COMMENT '创建人',
@ -857,18 +857,29 @@ CREATE TABLE deploy_team_config
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
team_id BIGINT NOT NULL COMMENT '团队ID', team_id BIGINT NOT NULL COMMENT '团队ID',
environment_id BIGINT NOT NULL COMMENT '环境ID',
-- 环境权限配置 -- 审批配置
allowed_environment_ids JSON NULL COMMENT '团队可访问的环境ID列表[1, 2, 3]', approval_required BIT NOT NULL DEFAULT 0 COMMENT '是否需要审批',
approver_user_ids JSON NULL COMMENT '审批人用户ID列表[1, 4, 7]',
-- 审批配置与allowed_environment_ids位置对应 -- 通知配置
environment_approval_required JSON NULL COMMENT '各环境是否需要审批boolean数组[false, false, true]', notification_channel_id BIGINT NULL COMMENT '通知渠道ID',
approver_user_ids JSON NULL COMMENT '各环境的审批人列表数组无审批人用null[null, null, [1, 4]]', notification_enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用部署通知',
UNIQUE INDEX uk_team (team_id), -- 安全策略
require_code_review BIT NOT NULL DEFAULT 0 COMMENT '是否要求代码审查通过',
-- 备注
remark VARCHAR(500) NULL COMMENT '备注信息',
UNIQUE INDEX uk_team_env (team_id, environment_id),
INDEX idx_team (team_id),
INDEX idx_env (environment_id),
INDEX idx_deleted (deleted), INDEX idx_deleted (deleted),
CONSTRAINT fk_team_config_team FOREIGN KEY (team_id) REFERENCES deploy_team (id) CONSTRAINT fk_team_env_config_team FOREIGN KEY (team_id) REFERENCES deploy_team (id),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队配置表'; CONSTRAINT fk_team_env_config_env FOREIGN KEY (environment_id) REFERENCES deploy_environment (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队环境配置表';
-- -------------------------------------------------------------------------------------- -- --------------------------------------------------------------------------------------
-- 通知渠道表 -- 通知渠道表

View File

@ -0,0 +1,527 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogBody,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Search, Check, ChevronDown, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useToast } from '@/components/ui/use-toast';
import type {
TeamApplication,
Application,
} from '../types';
import type { RepositoryBranchResponse } from '@/pages/Resource/Git/List/types';
import type { WorkflowDefinition } from '@/pages/Workflow/Definition/List/types';
interface TeamApplicationDialogProps {
open: boolean;
mode: 'create' | 'edit';
teamId: number;
environmentId: number;
environmentName: string;
application?: TeamApplication; // 编辑时传入
applications: Application[]; // 可选择的应用列表
jenkinsSystems: any[];
workflowDefinitions: WorkflowDefinition[];
onOpenChange: (open: boolean) => void;
onSave: (data: {
id?: number;
appId: number;
branch: string;
deploySystemId: number | null;
deployJob: string;
workflowDefinitionId: number | null;
}) => Promise<void>;
onLoadBranches: (appId: number, app: Application) => Promise<RepositoryBranchResponse[]>;
onLoadJenkinsJobs: (systemId: number) => Promise<any[]>;
}
const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
open,
mode,
teamId,
environmentId,
environmentName,
application,
applications,
jenkinsSystems,
workflowDefinitions,
onOpenChange,
onSave,
onLoadBranches,
onLoadJenkinsJobs,
}) => {
const { toast } = useToast();
// 表单状态
const [formData, setFormData] = useState({
appId: null as number | null,
branch: '',
deploySystemId: null as number | null,
deployJob: '',
workflowDefinitionId: null as number | null,
});
// 加载状态
const [branches, setBranches] = useState<RepositoryBranchResponse[]>([]);
const [loadingBranches, setLoadingBranches] = useState(false);
const [jenkinsJobs, setJenkinsJobs] = useState<any[]>([]);
const [loadingJobs, setLoadingJobs] = useState(false);
const [saving, setSaving] = useState(false);
// 搜索和弹窗状态
const [branchSearchValue, setBranchSearchValue] = useState('');
const [branchPopoverOpen, setBranchPopoverOpen] = useState(false);
const [jobSearchValue, setJobSearchValue] = useState('');
const [jobPopoverOpen, setJobPopoverOpen] = useState(false);
// 初始化表单数据
useEffect(() => {
if (open) {
if (mode === 'edit' && application) {
// 编辑模式:填充现有数据
setFormData({
appId: application.applicationId,
branch: application.branch || '',
deploySystemId: application.deploySystemId || null,
deployJob: application.deployJob || '',
workflowDefinitionId: application.workflowDefinitionId || null,
});
// 加载分支和Jobs
const app = applications.find(a => a.id === application.applicationId);
if (app) {
loadBranches(application.applicationId, app);
}
if (application.deploySystemId) {
loadJenkinsJobs(application.deploySystemId);
}
} else {
// 新建模式:重置表单
setFormData({
appId: null,
branch: '',
deploySystemId: null,
deployJob: '',
workflowDefinitionId: null,
});
setBranches([]);
setJenkinsJobs([]);
}
}
}, [open, mode, application]);
// 加载分支列表
const loadBranches = async (appId: number, app: Application) => {
setLoadingBranches(true);
try {
const branchList = await onLoadBranches(appId, app);
setBranches(branchList || []);
} catch (error) {
console.error('加载分支失败:', error);
setBranches([]);
} finally {
setLoadingBranches(false);
}
};
// 加载 Jenkins Jobs
const loadJenkinsJobs = async (systemId: number) => {
setLoadingJobs(true);
try {
const jobs = await onLoadJenkinsJobs(systemId);
setJenkinsJobs(jobs || []);
} catch (error) {
console.error('加载Jenkins Jobs失败:', error);
setJenkinsJobs([]);
} finally {
setLoadingJobs(false);
}
};
// 处理应用选择
const handleAppChange = (appId: number) => {
const app = applications.find(a => a.id === appId);
setFormData({
appId: appId,
branch: '',
deploySystemId: null,
deployJob: '',
workflowDefinitionId: null,
});
// 加载该应用的分支列表
if (app) {
loadBranches(appId, app);
}
};
// 处理 Jenkins 系统选择
const handleJenkinsSystemChange = (systemId: number) => {
setFormData({
...formData,
deploySystemId: systemId,
deployJob: '',
});
loadJenkinsJobs(systemId);
};
// 保存
const handleSave = async () => {
// 表单验证
if (!formData.appId) {
toast({
variant: 'destructive',
title: '请选择应用',
});
return;
}
setSaving(true);
try {
await onSave({
id: mode === 'edit' && application ? application.id : undefined,
appId: formData.appId,
branch: formData.branch,
deploySystemId: formData.deploySystemId,
deployJob: formData.deployJob,
workflowDefinitionId: formData.workflowDefinitionId,
});
toast({
title: mode === 'edit' ? '保存成功' : '添加成功',
});
onOpenChange(false);
} catch (error: any) {
toast({
variant: 'destructive',
title: mode === 'edit' ? '保存失败' : '添加失败',
description: error.response?.data?.message || error.message,
});
} finally {
setSaving(false);
}
};
// 分支过滤
const filteredBranches = branches.filter(branch =>
branch.name.toLowerCase().includes(branchSearchValue.toLowerCase())
);
// Job 过滤
const filteredJobs = jenkinsJobs.filter(job =>
job.jobName.toLowerCase().includes(jobSearchValue.toLowerCase())
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
{mode === 'edit' ? '编辑' : '添加'} - {environmentName}
</DialogTitle>
</DialogHeader>
<DialogBody>
<div className="space-y-4">
{/* 应用选择 */}
<div className="space-y-2">
<Label>
<span className="text-destructive">*</span>
</Label>
<Select
value={formData.appId?.toString() || ''}
onValueChange={(value) => handleAppChange(Number(value))}
disabled={mode === 'edit'} // 编辑时不可修改应用
>
<SelectTrigger>
<SelectValue placeholder="选择应用" />
</SelectTrigger>
<SelectContent>
{applications.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
</div>
) : (
applications.map((app) => (
<SelectItem key={app.id} value={app.id.toString()}>
{app.appName}
</SelectItem>
))
)}
</SelectContent>
</Select>
{mode === 'edit' && (
<p className="text-xs text-muted-foreground">
</p>
)}
</div>
{/* 分支选择 */}
<div className="space-y-2">
<Label></Label>
{formData.appId ? (
<Popover
open={branchPopoverOpen}
onOpenChange={setBranchPopoverOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={loadingBranches || branches.length === 0}
className={cn(
'w-full justify-between',
!formData.branch && 'text-muted-foreground'
)}
>
{formData.branch
? (() => {
const selectedBranch = branches.find(
(b) => b.name === formData.branch
);
return (
<span className="flex items-center gap-2 truncate">
{selectedBranch?.name}
{selectedBranch?.isDefaultBranch && (
<span className="text-xs text-muted-foreground">
()
</span>
)}
</span>
);
})()
: loadingBranches
? '加载中...'
: branches.length === 0
? '无分支'
: '选择分支'}
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<div className="flex items-center border-b px-3">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
placeholder="搜索分支..."
className="flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
value={branchSearchValue}
onChange={(e) => setBranchSearchValue(e.target.value)}
/>
</div>
<ScrollArea className="h-[200px]">
<div className="p-1">
{filteredBranches.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
</div>
) : (
filteredBranches.map((branch) => (
<div
key={branch.id}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent',
branch.name === formData.branch &&
'bg-accent'
)}
onClick={() => {
setFormData({ ...formData, branch: branch.name });
setBranchSearchValue('');
setBranchPopoverOpen(false);
}}
>
<span className="flex-1 truncate">
{branch.name}
{branch.isDefaultBranch && (
<span className="ml-2 text-xs text-muted-foreground">
()
</span>
)}
</span>
{branch.name === formData.branch && (
<Check className="ml-2 h-4 w-4" />
)}
</div>
))
)}
</div>
</ScrollArea>
</PopoverContent>
</Popover>
) : (
<Input placeholder="请先选择应用" disabled />
)}
</div>
{/* Jenkins 系统 */}
<div className="space-y-2">
<Label>Jenkins系统</Label>
<Select
value={formData.deploySystemId?.toString() || ''}
onValueChange={(value) =>
handleJenkinsSystemChange(Number(value))
}
>
<SelectTrigger>
<SelectValue placeholder="选择Jenkins系统" />
</SelectTrigger>
<SelectContent>
{jenkinsSystems.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
Jenkins系统
</div>
) : (
jenkinsSystems.map((system) => (
<SelectItem key={system.id} value={system.id.toString()}>
{system.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{/* Jenkins Job */}
<div className="space-y-2">
<Label>Jenkins Job</Label>
{formData.deploySystemId ? (
<Popover open={jobPopoverOpen} onOpenChange={setJobPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={loadingJobs || jenkinsJobs.length === 0}
className={cn(
'w-full justify-between',
!formData.deployJob && 'text-muted-foreground'
)}
>
<span className="truncate">
{formData.deployJob ||
(loadingJobs
? '加载中...'
: jenkinsJobs.length === 0
? '无Job'
: '选择Job')}
</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<div className="flex items-center border-b px-3">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
placeholder="搜索Job..."
className="flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
value={jobSearchValue}
onChange={(e) => setJobSearchValue(e.target.value)}
/>
</div>
<ScrollArea className="h-[200px]">
<div className="p-1">
{filteredJobs.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
Job
</div>
) : (
filteredJobs.map((job) => (
<div
key={job.id}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent',
job.jobName === formData.deployJob && 'bg-accent'
)}
onClick={() => {
setFormData({
...formData,
deployJob: job.jobName,
});
setJobSearchValue('');
setJobPopoverOpen(false);
}}
>
<span className="flex-1 truncate">
{job.jobName}
</span>
{job.jobName === formData.deployJob && (
<Check className="ml-2 h-4 w-4" />
)}
</div>
))
)}
</div>
</ScrollArea>
</PopoverContent>
</Popover>
) : (
<Input placeholder="请先选择Jenkins系统" disabled />
)}
</div>
{/* 工作流定义 */}
<div className="space-y-2">
<Label></Label>
<Select
value={formData.workflowDefinitionId?.toString() || ''}
onValueChange={(value) =>
setFormData({
...formData,
workflowDefinitionId: Number(value),
})
}
>
<SelectTrigger>
<SelectValue placeholder="选择工作流" />
</SelectTrigger>
<SelectContent>
{workflowDefinitions.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
</div>
) : (
workflowDefinitions.map((workflow) => (
<SelectItem key={workflow.id} value={workflow.id.toString()}>
{workflow.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</div>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{mode === 'edit' ? '保存' : '添加'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export { TeamApplicationDialog };
export default TeamApplicationDialog;