diff --git a/backend/.cursorrules b/backend/.cursorrules new file mode 100644 index 00000000..6bc7ace8 --- /dev/null +++ b/backend/.cursorrules @@ -0,0 +1,526 @@ +您是Java编程、Spring Boot、Spring Framework、Maven、JUnit和相关Java技术的专家,你深思熟虑,给出细致入微的答案,并且善于推理。你细心地提供准确、真实、周到的答案,是一个推理天才。 +需要实现的是企业应用级别的管理框架,需要具有高性能、 + +### 严格遵循的要求 +- 首先,一步一步地思考——详细描述你在伪代码中构建什么的计划。 +- 确认,然后写代码! +- 始终编写正确、最佳实践、DRY原则(不要重复自己)、无错误、功能齐全且可工作的代码,还应与下面代码实施指南中列出的规则保持一致。 +- 专注于简单易读的代码,而不是高性能。 +- 完全实现所有要求的功能。 +- 不要留下待办事项、占位符或缺失的部分。 +- 确保代码完整!彻底确认。 +- 包括所有必需的导入的包,并确保关键组件的正确命名。 +- 如果你认为可能没有正确答案,你就说出来。 +- 如果你不知道答案,就说出来,而不是猜测。 +- 可以提出合理化的建议,但是需要等待是否可以。 +- 对于新设计的实体类、字段、方法都要写注释,对于实际的逻辑要有逻辑注释。 +- 写了新的数据库表,应该在V1.0.0__init_schema.sql、V1.0.1__init_data.sql补充数据库表和初始化数据。 +- 编写代码前,先查看一下V1.0.0__init_schema.sql、V1.0.1__init_data.sql表结构,再进行编写。 + +# Deploy Ease Platform 开发规范 + +### 包结构说明 +- 框架包路径 + com.qqchen.deploy.backend.framework + com.qqchen.deploy.backend.framework.annotation 注解实现 + com.qqchen.deploy.backend.framework.api api相关 + com.qqchen.deploy.backend.framework.audit 审计 + com.qqchen.deploy.backend.framework.controller BaseController + com.qqchen.deploy.backend.framework.converter BaseConverter + com.qqchen.deploy.backend.framework.domain 聚合(AggregateRoot、Entity所有的实体类都继承Entity) + com.qqchen.deploy.backend.framework.dto DTO(BaseDTO、BaseResponse) + com.qqchen.deploy.backend.framework.enums 枚举 + com.qqchen.deploy.backend.framework.exception 异常 + com.qqchen.deploy.backend.framework.handler 全局异常拦截 + com.qqchen.deploy.backend.framework.query 接口请求入参(BaseQuery、DateRange、Range) + com.qqchen.deploy.backend.framework.repository IBaseRepository + com.qqchen.deploy.backend.framework.security JWT + com.qqchen.deploy.backend.framework.service IBaseService + com.qqchen.deploy.backend.framework.service.impl BaseServiceImpl +- 业务包路径 + com.qqchen.deploy.backend.api 三方接口 + com.qqchen.deploy.backend.controller 二方接口 + com.qqchen.deploy.backend.converter 转换器 + com.qqchen.deploy.backend.entity 数据库实体类 + com.qqchen.deploy.backend.integration 第三方系统对接 + com.qqchen.deploy.backend.model + com.qqchen.deploy.backend.model.dto 存放所有DTO对象 + com.qqchen.deploy.backend.model.query 配套page、list接口使用 + com.qqchen.deploy.backend.model.request 接口入参(复杂业务场景使用) + com.qqchen.deploy.backend.model.response 接口出参(复杂业务场景使用) + +### DTO设计规范 +- 简单CRUD场景使用统一的DTO,无需额外的Request/Response对象 +- 以下场景需要使用专门的Request/Response: + 1. 复杂的业务场景(如用户注册、登录) + 2. 有特殊验证需求的接口 + 3. 入参和出参差异较大的接口 + 4. 需要特殊安全处理的接口 +- DTO应继承BaseDTO,获取基础字段支持 +- Request/Response对象应该放在对应的model.request和model.response包中 + +### 验证规范 +- DTO字段验证: + 1. 使用Jakarta Validation注解 + 2. 自定义验证注解 + 3. 分组验证 +- 示例: +```java +@Data +public class ExternalSystemDTO extends BaseDTO { + @NotBlank(message = "系统名称不能为空") + private String name; + + @NotNull(message = "系统类型不能为空") + private SystemType type; +} +``` + +### Service层规范 +- 简单CRUD场景直接继承BaseServiceImpl即可 +- 复杂业务场景需要: + 1. 定义业务接口方法 + 2. 实现具体的业务逻辑 + 3. 处理业务异常 + 4. 添加事务控制 +- 示例: +```java +@Slf4j +@Service +@ServiceType(DATABASE) +public class ExternalSystemServiceImpl extends BaseServiceImpl + implements IExternalSystemService { + + @Override + @Transactional + public boolean testConnection(Long id) { + // 业务实现 + } +} +``` + +### 查询规范 +- 简单查询使用BaseQuery +- 复杂查询需要: + 1. 继承BaseQuery + 2. 使用@QueryField注解标注查询字段 + 3. 指定查询类型 +- 示例: +```java +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalSystemQuery extends BaseQuery { + @QueryField(field = "name", type = QueryType.LIKE) + private String name; + + @QueryField(field = "type") + private SystemType type; +} +``` + +### 异常处理规范 +- 业务异常应承BusinessException +- 使用ResponseCode定义错误码 +- 在messages.properties中定义错误消息 +- 通过GlobalExceptionHandler统一处理异常 +- 示例: +```java +throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_DISABLED); +``` + +### Controller层规范 +- REST FULL接口使用框架BaseController +- 新增接口命名规范: + - 三方接口:模块名ApiController(如:ExternalSystemApiController) + - 二方接口:模块名Controller(如:ExternalSystemController) +- 示例: +```java +@Slf4j +@RestController +@RequestMapping("/api/v1/external-system") +@Tag(name = "外部系统管理", description = "外部系统管理相关接口") +public class ExternalSystemApiController extends BaseController { + // 特定业务方法实现 +} +``` + +### Repository层规范 +- 继承IBaseRepository +- 定义特定的查询方法 +- 示例: +```java +@Repository +public interface IExternalSystemRepository extends IBaseRepository { + boolean existsByNameAndDeletedFalse(String name); +} +``` + +### Converter规范 +- 继承BaseConverter,遵循以下规则: + 1. 简单场景(字段完全匹配)示例: + ```java + @Mapper(config = BaseConverter.class) + public interface ExternalSystemConverter extends BaseConverter { + // 字段完全匹配时无需额外配置 + } + ``` + 2. 复杂场景(需要特殊映射)示例: + ```java + @Mapper(config = BaseConverter.class) + public interface UserConverter extends BaseConverter { + @Mapping(target = "departmentId", source = "department.id") + @Mapping(target = "departmentName", source = "department.name") + UserDTO toDto(User entity); + } + ``` + +### 测试规范 +- Service层测试: + 1. 使用@SpringBootTest + 2. 使用@MockBean模拟依赖 + 3. 测试所有业务场景 +- Controller层测试: + 1. 使用@WebMvcTest或@SpringBootTest + @AutoConfigureMockMvc + 2. 使用MockMvc测试接口 + 3. 测试所有接口路径 +- Repository层测试: + 1. 使用@DataJpaTest + 2. 测试所有查询方法 + +### 文档规范 +- 类注释:说明类的用途、作者、版本 +- 方法注释:说明参数、返回值、异常 +- 业务方法注释:说明业务规则 +- API文档:使用Swagger注解 + +### 命名规范 +- 使用PascalCase作为类名(例,UserController、OrderService) +- 使用camelCase作为方法和变量名(例如,findUserById、isOrderValid) +- 对常量使用ALL_CAPS(例如,MAX_RETRY_ATTEMPTS、DEFAULT_PAGE_SIZE) +- service、repository接口类需要以I开头,service实现类无需使用I开头但尾部需要Impl结尾 + +### 数据库规范 +- 使用Flyway进行数据库版本控制 +- 新增表结构写入V1.0.0__init_schema.sql +- 新增初始数据写入V1.0.1__init_data.sql +- 表名使用下划线命名法(例如:sys_user, sys_role) +- 字段名使用下划线命名法(例如:create_time, update_by) +- 必须包含基础字段:id, create_time, create_by, update_time, update_by, version, deleted + +### 国际化规范 +- 使用ResponseCode进行异常码定义 +- 在messages.properties中定义中文消息 +- 在messages_en_US.properties中定义英文消息 +- 在messages_zh_CN.properties中定义中文繁体消息 + +### 文档维护规范 +- README.md文件维护规则: + 1. 接口变更必须同步更新README.md + - 新增接口:在对应模块的API文档中添加接口说明 + - 修改接口:更新对应接口的参数说明和响应格式 + - 删除接口:移除对应接口的文档说明 + 2. 接口文档格式要求: + ```http + [HTTP方法] [接口路径] + + 请求参数: + { + "参数1": "值1", // 参数说明(是否必填) + "参数2": "值2" // 参数说明(是否必填) + } + + 响应结果: + { + "success": true, + "code": 200, + "message": "操作成功", + "data": { + // 响应数据结构 + } + } + ``` + 3. 功能清单维护: + - 新增功能:在"已实现功能"章节添加功能说明 + - 修改功能:更新对应功能的描述 + - 删除功能:移除对应功能说明 + 4. 文档结构规范: + - 功能描述必须清晰准确 + - 使用markdown格式化文档 + - 保持文档层级结构一致 + - 使用统一的示例格式 + +### 错误码规范 +- 错误码分类: + 1. 系统级错误(1xxx) + - 1000-1099:通用系统错误 + - 1100-1199:依赖注入错误 + - 1200-1299:数据库错误 + 2. 业务级错误(2xxx) + - 2000-2099:通用业务错误 + - 2100-2199:角色相关错误 + - 2200-2299:JWT相关错误 + - 2300-2399:部门相关错误 + - 2400-2499:权限相关错误 + - 2500-2599:外部系统相关错误 +- 错误码命名规则: + 1. 使用大写字母和下划线 + 2. 采用`模块_操作_错误`的命名方式 + 3. 在ResponseCode枚举类中定义 +- 错误信息规范: + 1. 在messages.properties中定义错误信息 + 2. 错误信息要简洁明了 + 3. 包含问题描述和可能的解决方案 + 4. 支持国际化 + +### 接口安全规范 +- JWT Token规范: + 1. Token结构: + - Header:包含算法信息 + - Payload:包含用户信息和权限信息 + - Signature:使用密钥签名 + 2. Token有效期: + - Access Token:2小时 + - Refresh Token:7天 + 3. Token刷新机制: + - 使用Refresh Token获取新的Access Token + - Refresh Token一次性使用 +- 接口权限控制: + 1. 使用@PreAuthorize注解控制接口访问权限 + 2. 使用@Secured注解控制方法级别权限 + 3. 实现自定义权限评估器 +- 敏感数据处理: + 1. 密码等敏感信息必须加密存储 + 2. 使用AES加密传输敏感数据 + 3. 日志中不得打印敏感信息 + 4. 接口返回时脱敏处理 + +### 代码审查规范 +- 审查重点: + 1. 代码质量: + - 代码是否符合编码规范 + - 是否存在重复代码 + - 是否有性能问题 + 2. 业务逻辑: + - 业务流程是否正确 + - 异常处理是否完善 + - 边界条件是否考虑 + 3. 安全性: + - 是否存在安全漏洞 + - 敏感数据是否安全处理 + - 权限控制是否正确 + 4. 测试覆盖: + - 单元测试是否完整 + - 是否覆盖关键路径 + - 是否包含边界测试 +- 审查流程: + 1. 提交前自查 + 2. 提交Pull Request + 3. 代码评审 + 4. 修改完善 + 5. 评审通过 +- 审查标准: + 1. 代码实现是否满足需求 + 2. 是否符合开发规范 + 3. 是否有充分的测试覆盖 + 4. 文档是否同步更新 + +### 缓存使用规范 +- 缓存注解: + 1. @Cacheable:适用于查询操作 + 2. @CachePut:适用于更新操作 + 3. @CacheEvict:适用于删除操作 + 4. @Caching:组合多个缓存操作 +- 缓存Key: + 1. 格式:`模块:业务:标识` + 2. 示例:`user:info:1` + 3. 避免特殊字符 + 4. 长度控制在200以内 +- 缓存策略: + 1. 读多写少用Cache Aside + 2. 写多读少用Write Through + 3. 高一致性用Write Back +- 示例: +```java +@Slf4j +@Service +@ServiceType(DATABASE) +public class UserServiceImpl extends BaseServiceImpl implements IUserService { + + @Resource + private IUserRepository userRepository; + + @Resource + private UserConverter userConverter; + + @Override + @Cacheable(value = "user", key = "#id") + public UserDTO findById(Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new BusinessException(ResponseCode.USER_NOT_FOUND)); + return userConverter.toDto(user); + } + + @Override + @Transactional + @CacheEvict(value = "user", key = "#id") + public void delete(Long id) { + User user = findEntityById(id); + user.setDeleted(true); + userRepository.save(user); + } +} +``` + +### 分层设计规范 +- Controller层: + 1. REST接口设计 + 2. 参数校验 + 3. 权限控制 + 4. 响应封装 +- Service层: + 1. 业务逻辑处理 + 2. 事务控制 + 3. 缓存处理 + 4. 并发控制 +- Repository层: + 1. 数据访问 + 2. 查询优化 + 3. 持久化操作 + +### 数据对象规范 +- DTO设计: + 1. 简单CRUD场景使用统一DTO + 2. 复杂业务场景使用专门的Request/Response + 3. 继承BaseDTO获取基础字段 +- 验证规则: + 1. 使用Jakarta Validation注解 + 2. 自定义验证注解 + 3. 分组验证 +- 对象转换: + 1. 使用MapStruct进行对象转换 + 2. 继承BaseConverter + 3. 显式声明特殊映射 + +### 业务处理规范 +- 事务管理: + 1. 声明式事务(@Transactional) + 2. 事务传播机制: + - REQUIRED:默认,需要事务 + - REQUIRES_NEW:新建事务 + - SUPPORTS:支持当前事务 + - NOT_SUPPORTED:不支持事务 + - NEVER:不允许事务 + 3. 事务隔离级别: + - READ_COMMITTED:默认 + - REPEATABLE_READ:需要时使用 +- 并发控制: + 1. 乐观锁: + - 使用@Version注解 + - 实现重试机制 + 2. 悲观锁: + - findByIdWithLock方法 + - 限定锁范围和时间 + 3. 分布式锁: + - 使用Redis实现 + - 设置超时时间 + - 防止死锁 +- 缓存策略: + 1. 缓存注解使用: + - @Cacheable:查询 + - @CachePut:更新 + - @CacheEvict:删除 + 2. 缓存Key规范: + - 格式:模块:业务:标识 + - 避免特殊字符 + 3. 缓存更新策略: + - Cache Aside + - Write Through + - Write Back + +### 安全规范 +- 认证授权: + 1. JWT Token结构和有效期 + 2. 权限注解使用 + 3. 自定义权限评估 +- 敏感信息: + 1. 密码加密存储 + 2. 传输数据加密 + 3. 日志脱敏处理 + 4. 接口参数脱敏 +- 接口防护: + 1. 参数校验 + 2. SQL注入防护 + 3. XSS防护 + 4. CSRF防护 + +### 异常处理规范 +- 异常分类: + 1. 业务异常(BusinessException) + 2. 系统异常(SystemException) + 3. 参数异常(ValidationException) +- 错误码: + 1. 系统级错误(1xxx) + 2. 业务级错误(2xxx) + 3. 第三方错误(3xxx) +- 异常处理: + 1. 统一异常处理器 + 2. 错误信息国际化 + 3. 异常日志记录 + +### 日志规范 +- 日志级别: + 1. ERROR:系统错误、业务异常 + 2. WARN:潜在问题警告 + 3. INFO:重要业务操作 + 4. DEBUG:调试信息 +- 日志内容: + 1. 操作人信息 + 2. 业务模块 + 3. 操作类型 + 4. 关键参数 +- 日志脱敏: + 1. 密码信息 + 2. 个人隐私 + 3. 敏感配置 + +### 文档规范 +- 代码注释: + 1. 类注释:用途、作者、版本 + 2. 方法注释:参数、返回值、异常 + 3. 业务注释:业务规则、处理逻辑 +- API文档: + 1. 使用Swagger注解 + 2. 接口说明完整 + 3. 参数说明清晰 +- README维护: + 1. 功能清单及时更新 + 2. 接口文档同步修改 + 3. 环境配置说明 + +### 测试规范 +- 单元测试: + 1. Service层业务测试 + 2. 重要工具类测试 + 3. 边界条件测试 +- 集成测试: + 1. Controller层接口测试 + 2. 数据库操作测试 + 3. 缓存操作测试 +- 测试原则: + 1. 测试覆盖率要求 + 2. 测试数据隔离 + 3. 测试用例完整性 + +### 性能优化规范 +- 数据库优化: + 1. 索引设计 + 2. SQL优化 + 3. 分页查询 +- 代码优化: + 1. 循环优化 + 2. 集合操作 + 3. 字符串处理 +- 缓存优化: + 1. 缓存粒度 + 2. 缓存更新 + 3. 缓存穿透处理 \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index baf8185f..7ca9e4ae 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,136 +1,748 @@ +# Deploy Ease Platform -# 问题解决方案 -##### 1、Cannot compare left expression of type 'java.io.Serializable' with right expression of type 'java.lang.Long' -``` -使用@Query查询,因为无法解决序列化问题 -``` +Deploy Ease Platform 是一个现代化的部署管理平台,旨在简化和自动化应用程序的部署流程。 +## 已实现功能 -` -分层结构从上到下: -` -``` -Controller (表现层) - ↓ -Service (业务层) - ↓ -Repository (数据访问层) -``` -` -对于外部集成: -` -``` -Controller - ↓ -Service ←→ Integration Service (集成层) - ↓ -Repository -``` +### 1. 外部系统管理 +- [x] 基础功能 + - 创建外部系统(支持Jenkins、Git等类型) + - 更新系统信息 + - 删除系统 + - 查询系统详情 + - 分页查询系统列表 + - 条件筛选查询 +- [x] Jenkins集成 + - 连接测试 + - 任务同步 + - 状态检查 + - 支持用户名密码认证 + - 支持Token认证 +- [x] Git集成 + - 仓库连接测试 + - 代码分支同步 + - 提交历史查询 + - 支持用户名密码认证 + - 支持SSH密钥认证 + - 支持Token认证 -``` -GET /api/users - 查询所有用户 -GET /api/users/list?enabled=true - 查询所有启用的用户 -GET /api/users/page?pageNum=1&pageSize=10 - 分页查询用户 -``` +### 2. 用户权限管理 +- [x] 用户管理 + - 用户注册 + - 用户登录 + - 密码重置 + - 用户信息修改 + - 用户状态管理 + - 部门分配 +- [x] 角色管理 + - 角色创建与维护 + - 角色标签管理 + - 角色权限分配 + - 用户角色分配 +- [x] 权限管理 + - 权限创建与维护 + - 权限分类管理 + - 权限与菜单关联 + - 权限检查与控制 -`查询日期示例` -``` -UserQuery query = new UserQuery(); -query.setCreateTimeRange( - LocalDateTime.now().minusDays(7), // 一周内 - LocalDateTime.now() -); -query.setEnabled(true); -query.setCreateBy("admin"); +### 3. 菜单管理 +- [x] 基础功能 + - 菜单树维护 + - 菜单排序 + - 菜单显示控制 +- [x] 权限集成 + - 菜单权限配置 + - 按钮权限配置 + - 数据权限配置 +- [x] 前端集成 + - 动态路由生成 + - 菜单组件加载 + - 权限指令集成 +### 4. 部门管理 +- [x] 基础功能 + - 部门树维护 + - 部门编码管理 + - 部门描述维护 + - 部门排序 +- [x] 人员管理 + - 部门负责人设置 + - 部门人员管理 + - 部门权限控制 -@QueryField(type = QueryType.IN) -private String status; // 可以传入 "ACTIVE,PENDING,CLOSED" +### 5. 系统功能 +- [x] 认证授权 + - JWT令牌认证 + - 权限拦截器 + - 角色检查器 + - 权限注解支持 +- [x] 数据审计 + - 创建人记录 + - 创建时间记录 + - 更新人记录 + - 更新时间记录 + - 版本控制 +- [x] 异常处理 + - 统一异常处理 + - 业务异常处理 + - 系统异常处理 + - 参数校验异常处理 +- [x] 数据库支持 + - Flyway数据库版本控制 + - 基础表结构 + - 初始化数据 + - 软删除支持 -``` -``` -QueryDSL使用方法 -@Service -@Transactional -public class UserServiceImpl extends BaseServiceImpl implements UserService { - - private final UserRoleRepository userRoleRepository; - private final QUserRole qUserRole = QUserRole.userRole; - - public UserServiceImpl(UserRepository userRepository, UserRoleRepository userRoleRepository) { - super(userRepository); - this.userRoleRepository = userRoleRepository; +### 6. 开发支持 +- [x] 基础框架 + - 统一响应格式 + - 统一分页格式 + - 基础CRUD封装 + - DTO自动转换 +- [x] 代码规范 + - 开发规范文档 + - 接口文档规范 + - 测试规范 + - 注释规范 +- [x] 测试支持 + - 单元测试框架 + - 集成测试支持 + - 测试数据构建 + - Mock支持 + +## 技术栈 + +### 后端技术栈 +- Spring Boot 3.x +- Spring Security + JWT +- Spring Data JPA +- Flyway数据库版本控制 +- MapStruct对象映射 +- Lombok +- JUnit 5 + +### 数据库 +- MySQL 8.0+ + +## API文档 + +### 1. 外部系统管理API + +#### 1.1 分页查询 +```http +GET /api/v1/external-system/page + +请求参数: +{ + "pageNum": 1, // 页码(从1开始) + "pageSize": 10, // 每页大小 + "sortField": "createTime", // 排序字段 + "sortOrder": "desc", // 排序方式 + "name": "string", // 系统名称(可选) + "type": "JENKINS", // 系统类型(可选) + "enabled": true // 是否启用(可选) +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "content": [{ + "id": 1, + "createTime": "2024-01-01 12:00:00", + "createBy": "admin", + "updateTime": "2024-01-01 12:00:00", + "updateBy": "admin", + "name": "Jenkins测试环境", + "type": "JENKINS", + "url": "http://jenkins.test.com", + "remark": "测试环境Jenkins服务器", + "sort": 1, + "enabled": true, + "authType": "USERNAME_PASSWORD", + "username": "admin", + "password": "******", + "token": null, + "syncStatus": "SUCCESS", + "lastSyncTime": "2024-01-01 12:00:00", + "config": "{}" + }], + "totalElements": 100, + "totalPages": 10, + "size": 10, + "number": 0, + "first": true, + "last": false, + "empty": false } - - public Set getUserRoles(Long userId) { - // 使用QueryDSL构建查询条件 - Predicate predicate = qUserRole.user.id.eq(userId); - return StreamSupport.stream( - userRoleRepository.findAll(predicate).spliterator(), - false - ).collect(Collectors.toSet()); +} +``` + +#### 1.2 创建外部系统 +```http +POST /api/v1/external-system + +请求参数: +{ + "name": "Jenkins测试环境", // 系统名称(必填) + "type": "JENKINS", // 系统类型(必填) + "url": "http://jenkins.test.com", // 系统地址(必填) + "remark": "测试环境Jenkins服务器", // 备注(可选) + "sort": 1, // 排序(可选) + "enabled": true, // 是否启用(可选,默认true) + "authType": "USERNAME_PASSWORD", // 认证方式(必填) + "username": "admin", // 用户名(可选) + "password": "password123", // 密码(可选) + "token": null, // 访问令牌(可选) + "config": "{}" // 配置信息(可选) +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "id": 1, + // ... 其他字段同上 } - - public void assignRole(Long userId, Long roleId) { - User user = findById(userId); - Role role = roleRepository.findById(roleId) - .orElseThrow(() -> new RuntimeException("Role not found")); - - // 检查是否已存在 - Predicate predicate = qUserRole.user.id.eq(userId) - .and(qUserRole.role.id.eq(roleId)); - - if (!userRoleRepository.exists(predicate)) { - UserRole userRole = new UserRole(user, role); - userRoleRepository.save(userRole); +} +``` + +#### 1.3 更新外部系统 +```http +PUT /api/v1/external-system/{id} + +请求参数: +{ + // 同创建接口 +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "id": 1, + // ... 其他字段同上 + } +} +``` + +#### 1.4 删除外部系统 +```http +DELETE /api/v1/external-system/{id} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功" +} +``` + +#### 1.5 测试连接 +```http +POST /api/v1/external-system/{id}/test-connection + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": true +} +``` + +#### 1.6 同步数据 +```http +POST /api/v1/external-system/{id}/sync + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功" +} +``` + +#### 1.7 检查状态 +```http +GET /api/v1/external-system/{id}/status + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "syncStatus": "SUCCESS", + "lastSyncTime": "2024-01-01 12:00:00" + } +} +``` + +### 2. 用户认证API + +#### 2.1 用户登录 +```http +POST /api/v1/user/login + +请求参数: +{ + "username": "admin", // 用户名(必填) + "password": "password" // 密码(必填) +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiJ9...", + "user": { + "id": 1, + "username": "admin", + "nickname": "管理员", + "email": "admin@example.com", + "phone": "13800138000", + "departmentId": 1, + "departmentName": "技术部" } } } ``` -``` --- 插入正常用户数据 -INSERT INTO sys_user ( - username, password, nickname, email, phone, - enabled, deleted, dept_id, dept_name, - create_by, create_time, update_by, update_time, version -) VALUES --- 管理员用户 -('admin', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '系统管理员', 'admin@example.com', '13800138000', -true, false, 1, '技术部', -'system', '2024-01-01 09:00:00', null, null, 0), +#### 2.2 获取当前用户信息 +```http +GET /api/v1/user/current --- 普通用户 -('user01', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '张三', 'zhangsan@example.com', '13800138001', -true, false, 1, '技术部', -'admin', '2024-01-02 10:00:00', null, null, 0), - -('user02', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '李四', 'lisi@example.com', '13800138002', -true, false, 2, '市场部', -'admin', '2024-01-03 11:00:00', 'admin', '2024-01-04 15:00:00', 1), - --- 已禁用的用户 -('disabled_user', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '王五', 'wangwu@example.com', '13800138003', -false, false, 2, '市场部', -'admin', '2024-01-05 14:00:00', 'admin', '2024-01-06 16:00:00', 1), - --- 已删除的用户 -('deleted_user', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '赵六', 'zhaoliu@example.com', '13800138004', -true, true, 3, '财务部', -'admin', '2024-01-07 10:00:00', 'admin', '2024-01-08 09:00:00', 2), - --- 最近创建的用户 -('new_user01', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '小明', 'xiaoming@example.com', '13800138005', -true, false, 1, '技术部', -'admin', '2024-03-01 09:00:00', null, null, 0), - -('new_user02', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '小红', 'xiaohong@example.com', '13800138006', -true, false, 2, '市场部', -'admin', '2024-03-02 10:00:00', null, null, 0), - --- 多次更新的用户 -('updated_user', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '小张', 'xiaozhang@example.com', '13800138007', -true, false, 3, '财务部', -'admin', '2024-02-01 09:00:00', 'admin', '2024-03-01 15:00:00', 5); +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "id": 1, + "username": "admin", + "nickname": "管理员", + "email": "admin@example.com", + "phone": "13800138000", + "departmentId": 1, + "departmentName": "技术部" + } +} ``` +### 3. 菜单管理API + +#### 3.1 获取当前用户菜单 +```http +GET /api/v1/menu/current + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": [{ + "id": 1, + "name": "系统管理", + "path": "/system", + "component": "Layout", + "icon": "setting", + "permission": "system:view", + "type": 1, // 1:目录 2:菜单 3:按钮 + "parentId": null, + "sort": 1, + "hidden": false, + "children": [{ + "id": 2, + "name": "用户管理", + "path": "/system/user", + "component": "system/user/index", + "icon": "user", + "permission": "system:user:view", + "type": 2, + "parentId": 1, + "sort": 1, + "hidden": false, + "children": [] + }] + }] +} +``` + +#### 3.2 获取菜单树 +```http +GET /api/v1/menu/tree + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": [{ + // 结构同上 + }] +} +``` + +#### 3.3 获取权限树 +```http +GET /api/v1/menu/permission-tree + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": [{ + "id": 1, + "name": "系统管理", + "type": 1, + "children": [{ + "id": 2, + "name": "用户管理", + "type": 2, + "permissions": [{ + "id": 3, + "code": "system:user:add", + "name": "新增用户", + "type": "BUTTON" + }] + }] + }] +} +``` + +### 4. 部门管理API + +#### 4.1 获取部门树 +```http +GET /api/v1/department/tree + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": [{ + "id": 1, + "code": "TECH", + "name": "技术部", + "description": "技术研发部门", + "parentId": null, + "sort": 1, + "enabled": true, + "leaderId": 1, + "leaderName": "张三", + "children": [{ + "id": 2, + "code": "DEV", + "name": "研发组", + "description": "研发团队", + "parentId": 1, + "sort": 1, + "enabled": true, + "leaderId": 2, + "leaderName": "李四", + "children": [] + }] + }] +} +``` + +#### 4.2 创建部门 +```http +POST /api/v1/department + +请求参数: +{ + "code": "TECH", // 部门编码(必填) + "name": "技术部", // 部门名称(必填) + "description": "技术研发部门", // 描述(可选) + "parentId": null, // 上级部门ID(可选) + "sort": 1, // 排序(可选) + "enabled": true, // 是否启用(可选,默认true) + "leaderId": 1, // 负责人ID(可选) + "leaderName": "张三" // 负责人姓名(可选) +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "id": 1, + // ... 其他字段同上 + } +} +``` + +### 5. 角色管理API + +#### 5.1 分配角色标签 +```http +POST /api/v1/role/{id}/tags + +请求参数: +{ + "tagIds": [1, 2, 3] // 标签ID列表 +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功" +} +``` + +#### 5.2 分配用户角色 +```http +POST /api/v1/role/{userId}/assignRoles + +请求参数: +{ + "roleIds": [1, 2, 3] // 角色ID列表 +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功" +} +``` + +#### 5.3 获取角色权限 +```http +GET /api/v1/role/{id}/permissions + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": [{ + "id": 1, + "code": "system:user:view", + "name": "查看用户", + "type": "MENU", + "sort": 1, + "menuName": "用户管理" + }] +} +``` + +#### 5.4 分配角色权限 +```http +POST /api/v1/role/{id}/permissions + +请求参数: +{ + "permissionIds": [1, 2, 3] // 权限ID列表 +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功" +} +``` + +### 6. 权限管理API + +#### 6.1 分页查询权限 +```http +GET /api/v1/permission/page + +请求参数: +{ + "pageNum": 1, + "pageSize": 10, + "code": "system", // 权限编码(可选) + "name": "系统", // 权限名称(可选) + "type": "MENU", // 权限类型(可选) + "enabled": true, // 是否启用(可选) + "menuId": 1 // 菜单ID(可选) +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "content": [{ + "id": 1, + "code": "system:user:view", + "name": "查看用户", + "type": "MENU", + "sort": 1, + "menuId": 2, + "menuName": "用户管理" + }], + "totalElements": 100, + "totalPages": 10, + "size": 10, + "number": 0, + "first": true, + "last": false, + "empty": false + } +} +``` + +#### 6.2 创建权限 +```http +POST /api/v1/permission + +请求参数: +{ + "code": "system:user:add", // 权限编码(必填) + "name": "新增用户", // 权限名称(必填) + "type": "BUTTON", // 权限类型(必填) + "menuId": 2, // 菜单ID(必填) + "sort": 1 // 排序(可选) +} + +响应结果: +{ + "success": true, + "code": 200, + "message": "操作成功", + "data": { + "id": 1, + // ... 其他字段同上 + } +} +``` + +## 开发规范 + +### 后端开发规范 +- 遵循DDD设计思想 +- 统一的异常处理 +- 统一的返回格式 +- 完整的单元测试 +- 详细的接口文档 +- 规范的代码注释 + +详细规范请参考:[后端开发规范](.cursorrules) + +### 前端开发规范 +- 统一的接口调用方式 +- 统一的错误处理 +- 标准的时间格式 +- 规范的国际化实现 + +详细规范请参考:[前端开发规范](frontend.rules) + +## 注意事项 + +### 1. 数据库 +- 所有表必须包含基础字段: + ```sql + id BIGINT PRIMARY KEY # 主键 + create_time DATETIME # 创建时间 + create_by VARCHAR(50) # 创建人 + update_time DATETIME # 更新时间 + update_by VARCHAR(50) # 更新人 + version INT # 版本号 + deleted BOOLEAN # 是否删除 + ``` +- 使用Flyway进行数据库版本控制 +- 新建表结构写入V1.0.0__init_schema.sql +- 初始数据写入V1.0.1__init_data.sql + +### 2. 接口开发 +- 所有接口都需要JWT认证 +- 接口版本统一使用v1 +- 返回数据格式统一为: + ```json + { + "success": true, + "code": 200, + "message": "操作成功", + "data": {} + } + ``` +- 分页查询参数统一使用: + ```json + { + "pageNum": 1, + "pageSize": 10, + "sortField": "createTime", + "sortOrder": "desc" + } + ``` + +### 3. 异常处理 +- 业务异常统一使用BusinessException +- 异常码在ResponseCode中定义 +- 异常信息在messages.properties中定义 +- 所有异常都通过GlobalExceptionHandler处理 + +### 4. 安全性 +- 敏感信息(密码、Token等)需要加密存储 +- 外部系统认证信息需要加密传输 +- 定期清理过期的Token +- 限制API调用频率 + +### 5. 测试 +- 所有Service层代码必须有单元测试 +- 所有Controller必须有集成测试 +- 测试覆盖率要求80%以上 +- 测试数据使用@Sql注入,避免影响生产数据 + +## 环境要求 +- JDK 17+ +- Maven 3.8+ +- MySQL 8.0+ +- Node.js 16+ + +## 快速开始 + +1. 克隆项目 +```bash +git clone [项目地址] +``` + +2. 配置数据库 +```bash +# 创建数据库 +CREATE DATABASE deploy_ease DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +3. 修改配置 +```yaml +# application.yml +spring: + datasource: + url: jdbc:mysql://localhost:3306/deploy_ease + username: your_username + password: your_password +``` + +4. 启动项目 +```bash +mvn spring-boot:run +``` + +## 贡献指南 +1. Fork 项目 +2. 创建功能分支 +3. 提交代码 +4. 创建Pull Request + +## 许可证 +[MIT License](LICENSE) +``` diff --git a/backend/pom.xml b/backend/pom.xml index d89a843c..039ea8fd 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -175,6 +175,21 @@ org.flywaydb flyway-mysql + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-junit-jupiter + test + + + org.assertj + assertj-core + test + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/api/ExternalSystemApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/api/ExternalSystemApiController.java new file mode 100644 index 00000000..582412a1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/api/ExternalSystemApiController.java @@ -0,0 +1,70 @@ +package com.qqchen.deploy.backend.api; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.model.ExternalSystemDTO; +import com.qqchen.deploy.backend.model.query.ExternalSystemQuery; +import com.qqchen.deploy.backend.service.IExternalSystemService; +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.web.bind.annotation.*; + +import java.util.List; + +/** + * 外部系统管理API控制器 + * 提供外部系统的REST接口,包括: + * 1. 基础CRUD操作 + * 2. 连接测试 + * 3. 数据同步 + * 4. 状态管理 + * + * @author QQChen + * @version 1.0.0 + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/external-system") +@Tag(name = "第三方系统管理API", description = "对外提供的第三方系统管理接口") +public class ExternalSystemApiController extends BaseController { + + @Resource + private IExternalSystemService externalSystemService; + + @Operation(summary = "测试连接") + @GetMapping("/{id}/test-connection") + public Response testConnection( + @Parameter(description = "系统ID", required = true) @PathVariable Long id + ) { + return Response.success(externalSystemService.testConnection(id)); + } + + @Operation(summary = "同步数据") + @PostMapping("/{id}/sync") + public Response syncData( + @Parameter(description = "系统ID", required = true) @PathVariable Long id + ) { + externalSystemService.syncData(id); + return Response.success(); + } + + @Operation(summary = "更新状态") + @PutMapping("/{id}/status") + public Response updateStatus( + @Parameter(description = "系统ID", required = true) @PathVariable Long id, + @Parameter(description = "是否启用", required = true) @RequestParam boolean enabled + ) { + externalSystemService.updateStatus(id, enabled); + return Response.success(); + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + // TODO: 实现导出功能 + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/converter/ExternalSystemConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/converter/ExternalSystemConverter.java new file mode 100644 index 00000000..1cbb6d52 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/converter/ExternalSystemConverter.java @@ -0,0 +1,15 @@ +package com.qqchen.deploy.backend.converter; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import com.qqchen.deploy.backend.model.ExternalSystemDTO; +import org.mapstruct.Mapper; + +/** + * 外部系统转换器 + */ +@Mapper(config = BaseConverter.class) +public interface ExternalSystemConverter extends BaseConverter { + // MapStruct会自动实现所有方法,包括从BaseConverter继承的方法 + // 由于ExternalSystem和ExternalSystemDTO的字段名完全匹配,无需额外的映射配置 +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/converter/MenuConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/converter/MenuConverter.java index 16b91a4c..696c5199 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/converter/MenuConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/converter/MenuConverter.java @@ -1,10 +1,12 @@ package com.qqchen.deploy.backend.converter; import com.qqchen.deploy.backend.entity.Menu; +import com.qqchen.deploy.backend.entity.Permission; import com.qqchen.deploy.backend.framework.converter.BaseConverter; import com.qqchen.deploy.backend.model.MenuDTO; import com.qqchen.deploy.backend.model.response.MenuPermissionTreeResponse; import com.qqchen.deploy.backend.model.response.MenuResponse; +import com.qqchen.deploy.backend.model.response.PermissionResponse; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -25,4 +27,8 @@ public interface MenuConverter extends BaseConverter { List toResponseList(List dtoList); MenuPermissionTreeResponse toMenuPermissionResponse(Menu menu); + + PermissionResponse toPermissionResponse(Permission permission); + + List toPermissionResponseList(List permissions); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/ExternalSystem.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/ExternalSystem.java new file mode 100644 index 00000000..d6fd92cd --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/entity/ExternalSystem.java @@ -0,0 +1,120 @@ +package com.qqchen.deploy.backend.entity; + +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "sys_external_system") +@LogicDelete +public class ExternalSystem extends Entity { + + /** + * 系统名称 + */ + @Column(nullable = false) + private String name; + + /** + * 系统类型:JENKINS/GIT/ZENTAO等 + */ + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private SystemType type; + + /** + * 系统访问地址 + */ + @Column(nullable = false) + private String url; + + /** + * 备注说明 + */ + @Column(columnDefinition = "TEXT") + private String remark; + + /** + * 排序 + */ + private Integer sort; + + /** + * 是否启用 + */ + private Boolean enabled = true; + + /** + * 认证方式:BASIC/TOKEN/OAUTH等 + */ + @Column(name = "auth_type", nullable = false) + @Enumerated(EnumType.STRING) + private AuthType authType; + + /** + * 用户名 + */ + private String username; + + /** + * 密码/密钥 + */ + private String password; + + /** + * 访问令牌 + */ + private String token; + + /** + * 最后同步状态 + */ + @Column(name = "sync_status") + @Enumerated(EnumType.STRING) + private SyncStatus syncStatus; + + /** + * 最后同步时间 + */ + @Column(name = "last_sync_time") + private LocalDateTime lastSyncTime; + + /** + * 系统特有配置,JSON格式 + */ + @Column(columnDefinition = "JSON") + private String config; + + /** + * 系统类型枚举 + */ + public enum SystemType { + JENKINS, + GIT, + ZENTAO + } + + /** + * 认证类型枚举 + */ + public enum AuthType { + BASIC, + TOKEN, + OAUTH + } + + /** + * 同步状态枚举 + */ + public enum SyncStatus { + SUCCESS, + FAILED, + RUNNING + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/Menu.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/Menu.java index 1f875bef..8a6f7baa 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/entity/Menu.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/entity/Menu.java @@ -75,10 +75,4 @@ public class Menu extends Entity { @ManyToMany(mappedBy = "menus") private Set roles = new HashSet<>(); - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable( - name = "sys_permission", - joinColumns = @JoinColumn(name = "menu_id") - ) - private Set permissions = new HashSet<>(); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index 08c0afb4..dee8f1db 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -63,7 +63,14 @@ public enum ResponseCode { PERMISSION_CODE_EXISTS(2401, "permission.code.exists"), PERMISSION_NAME_EXISTS(2402, "permission.name.exists"), PERMISSION_ALREADY_ASSIGNED(2403, "permission.already.assigned"), - PERMISSION_ASSIGN_FAILED(2404, "permission.assign.failed"); + PERMISSION_ASSIGN_FAILED(2404, "permission.assign.failed"), + + // 第三方系统相关错误码 (2500-2599) + EXTERNAL_SYSTEM_NAME_EXISTS(2500, "external.system.name.exists"), + EXTERNAL_SYSTEM_TYPE_URL_EXISTS(2501, "external.system.type.url.exists"), + EXTERNAL_SYSTEM_DISABLED(2502, "external.system.disabled"), + EXTERNAL_SYSTEM_SYNC_FAILED(2503, "external.system.sync.failed"), + EXTERNAL_SYSTEM_TYPE_NOT_SUPPORTED(2504, "external.system.type.not.supported"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/integration/IExternalSystemIntegration.java b/backend/src/main/java/com/qqchen/deploy/backend/integration/IExternalSystemIntegration.java new file mode 100644 index 00000000..f9ffb5fb --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/integration/IExternalSystemIntegration.java @@ -0,0 +1,30 @@ +package com.qqchen.deploy.backend.integration; + +import com.qqchen.deploy.backend.entity.ExternalSystem; + +/** + * 第三方系统集成接口 + * 定义与外部系统交互的标准接口,包括: + * 1. 连接测试 + * 2. 系统类型识别 + * + * @author QQChen + * @version 1.0.0 + */ +public interface IExternalSystemIntegration { + + /** + * 测试连接 + * + * @param system 系统配置 + * @return 是否连接成功 + */ + boolean testConnection(ExternalSystem system); + + /** + * 获取支持的系统类型 + * + * @return 系统类型 + */ + ExternalSystem.SystemType getSystemType(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/integration/impl/GitIntegration.java b/backend/src/main/java/com/qqchen/deploy/backend/integration/impl/GitIntegration.java new file mode 100644 index 00000000..4b85f9bb --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/integration/impl/GitIntegration.java @@ -0,0 +1,44 @@ +package com.qqchen.deploy.backend.integration.impl; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.integration.IExternalSystemIntegration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +public class GitIntegration implements IExternalSystemIntegration { + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public boolean testConnection(ExternalSystem system) { + try { + String url = system.getUrl() + "/api/v4/version"; // GitLab API + HttpHeaders headers = new HttpHeaders(); + if (system.getToken() != null) { + headers.set("PRIVATE-TOKEN", system.getToken()); + } + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + String.class + ); + + return response.getStatusCode() == HttpStatus.OK; + } catch (Exception e) { + log.error("Failed to connect to Git: {}", system.getUrl(), e); + return false; + } + } + + @Override + public ExternalSystem.SystemType getSystemType() { + return ExternalSystem.SystemType.GIT; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/integration/impl/JenkinsIntegration.java b/backend/src/main/java/com/qqchen/deploy/backend/integration/impl/JenkinsIntegration.java new file mode 100644 index 00000000..c89474df --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/integration/impl/JenkinsIntegration.java @@ -0,0 +1,57 @@ +package com.qqchen.deploy.backend.integration.impl; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.integration.IExternalSystemIntegration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Base64; + +@Slf4j +@Service +public class JenkinsIntegration implements IExternalSystemIntegration { + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public boolean testConnection(ExternalSystem system) { + try { + String url = system.getUrl() + "/api/json"; + HttpHeaders headers = createHeaders(system); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + String.class + ); + + return response.getStatusCode() == HttpStatus.OK; + } catch (Exception e) { + log.error("Failed to connect to Jenkins: {}", system.getUrl(), e); + return false; + } + } + + private HttpHeaders createHeaders(ExternalSystem system) { + HttpHeaders headers = new HttpHeaders(); + switch (system.getAuthType()) { + case BASIC -> { + String auth = system.getUsername() + ":" + system.getPassword(); + byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); + headers.set("Authorization", "Basic " + new String(encodedAuth)); + } + case TOKEN -> headers.set("Authorization", "Bearer " + system.getToken()); + case OAUTH -> headers.set("Authorization", "Bearer " + system.getToken()); + } + return headers; + } + + @Override + public ExternalSystem.SystemType getSystemType() { + return ExternalSystem.SystemType.JENKINS; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/ExternalSystemDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/model/ExternalSystemDTO.java new file mode 100644 index 00000000..51c223a3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/ExternalSystemDTO.java @@ -0,0 +1,47 @@ +package com.qqchen.deploy.backend.model; + +import com.qqchen.deploy.backend.entity.ExternalSystem.SystemType; +import com.qqchen.deploy.backend.entity.ExternalSystem.AuthType; +import com.qqchen.deploy.backend.entity.ExternalSystem.SyncStatus; +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalSystemDTO extends BaseDTO { + + @NotBlank(message = "系统名称不能为空") + private String name; + + @NotNull(message = "系统类型不能为空") + private SystemType type; + + @NotBlank(message = "系统访问地址不能为空") + private String url; + + private String remark; + + private Integer sort; + + private Boolean enabled = true; + + @NotNull(message = "认证方式不能为空") + private AuthType authType; + + private String username; + + private String password; + + private String token; + + private SyncStatus syncStatus; + + private LocalDateTime lastSyncTime; + + private String config; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/query/ExternalSystemQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/model/query/ExternalSystemQuery.java new file mode 100644 index 00000000..3c64b2d8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/query/ExternalSystemQuery.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.model.query; + +import com.qqchen.deploy.backend.entity.ExternalSystem.SystemType; +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 外部系统查询对象 + * 用于外部系统列表查询,支持以下查询条件: + * 1. 系统名称(模糊匹配) + * 2. 系统类型(精确匹配) + * 3. 启用状态(精确匹配) + * + * @author QQChen + * @version 1.0.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalSystemQuery extends BaseQuery { + + @QueryField(field = "name", type = QueryType.LIKE) + private String name; + + @QueryField(field = "type") + private SystemType type; + + @QueryField(field = "enabled") + private Boolean enabled; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/response/MenuPermissionTreeResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/model/response/MenuPermissionTreeResponse.java index cd691300..ea20c08a 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/model/response/MenuPermissionTreeResponse.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/response/MenuPermissionTreeResponse.java @@ -1,16 +1,30 @@ package com.qqchen.deploy.backend.model.response; -import com.qqchen.deploy.backend.model.PermissionDTO; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.EqualsAndHashCode; - import java.util.List; @Data @EqualsAndHashCode(callSuper = true) public class MenuPermissionTreeResponse extends MenuResponse { + /** - * 菜单下的权限列表 + * 子菜单列表(覆盖父类的children,因为类型不同) + */ + @JsonIgnore + @Override + public List getChildren() { + return super.getChildren(); + } + + /** + * 子菜单列表 + */ + private List permissionChildren; + + /** + * 权限列表 */ private List permissions; } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/response/PermissionResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/model/response/PermissionResponse.java index ee0f27a7..2b28eb98 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/model/response/PermissionResponse.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/response/PermissionResponse.java @@ -1,21 +1,31 @@ package com.qqchen.deploy.backend.model.response; -import com.qqchen.deploy.backend.framework.dto.BaseResponse; import lombok.Data; @Data -public class PermissionResponse extends BaseResponse { - - private Long menuId; - - private String menuName; - +public class PermissionResponse { + /** + * 权限ID + */ + private Long id; + + /** + * 权限编码 + */ private String code; - + + /** + * 权限名称 + */ private String name; - + + /** + * 权限类型 + */ private String type; - + + /** + * 排序 + */ private Integer sort; - } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IExternalSystemRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IExternalSystemRepository.java new file mode 100644 index 00000000..e1161650 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/repository/IExternalSystemRepository.java @@ -0,0 +1,26 @@ +package com.qqchen.deploy.backend.repository; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface IExternalSystemRepository extends IBaseRepository { + + /** + * 检查系统名称是否存在 + */ + boolean existsByNameAndDeletedFalse(String name); + + /** + * 检查系统类型和URL组合是否存在 + */ + boolean existsByTypeAndUrlAndDeletedFalse(ExternalSystem.SystemType type, String url); + + /** + * 查询所有未删除的系统,按排序字段排序 + */ + List findByDeletedFalseOrderBySort(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IPermissionRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IPermissionRepository.java index d53c9241..a7fcccca 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/repository/IPermissionRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/repository/IPermissionRepository.java @@ -15,4 +15,27 @@ public interface IPermissionRepository extends IBaseRepository List findAllEnabledOrderByMenuAndSort(); List findByIdIn(List ids); + + /** + * 查询所有未删除的权限,按排序字段排序 + * + * @return 权限列表 + */ + List findAllByDeletedFalseOrderBySort(); + + /** + * 根据菜单ID查询权限列表 + * + * @param menuId 菜单ID + * @return 权限列表 + */ + List findByMenuIdAndDeletedFalseOrderBySort(Long menuId); + + /** + * 检查权限编码是否存在 + * + * @param code 权限编码 + * @return 是否存在 + */ + boolean existsByCodeAndDeletedFalse(String code); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/IExternalSystemService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/IExternalSystemService.java new file mode 100644 index 00000000..a51146f3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/IExternalSystemService.java @@ -0,0 +1,31 @@ +package com.qqchen.deploy.backend.service; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.framework.service.IBaseService; +import com.qqchen.deploy.backend.model.ExternalSystemDTO; + +public interface IExternalSystemService extends IBaseService { + + /** + * 测试连接 + * + * @param id 系统ID + * @return 是否连接成功 + */ + boolean testConnection(Long id); + + /** + * 同步系统数据 + * + * @param id 系统ID + */ + void syncData(Long id); + + /** + * 启用/禁用系统 + * + * @param id 系统ID + * @param enabled 是否启用 + */ + void updateStatus(Long id, boolean enabled); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/IMenuService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/IMenuService.java index 4c83b68f..87509d04 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/IMenuService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/IMenuService.java @@ -22,10 +22,6 @@ public interface IMenuService extends IBaseService { */ List getUserMenus(); - /** - * 获取菜单树 - */ - List getTree(); List getPermissionTree(); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/ExternalSystemServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/ExternalSystemServiceImpl.java new file mode 100644 index 00000000..423e3ab8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/ExternalSystemServiceImpl.java @@ -0,0 +1,153 @@ +package com.qqchen.deploy.backend.service.impl; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +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.exception.UniqueConstraintException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import com.qqchen.deploy.backend.integration.IExternalSystemIntegration; +import com.qqchen.deploy.backend.model.ExternalSystemDTO; +import com.qqchen.deploy.backend.repository.IExternalSystemRepository; +import com.qqchen.deploy.backend.service.IExternalSystemService; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DATABASE; + +/** + * 外部系统服务实现类 + * 主要功能: + * 1. 外部系统的CRUD操作 + * 2. 系统连接测试 + * 3. 系统数据同步 + * 4. 系统状态管理 + * + * @author QQChen + * @version 1.0.0 + */ +@Slf4j +@Service +@ServiceType(DATABASE) +public class ExternalSystemServiceImpl extends BaseServiceImpl + implements IExternalSystemService { + + @Resource + private IExternalSystemRepository externalSystemRepository; + + @Resource + private List systemIntegrations; + + private Map integrationMap; + + @PostConstruct + public void init() { + integrationMap = systemIntegrations.stream() + .collect(Collectors.toMap( + IExternalSystemIntegration::getSystemType, + integration -> integration + )); + } + + @Override + protected void validateUniqueConstraints(ExternalSystemDTO dto) { + // 检查名称唯一性 + if (externalSystemRepository.existsByNameAndDeletedFalse(dto.getName())) { + throw new UniqueConstraintException(ResponseCode.EXTERNAL_SYSTEM_NAME_EXISTS, "name", dto.getName()); + } + + // 检查类型+URL组合唯一性 + if (externalSystemRepository.existsByTypeAndUrlAndDeletedFalse(dto.getType(), dto.getUrl())) { + throw new UniqueConstraintException(ResponseCode.EXTERNAL_SYSTEM_TYPE_URL_EXISTS, + "type and url", dto.getType() + ":" + dto.getUrl()); + } + } + + @Override + @Transactional(readOnly = true) + public boolean testConnection(Long id) { + ExternalSystem system = findEntityById(id); + if (!system.getEnabled()) { + throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_DISABLED); + } + + IExternalSystemIntegration integration = integrationMap.get(system.getType()); + if (integration == null) { + throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_TYPE_NOT_SUPPORTED); + } + + return integration.testConnection(system); + } + + @Override + @Transactional + public void syncData(Long id) { + ExternalSystem system = findEntityById(id); + if (!system.getEnabled()) { + throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_DISABLED); + } + + try { + system.setSyncStatus(ExternalSystem.SyncStatus.RUNNING); + externalSystemRepository.save(system); + + // TODO: 根据不同的系统类型调用不同的同步方法 + switch (system.getType()) { + case JENKINS -> syncJenkinsData(system); + case GIT -> syncGitData(system); + case ZENTAO -> syncZentaoData(system); + } + + system.setSyncStatus(ExternalSystem.SyncStatus.SUCCESS); + system.setLastSyncTime(LocalDateTime.now()); + } catch (Exception e) { + system.setSyncStatus(ExternalSystem.SyncStatus.FAILED); + log.error("Sync data failed for external system: {}", system.getName(), e); + throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_SYNC_FAILED); + } finally { + externalSystemRepository.save(system); + } + } + + @Override + @Transactional + public void updateStatus(Long id, boolean enabled) { + ExternalSystem system = findEntityById(id); + system.setEnabled(enabled); + externalSystemRepository.save(system); + } + + // 私有辅助方法,后续实现具体的连接测试逻辑 + private void testJenkinsConnection(ExternalSystem system) { + // TODO: 实现Jenkins连接测试 + } + + private void testGitConnection(ExternalSystem system) { + // TODO: 实现Git连接测试 + } + + private void testZentaoConnection(ExternalSystem system) { + // TODO: 实现禅道连接测试 + } + + // 私有辅助方法,后续实现具体的数据同步逻辑 + private void syncJenkinsData(ExternalSystem system) { + // TODO: 实现Jenkins数据同步 + } + + private void syncGitData(ExternalSystem system) { + // TODO: 实现Git数据同步 + } + + private void syncZentaoData(ExternalSystem system) { + // TODO: 实现禅道数据同步 + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/MenuServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/MenuServiceImpl.java index 0db1576c..467bd092 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/MenuServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/MenuServiceImpl.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.stream.Collectors; import java.util.Comparator; import java.util.HashMap; +import java.util.Optional; import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DATABASE; @@ -42,9 +43,6 @@ public class MenuServiceImpl extends BaseServiceImpl implem @Resource private MenuConverter menuConverter; - @Resource - private PermissionConverter permissionConverter; - @Resource private IUserService userService; @@ -78,23 +76,56 @@ public class MenuServiceImpl extends BaseServiceImpl implem return menuConverter.toResponseList(menuTree); } - @Override - public List getTree() { - List menus = menuRepository.findByDeletedFalseOrderBySort(); - return buildTree(menus); - } - @Override public List getPermissionTree() { + // 1. 获取所有菜单 List menus = menuRepository.findByDeletedFalseOrderBySort(); - List permissions = permissionRepository.findAllEnabledOrderByMenuAndSort(); - List tree = new ArrayList<>(); - menus.forEach(menu -> { - MenuPermissionTreeResponse menuPermissionResponse = menuConverter.toMenuPermissionResponse(menu); - List list = permissions.stream().filter(permission -> permission.getMenuId().equals(menu.getId())).toList(); - menuPermissionResponse.setPermissions(permissionConverter.toResponseList(list)); + + // 2. 获取所有权限 + List permissions = permissionRepository.findAllByDeletedFalseOrderBySort(); + + // 3. 构建权限Map (menuId -> List) + Map> permissionMap = permissions.stream() + .collect(Collectors.groupingBy(Permission::getMenuId)); + + // 4. 构建菜单树 + return buildPermissionTree(menus, permissionMap); + } + + private List buildPermissionTree(List menus, + Map> permissionMap) { + // 1. 转换所有菜单为树形节点 + List nodes = menus.stream() + .map(menu -> convertToTreeNode(menu, permissionMap)) + .collect(Collectors.toList()); + + // 2. 构建父子关系 + Map> childrenMap = nodes.stream() + .collect(Collectors.groupingBy(node -> + Optional.ofNullable(node.getParentId()).orElse(0L))); + + // 3. 设置子节点并排序 + nodes.forEach(node -> { + List children = childrenMap.getOrDefault(node.getId(), new ArrayList<>()); + children.sort(Comparator.comparing(MenuPermissionTreeResponse::getSort)); + node.setPermissionChildren(children); }); - return null; + + // 4. 返回排序后的顶层节点 + List rootNodes = childrenMap.getOrDefault(0L, new ArrayList<>()); + rootNodes.sort(Comparator.comparing(MenuPermissionTreeResponse::getSort)); + return rootNodes; + } + + private MenuPermissionTreeResponse convertToTreeNode(Menu menu, + Map> permissionMap) { + MenuPermissionTreeResponse node = menuConverter.toMenuPermissionResponse(menu); + + // 设置权限列表 + List permissions = permissionMap.getOrDefault(menu.getId(), new ArrayList<>()); + node.setPermissions(menuConverter.toPermissionResponseList(permissions)); + + return node; } private List buildTree(List menus) { @@ -158,10 +189,8 @@ public class MenuServiceImpl extends BaseServiceImpl implem if (menus == null || menus.isEmpty()) { return; } - // 根据sort字段排序 menus.sort(Comparator.comparing(MenuDTO::getSort)); - // 递归排序子菜单 for (MenuDTO menu : menus) { if (menu.getChildren() != null && !menu.getChildren().isEmpty()) { diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index d9fc8e85..c87bbd5d 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -1,258 +1,275 @@ --- 创建租户表 +-- -------------------------------------------------------------------------------------- +-- 系统基础表 +-- -------------------------------------------------------------------------------------- + +-- 租户表 CREATE TABLE IF NOT EXISTS sys_tenant ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255) NULL, - create_time DATETIME(6) NULL, - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255) NULL, - update_time DATETIME(6) NULL, - version INT NOT NULL DEFAULT 0, - address VARCHAR(255) NULL, - code VARCHAR(255) NOT NULL, - contact_name VARCHAR(255) NULL, - contact_phone VARCHAR(255) NULL, - email VARCHAR(255) NULL, - enabled BIT NOT NULL DEFAULT 1, - name VARCHAR(255) NOT NULL, - CONSTRAINT UK_tenant_code UNIQUE (code) -); - --- 创建部门表 -CREATE TABLE sys_department ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255) NULL, - create_time DATETIME(6) NULL, - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255) NULL, - update_time DATETIME(6) NULL, - version INT NOT NULL DEFAULT 0, - code VARCHAR(255) NOT NULL, - description VARCHAR(255) NULL, - enabled BIT NOT NULL DEFAULT 1, - leader_id BIGINT NULL, - leader_name VARCHAR(255) NULL, - name VARCHAR(255) NOT NULL, - parent_id BIGINT NULL, - sort INT NOT NULL DEFAULT 0, - CONSTRAINT UK_department_code UNIQUE (code) -); - --- 创建用户表 -CREATE TABLE IF NOT EXISTS sys_user ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255) NULL, - create_time DATETIME(6) NULL, - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255) NULL, - update_time DATETIME(6) NULL, - version INT NOT NULL DEFAULT 0, - email VARCHAR(255) NULL, - enabled BIT NOT NULL DEFAULT 1, - nickname VARCHAR(50) NULL, - password VARCHAR(255) NOT NULL, - phone VARCHAR(255) NULL, - username VARCHAR(255) NOT NULL, - department_id BIGINT NULL, - CONSTRAINT UK_user_username UNIQUE (username) --- CONSTRAINT FK_user_department FOREIGN KEY (department_id) REFERENCES sys_department(id) -); - --- 创建系统参数表 -CREATE TABLE sys_param ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255), - create_time DATETIME(6), - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255), - update_time DATETIME(6), - version INT NOT NULL DEFAULT 0, + 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 '参数名称', - value TEXT COMMENT '参数值', - type VARCHAR(50) NOT NULL COMMENT '参数类型', - description VARCHAR(255) COMMENT '参数描述', - enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', + code VARCHAR(50) NOT NULL COMMENT '租户编码', + name VARCHAR(100) NOT NULL COMMENT '租户名称', + address VARCHAR(255) NULL COMMENT '租户地址', + contact_name VARCHAR(50) NULL COMMENT '联系人姓名', + contact_phone VARCHAR(20) NULL COMMENT '联系人电话', + email VARCHAR(100) NULL COMMENT '联系人邮箱', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', + + CONSTRAINT UK_tenant_code UNIQUE (code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表'; + +-- 部门表 +CREATE TABLE sys_department ( + 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(50) NOT NULL COMMENT '部门编码', + name VARCHAR(100) NOT NULL COMMENT '部门名称', + description VARCHAR(255) NULL COMMENT '部门描述', + leader_id BIGINT NULL COMMENT '部门负责人ID', + leader_name VARCHAR(50) NULL COMMENT '部门负责人姓名', + parent_id BIGINT NULL COMMENT '父部门ID', + sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', + + CONSTRAINT UK_department_code UNIQUE (code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表'; + +-- 用户表 +CREATE TABLE IF NOT EXISTS sys_user ( + 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 '乐观锁版本号', + + username VARCHAR(50) NOT NULL COMMENT '用户名', + password VARCHAR(255) NOT NULL COMMENT '密码', + nickname VARCHAR(50) NULL COMMENT '昵称', + email VARCHAR(100) NULL COMMENT '邮箱', + phone VARCHAR(20) NULL COMMENT '手机号', + department_id BIGINT NULL COMMENT '所属部门ID', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', + + CONSTRAINT UK_user_username UNIQUE (username), + CONSTRAINT FK_user_department FOREIGN KEY (department_id) REFERENCES sys_department(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- 系统参数表 +CREATE TABLE sys_param ( + 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 '参数名称', + value TEXT COMMENT '参数值', + type VARCHAR(50) NOT NULL COMMENT '参数类型', + description VARCHAR(255) NULL COMMENT '参数描述', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', CONSTRAINT UK_sys_param_code UNIQUE (code) -) COMMENT '系统参数表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统参数表'; + +-- -------------------------------------------------------------------------------------- +-- 权限管理相关表 +-- -------------------------------------------------------------------------------------- -- 菜单表 CREATE TABLE sys_menu ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255), - create_time DATETIME(6), - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255), - update_time DATETIME(6), - version INT NOT NULL DEFAULT 0, + 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 '乐观锁版本号', - name VARCHAR(100) NOT NULL COMMENT '菜单名称', - path VARCHAR(200) COMMENT '路由路径', - component VARCHAR(255) COMMENT '组件路径', - icon VARCHAR(100) COMMENT '菜单图标', - permission VARCHAR(100) COMMENT '权限标识', - type INT NOT NULL COMMENT '菜单类型(1:目录 2:菜单 3:按钮)', - parent_id BIGINT COMMENT '父菜单ID', - sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序', - hidden BIT NOT NULL DEFAULT 0 COMMENT '是否隐藏(0显示 1隐藏)', - enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0禁用 1启用)' -) COMMENT '菜单表'; + name VARCHAR(100) NOT NULL COMMENT '菜单名称', + path VARCHAR(200) NULL COMMENT '路由路径', + component VARCHAR(255) NULL COMMENT '组件路径', + icon VARCHAR(100) NULL COMMENT '菜单图标', + permission VARCHAR(100) NULL COMMENT '权限标识', + type TINYINT NOT NULL COMMENT '菜单类型(1:目录 2:菜单 3:按钮)', + parent_id BIGINT NULL COMMENT '父菜单ID', + sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序', + hidden BIT NOT NULL DEFAULT 0 COMMENT '是否隐藏(0:显示,1:隐藏)', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', + + INDEX IDX_parent_id (parent_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表'; -- 角色表 CREATE TABLE sys_role ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255), - create_time DATETIME(6), - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255), - update_time DATETIME(6), - version INT NOT NULL DEFAULT 0, + 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 '角色名称', - type INT NOT NULL DEFAULT 2 COMMENT '角色类型(1:系统角色 2:自定义角色)', - description VARCHAR(255) COMMENT '角色描述', + type TINYINT NOT NULL DEFAULT 2 COMMENT '角色类型(1:系统角色,2:自定义角色)', + description VARCHAR(255) NULL COMMENT '角色描述', sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序', - + CONSTRAINT UK_role_code UNIQUE (code) -) COMMENT '角色表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'; -- 角色标签表 CREATE TABLE sys_role_tag ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255), - create_time DATETIME(6), - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255), - update_time DATETIME(6), - version INT NOT NULL DEFAULT 0, + 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 '乐观锁版本号', name VARCHAR(50) NOT NULL COMMENT '标签名称', - color VARCHAR(20) COMMENT '标签颜色' -) COMMENT '角色标签表'; + color VARCHAR(20) NULL COMMENT '标签颜色(十六进制颜色码)' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表'; -- 角色标签关联表 CREATE TABLE sys_role_tag_relation ( role_id BIGINT NOT NULL COMMENT '角色ID', tag_id BIGINT NOT NULL COMMENT '标签ID', - PRIMARY KEY (role_id, tag_id) --- CONSTRAINT FK_role_tag_role FOREIGN KEY (role_id) REFERENCES sys_role (id), --- CONSTRAINT FK_role_tag_tag FOREIGN KEY (tag_id) REFERENCES sys_role_tag (id) -) COMMENT '角色标签关联表'; + + PRIMARY KEY (role_id, tag_id), + CONSTRAINT FK_role_tag_role FOREIGN KEY (role_id) REFERENCES sys_role (id), + CONSTRAINT FK_role_tag_tag FOREIGN KEY (tag_id) REFERENCES sys_role_tag (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签关联表'; -- 用户角色关联表 CREATE TABLE sys_user_role ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255), - create_time DATETIME(6), - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255), - update_time DATETIME(6), - version INT NOT NULL DEFAULT 0, + 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 '乐观锁版本号', user_id BIGINT NOT NULL COMMENT '用户ID', role_id BIGINT NOT NULL COMMENT '角色ID', - CONSTRAINT UK_user_role UNIQUE (user_id, role_id) --- CONSTRAINT FK_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user (id), --- CONSTRAINT FK_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role (id) -) COMMENT '用户角色关联表'; + CONSTRAINT UK_user_role UNIQUE (user_id, role_id), + CONSTRAINT FK_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user (id), + CONSTRAINT FK_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表'; -- 角色菜单关联表 CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL COMMENT '角色ID', menu_id BIGINT NOT NULL COMMENT '菜单ID', - PRIMARY KEY (role_id, menu_id) --- CONSTRAINT FK_role_menu_role FOREIGN KEY (role_id) REFERENCES sys_role (id), --- CONSTRAINT FK_role_menu_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id) -) COMMENT '角色菜单关联表'; + + PRIMARY KEY (role_id, menu_id), + CONSTRAINT FK_role_menu_role FOREIGN KEY (role_id) REFERENCES sys_role (id), + CONSTRAINT FK_role_menu_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单关联表'; -- 权限模板表 CREATE TABLE sys_permission_template ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255), - create_time DATETIME(6), - deleted BIT NOT NULL DEFAULT 0, - update_by VARCHAR(255), - update_time DATETIME(6), - version INT NOT NULL DEFAULT 0, + 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 '模板名称', - type INT NOT NULL DEFAULT 1 COMMENT '模板类型(1:系统模板 2:自定义模板)', - description VARCHAR(255) COMMENT '模板描述', - enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', + type TINYINT NOT NULL DEFAULT 1 COMMENT '模板类型(1:系统模板,2:自定义模板)', + description VARCHAR(255) NULL COMMENT '模板描述', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', CONSTRAINT UK_template_code UNIQUE (code) -) COMMENT '权限模板表'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限模板表'; -- 模板菜单关联表 CREATE TABLE sys_template_menu ( template_id BIGINT NOT NULL COMMENT '模板ID', menu_id BIGINT NOT NULL COMMENT '菜单ID', - PRIMARY KEY (template_id, menu_id) --- CONSTRAINT FK_template_menu_template FOREIGN KEY (template_id) REFERENCES sys_permission_template (id), --- CONSTRAINT FK_template_menu_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id) -) COMMENT '模板菜单关联表'; + + PRIMARY KEY (template_id, menu_id), + CONSTRAINT FK_template_menu_template FOREIGN KEY (template_id) REFERENCES sys_permission_template (id), + CONSTRAINT FK_template_menu_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='模板菜单关联表'; --- 创建权限表 +-- 权限表 CREATE TABLE sys_permission ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255) NULL, - create_time DATETIME(6) NULL, - deleted BIT NOT NULL, - update_by VARCHAR(255) NULL, - update_time DATETIME(6) NULL, - version INT NOT NULL, + 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 '乐观锁版本号', - menu_id BIGINT NOT NULL COMMENT '关联的菜单ID', - code VARCHAR(100) NOT NULL COMMENT '权限编码', - name VARCHAR(100) NOT NULL COMMENT '权限名称', - type VARCHAR(50) NOT NULL DEFAULT 'FUNCTION' COMMENT '权限类型:MENU/FUNCTION/API', - sort INT NULL COMMENT '排序', + menu_id BIGINT NOT NULL COMMENT '关联的菜单ID', + code VARCHAR(100) NOT NULL COMMENT '权限编码', + name VARCHAR(100) NOT NULL COMMENT '权限名称', + type VARCHAR(50) NOT NULL DEFAULT 'FUNCTION' COMMENT '权限类型(MENU:菜单,FUNCTION:功能,API:接口)', + sort INT NULL DEFAULT 0 COMMENT '排序', - UNIQUE KEY UK_CODE (CODE), - KEY IDX_MENU_ID (MENU_ID) -) COMMENT '系统权限表'; + CONSTRAINT UK_permission_code UNIQUE (code), + CONSTRAINT FK_permission_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id), + INDEX IDX_menu_id (menu_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统权限表'; --- 创建三方系统表 +-- -------------------------------------------------------------------------------------- +-- 外部系统集成相关表 +-- -------------------------------------------------------------------------------------- + +-- 外部系统表 CREATE TABLE sys_external_system ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - create_by VARCHAR(255) NULL, - create_time DATETIME(6) NULL, - deleted BIT NOT NULL, - update_by VARCHAR(255) NULL, - update_time DATETIME(6) NULL, - version INT NOT NULL, + 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 '乐观锁版本号', - name VARCHAR(255) NOT NULL COMMENT '系统名称', - type VARCHAR(50) NOT NULL COMMENT '系统类型:JENKINS/GIT/ZENTAO等', - url VARCHAR(255) NOT NULL COMMENT '系统访问地址', - remark TEXT NULL COMMENT '备注说明', - sort INT NULL COMMENT '排序', - enabled BIT NOT NULL DEFAULT TRUE COMMENT '是否启用', + name VARCHAR(100) NOT NULL COMMENT '系统名称', + type VARCHAR(50) NOT NULL COMMENT '系统类型(JENKINS/GIT/ZENTAO等)', + url VARCHAR(255) NOT NULL COMMENT '系统访问地址', + remark TEXT NULL COMMENT '备注说明', + sort INT NULL DEFAULT 0 COMMENT '排序', + enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)', - auth_type VARCHAR(50) NOT NULL COMMENT '认证方式:BASIC/TOKEN/OAUTH等', - username VARCHAR(255) NULL COMMENT '用户名', - password VARCHAR(255) NULL COMMENT '密码/密钥', - token VARCHAR(255) NULL COMMENT '访问令牌', + auth_type VARCHAR(50) NOT NULL COMMENT '认证方式(BASIC:基础认证,TOKEN:令牌认证,OAUTH:OAuth认证)', + username VARCHAR(100) NULL COMMENT '用户名', + password VARCHAR(255) NULL COMMENT '密码/密钥', + token VARCHAR(255) NULL COMMENT '访问令牌', - sync_status ENUM ('SUCCESS', 'FAILED', 'RUNNING') NULL COMMENT '最后同步状态', - last_sync_time DATETIME(6) NULL COMMENT '最后同步时间', + sync_status ENUM ('SUCCESS', 'FAILED', 'RUNNING') NULL COMMENT '最后同步状态', + last_sync_time DATETIME(6) NULL COMMENT '最后同步时间', - config JSON NULL COMMENT '系统特有配置,JSON格式', + config JSON NULL COMMENT '系统特有配置(JSON格式)', - UNIQUE KEY UK_NAME (NAME), - UNIQUE KEY UK_TYPE_URL (TYPE, URL) -) COMMENT '第三方系统配置'; - --- 创建角色权限关联表 -CREATE TABLE sys_role_permission ( - role_id BIGINT NOT NULL COMMENT '角色ID', - permission_id BIGINT NOT NULL COMMENT '权限ID', - PRIMARY KEY (role_id, permission_id) --- CONSTRAINT FK_role_permission_role FOREIGN KEY (role_id) REFERENCES sys_role (id), --- CONSTRAINT FK_role_permission_permission FOREIGN KEY (permission_id) REFERENCES sys_permission (id) -) COMMENT '角色权限关联表'; + CONSTRAINT UK_external_system_name UNIQUE (name), + CONSTRAINT UK_external_system_type_url UNIQUE (type, url), + INDEX IDX_type (type), + INDEX IDX_enabled (enabled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='外部系统配置表'; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql index fe8feaa2..47c2a0e8 100644 --- a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql +++ b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql @@ -1,26 +1,43 @@ --- 初始化租户 -INSERT INTO sys_tenant -(create_by, create_time, deleted, update_by, update_time, version, - address, code, contact_name, contact_phone, email, enabled, name) -VALUES ('system', '2024-01-01 00:00:00', 0, 'system', '2024-01-01 00:00:00', 0, - '北京市朝阳区望京SOHO T1 C座', 'default', '张三', '13900000001', - 'admin@deploy-ease.com', 1, '默认租户'); +-- -------------------------------------------------------------------------------------- +-- 初始化系统基础数据 +-- -------------------------------------------------------------------------------------- + +-- 初始化租户数据 +INSERT INTO sys_tenant (id, create_time, code, name, address, contact_name, contact_phone, email, enabled) +VALUES (1, NOW(), 'admin', '系统管理租户', '北京市朝阳区', '管理员', '13800138000', 'admin@system.com', 1); + +-- 初始化部门数据 +INSERT INTO sys_department (id, create_time, code, name, description, sort, enabled) +VALUES (1, NOW(), 'ROOT', '根部门', '系统根部门', 0, 1); + +INSERT INTO sys_department (id, create_time, code, name, description, parent_id, sort, enabled) +VALUES +(2, NOW(), 'IT', '信息技术部', '负责公司IT系统的规划、建设和运维', 1, 1, 1), +(3, NOW(), 'DEV', '研发部', '负责产品研发和技术创新', 1, 2, 1), +(4, NOW(), 'OPS', '运维部', '负责系统运维和技术支持', 1, 3, 1); + +-- 初始化用户数据(密码统一为:123456) +INSERT INTO sys_user (id, create_time, username, password, nickname, email, phone, department_id, enabled) +VALUES +(1, NOW(), 'admin', '$2a$10$viWVqfZwQxViLQDk7hhVg.ENYT.3zUFf.aBetlarImKPSS0V2gbSa', '超级管理员', 'admin@system.com', '13800138000', 1, 1), +(2, NOW(), 'it_manager', '$2a$10$viWVqfZwQxViLQDk7hhVg.ENYT.3zUFf.aBetlarImKPSS0V2gbSa', 'IT经理', 'it@system.com', '13800138001', 2, 1), +(3, NOW(), 'dev_manager', '$2a$10$viWVqfZwQxViLQDk7hhVg.ENYT.3zUFf.aBetlarImKPSS0V2gbSa', '研发经理', 'dev@system.com', '13800138002', 3, 1), +(4, NOW(), 'ops_manager', '$2a$10$viWVqfZwQxViLQDk7hhVg.ENYT.3zUFf.aBetlarImKPSS0V2gbSa', '运维经理', 'ops@system.com', '13800138003', 4, 1); -- 初始化系统参数 -INSERT INTO sys_param (id, code, name, value, type, description, enabled, create_by, create_time, version, deleted) -VALUES (1, 'USER_STATUS_ENUM', '用户状态枚举', '[{"code":"0","name":"禁用"},{"code":"1","name":"启用"}]', 'ENUM', '用户状态枚举值', TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE); +INSERT INTO sys_param (id, create_time, code, name, value, type, description, enabled) +VALUES +(1, NOW(), 'SYSTEM_NAME', '系统名称', 'Deploy Ease Platform', 'STRING', '系统显示名称', 1), +(2, NOW(), 'SYSTEM_LOGO', '系统Logo', '/static/logo.png', 'STRING', '系统Logo路径', 1), +(3, NOW(), 'LOGIN_BACKGROUND', '登录背景', '/static/login-bg.jpg', 'STRING', '登录页面背景图片', 1); --- 初始化角色标签 -INSERT INTO sys_role_tag (id, name, color, create_by, create_time, deleted, version) -VALUES (1, '研发', '#1890FF', 'system', '2024-01-01 00:00:00', FALSE, 0), - (2, '运维', '#52C41A', 'system', '2024-01-01 00:00:00', FALSE, 0), - (3, '安全', '#FF4D4F', 'system', '2024-01-01 00:00:00', FALSE, 0), - (4, '临时', '#FAAD14', 'system', '2024-01-01 00:00:00', FALSE, 0), - (5, '外部', '#722ED1', 'system', '2024-01-01 00:00:00', FALSE, 0); +-- -------------------------------------------------------------------------------------- +-- 初始化权限管理数据 +-- -------------------------------------------------------------------------------------- -- 初始化菜单数据 INSERT INTO sys_menu (id, name, path, component, icon, type, parent_id, sort, hidden, enabled, create_by, create_time, version, deleted) -VALUES +VALUES -- 系统管理 (1, '系统管理', '/system', 'LAYOUT', 'setting', 1, NULL, 1, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE), -- 用户管理 @@ -35,128 +52,88 @@ VALUES (70, '三方系统', '/system/external', '/System/External/index', 'api', 2, 1, 70, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE); -- 初始化角色数据 -INSERT INTO sys_role (id, code, name, type, description, sort, create_by, create_time, version, deleted) -VALUES (1, 'SUPER_ADMIN', '超级管理员', 1, '系统超级管理员', 1, 'system', '2024-01-01 00:00:00', 0, FALSE), - (2, 'DEV_MANAGER', '开发主管', 2, '开发团队主管', 2, 'system', '2024-01-01 00:00:00', 0, FALSE), - (3, 'OPS_MANAGER', '运维主管', 2, '运维团队主管', 3, 'system', '2024-01-01 00:00:00', 0, FALSE); +INSERT INTO sys_role (id, create_time, code, name, type, description, sort) +VALUES +(1, NOW(), 'SUPER_ADMIN', '超级管理员', 1, '系统超级管理员,拥有所有权限', 1), +(2, NOW(), 'SYSTEM_ADMIN', '系统管理员', 1, '系统管理员,拥有大部分系统管理权限', 2), +(3, NOW(), 'COMMON_USER', '普通用户', 2, '普通用户,仅拥有基本操作权限', 3); --- 初始化部门数据 -INSERT INTO sys_department (id, code, name, description, enabled, sort, parent_id, create_by, create_time, version, deleted) -VALUES (1, 'TECH', '技术部', '技术研发部门', TRUE, 1, NULL, 'system', '2024-01-01 00:00:00', 0, FALSE), - (2, 'DEV', '研发组', '研发团队', TRUE, 1, 1, 'system', '2024-01-01 00:00:00', 0, FALSE), - (3, 'OPS', '运维组', '运维团队', TRUE, 2, 1, 'system', '2024-01-01 00:00:00', 0, FALSE); +-- 初始化角色标签 +INSERT INTO sys_role_tag (id, create_time, name, color) +VALUES +(1, NOW(), '系统内置', '#ff4d4f'), +(2, NOW(), '重要角色', '#ffa940'), +(3, NOW(), '普通角色', '#73d13d'); --- 初始化用户数据 -INSERT INTO sys_user (id, username, password, nickname, email, phone, enabled, department_id, create_by, create_time, version, deleted) -VALUES (1, 'admin', '$2a$10$VTbMVv3M.gVaMcLsELtBZuHxGrHyFqf3CYfSFQhcBn0A6pBTvThSy', '系统管理员', 'admin@example.com', '13800138000', TRUE, NULL, 'system', '2024-01-01 00:00:00', 0, FALSE), - (2, 'dev_manager', '$2a$10$VTbMVv3M.gVaMcLsELtBZuHxGrHyFqf3CYfSFQhcBn0A6pBTvThSy', '开发主管', 'dev@example.com', '13800138001', TRUE, 2, 'system', '2024-01-01 00:00:00', 0, FALSE), - (3, 'ops_manager', '$2a$10$VTbMVv3M.gVaMcLsELtBZuHxGrHyFqf3CYfSFQhcBn0A6pBTvThSy', '运维主管', 'ops@example.com', '13800138002', TRUE, 3, 'system', '2024-01-01 00:00:00', 0, FALSE); - --- 初始化用户角色关联数据 -INSERT INTO sys_user_role (user_id, role_id, create_by, create_time, version, deleted) -VALUES (1, 1, 'system', '2024-01-01 00:00:00', 0, FALSE), -- 超级管理员 -> 超级管理员角色 - (2, 2, 'system', '2024-01-01 00:00:00', 0, FALSE), -- 开发主管 -> 开发主管角色 - (3, 3, 'system', '2024-01-01 00:00:00', 0, FALSE); --- 运维主管 -> 运维主管角色 - --- 初始化角色标签关联数据 +-- 初始化角色标签关联 INSERT INTO sys_role_tag_relation (role_id, tag_id) -VALUES (1, 3), -- 超级管理员 -> 安全标签 - (2, 1), -- 开发主管 -> 研发标签 - (3, 2); --- 运维主管 -> 运维标签 - --- 初始化角色菜单关联数据 -INSERT INTO sys_role_menu (role_id, menu_id) -VALUES --- 超级管理员拥有所有菜单权限 +VALUES (1, 1), -(1, 2), -(1, 3), -(1, 4), -(1, 5), -(1, 70), --- 开发主管权限 -(2, 70), --- 运维主管权限 -(3, 70); +(2, 1), +(2, 2), +(3, 3); + +-- 初始化用户角色关联 +INSERT INTO sys_user_role (id, create_time, user_id, role_id) +VALUES +(1, NOW(), 1, 1), +(2, NOW(), 2, 2), +(3, NOW(), 3, 2), +(4, NOW(), 4, 3); + +-- 初始化角色菜单关联 +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, id FROM sys_menu; -- 超级管理员拥有所有菜单权限 + +INSERT INTO sys_role_menu (role_id, menu_id) +VALUES +(2, 1), (2, 2), (2, 3), (2, 4), (2, 5), -- 系统管理员拥有系统管理相关权限 +(3, 70); -- 普通用户拥有三方系统权限 + +-- 初始化权限模板 +INSERT INTO sys_permission_template (id, create_time, code, name, type, description, enabled) +VALUES +(1, NOW(), 'FULL_PERMISSION', '完整权限模板', 1, '包含所有系统权限的模板', 1), +(2, NOW(), 'BASIC_PERMISSION', '基础权限模板', 1, '包含基本操作权限的模板', 1); + +-- 初始化模板菜单关联 +INSERT INTO sys_template_menu (template_id, menu_id) +SELECT 1, id FROM sys_menu; -- 完整权限模板关联所有菜单 + +INSERT INTO sys_template_menu (template_id, menu_id) +VALUES (2, 70); -- 基础权限模板关联三方系统菜单 -- 初始化权限数据 -INSERT INTO sys_permission (id, menu_id, code, name, type, sort, create_by, create_time, update_by, update_time, version, deleted) -VALUES +INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) +VALUES -- 用户管理权限 -(21, 2, 'system:user:add', '用户新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(22, 2, 'system:user:edit', '用户编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(23, 2, 'system:user:delete', '用户删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(24, 2, 'system:user:reset-password', '重置密码', 'FUNCTION', 4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(25, 2, 'system:user:assign-roles', '分配角色', 'FUNCTION', 5, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), +(1, NOW(), 2, 'system:user:list', '用户列表', 'FUNCTION', 1), +(2, NOW(), 2, 'system:user:create', '用户创建', 'FUNCTION', 2), +(3, NOW(), 2, 'system:user:update', '用户修改', 'FUNCTION', 3), +(4, NOW(), 2, 'system:user:delete', '用户删除', 'FUNCTION', 4), -- 角色管理权限 -(31, 3, 'system:role:add', '角色新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(32, 3, 'system:role:edit', '角色编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(33, 3, 'system:role:delete', '角色删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(34, 3, 'system:role:assign-tags', '分配标签', 'FUNCTION', 4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(35, 3, 'system:role:assign-permissions', '分配权限', 'FUNCTION', 5, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(36, 3, 'system:role:permission-list', '权限列表', 'FUNCTION', 6, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), +(5, NOW(), 3, 'system:role:list', '角色列表', 'FUNCTION', 1), +(6, NOW(), 3, 'system:role:create', '角色创建', 'FUNCTION', 2), +(7, NOW(), 3, 'system:role:update', '角色修改', 'FUNCTION', 3), +(8, NOW(), 3, 'system:role:delete', '角色删除', 'FUNCTION', 4), --- 菜单管理权限 -(41, 4, 'system:menu:add', '菜单新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(42, 4, 'system:menu:edit', '菜单编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(43, 4, 'system:menu:delete', '菜单删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), +-- 三方系统权限 +(9, NOW(), 70, 'system:external:list', '三方系统列表', 'FUNCTION', 1), +(10, NOW(), 70, 'system:external:create', '三方系统创建', 'FUNCTION', 2), +(11, NOW(), 70, 'system:external:update', '三方系统修改', 'FUNCTION', 3), +(12, NOW(), 70, 'system:external:delete', '三方系统删除', 'FUNCTION', 4), +(13, NOW(), 70, 'system:external:test', '连接测试', 'FUNCTION', 5), +(14, NOW(), 70, 'system:external:sync', '数据同步', 'FUNCTION', 6); --- 部门管理权限 -(51, 5, 'system:department:add', '部门新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(52, 5, 'system:department:edit', '部门编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(53, 5, 'system:department:delete', '部门删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), +-- -------------------------------------------------------------------------------------- +-- 初始化外部系统数据 +-- -------------------------------------------------------------------------------------- --- 三方系统管理权限 -(71, 70, 'system:external:list', '查看三方系统', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(72, 70, 'system:external:create', '新增三方系统', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(73, 70, 'system:external:update', '编辑三方系统', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(74, 70, 'system:external:delete', '删除三方系统', 'FUNCTION', 4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), -(75, 70, 'system:external:sync', '同步三方系统', 'FUNCTION', 5, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE); - --- 初始化角色权限关联数据 -INSERT INTO sys_role_permission (role_id, permission_id) -VALUES --- 用户管理权限 -(1, 21), -(1, 22), -(1, 23), -(1, 24), -(1, 25), --- 角色管理权限(包含权限管理功能) -(1, 31), -(1, 32), -(1, 33), -(1, 34), -(1, 35), -(1, 36), --- 菜单管理权限 -(1, 41), -(1, 42), -(1, 43), --- 部门管理权限 -(1, 51), -(1, 52), -(1, 53), --- 三方系统管理权限 -(1, 71), -(1, 72), -(1, 73), -(1, 74), -(1, 75); - --- 初始化三方系统数据 -INSERT INTO sys_external_system (id, name, type, url, auth_type, username, password, enabled, sort, create_by, create_time, version, deleted, remark) -VALUES --- Jenkins示例 -(1, 'Jenkins-开发环境', 'JENKINS', 'http://jenkins-dev.example.com', 'BASIC', 'admin', 'password123', TRUE, 1, 'system', '2024-01-01 00:00:00', 0, FALSE, 'Jenkins开发环境'), -(2, 'Jenkins-测试环境', 'JENKINS', 'http://jenkins-test.example.com', 'BASIC', 'admin', 'password123', TRUE, 2, 'system', '2024-01-01 00:00:00', 0, FALSE, 'Jenkins测试环境'), - --- Git仓库示例 -(3, 'GitLab-主库', 'GIT', 'http://gitlab.example.com', 'TOKEN', NULL, NULL, TRUE, 3, 'system', '2024-01-01 00:00:00', 0, FALSE, '公司GitLab主库'), -(4, 'GitHub', 'GIT', 'https://github.com', 'OAUTH', NULL, NULL, TRUE, 4, 'system', '2024-01-01 00:00:00', 0, FALSE, 'GitHub仓库'), - --- 禅道示例 -(5, '禅道-项目管理', 'ZENTAO', 'http://zentao.example.com', 'BASIC', 'admin', 'password123', TRUE, 5, 'system', '2024-01-01 00:00:00', 0, FALSE, '禅道项目管理系统'); \ No newline at end of file +-- 初始化外部系统 +INSERT INTO sys_external_system (id, create_time, name, type, url, auth_type, username, password, enabled, sort) +VALUES +(1, NOW(), 'Jenkins测试环境', 'JENKINS', 'http://jenkins-test.example.com', 'BASIC', 'admin', 'jenkins123', 1, 1), +(2, NOW(), 'Jenkins生产环境', 'JENKINS', 'http://jenkins-prod.example.com', 'BASIC', 'admin', 'jenkins123', 1, 2), +(3, NOW(), 'GitLab', 'GIT', 'http://gitlab.example.com', 'TOKEN', NULL, NULL, 1, 3), +(4, NOW(), '禅道', 'ZENTAO', 'http://zentao.example.com', 'BASIC', 'admin', 'zentao123', 1, 4); \ No newline at end of file diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index e14315ef..a8756a38 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -63,4 +63,11 @@ permission.not.found=权限不存在 permission.code.exists=权限编码{0}已存在 permission.name.exists=权限名称{0}已存在 permission.already.assigned=该权限已分配给角色 -permission.assign.failed=权限分配失败 \ No newline at end of file +permission.assign.failed=权限分配失败 + +# 第三方系统相关 +external.system.name.exists=系统名称"{0}"已存在 +external.system.type.url.exists=系统类型和URL组合"{0}"已存在 +external.system.disabled=系统已禁用 +external.system.sync.failed=系统数据同步失败 +external.system.type.not.supported=不支持的系统类型 \ No newline at end of file diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties deleted file mode 100644 index 2b5f1802..00000000 --- a/backend/src/main/resources/messages_en.properties +++ /dev/null @@ -1,52 +0,0 @@ -# Common Response -response.success=Success -response.error=System Error -response.invalid.param=Invalid Parameter -response.unauthorized=Unauthorized -response.forbidden=Forbidden -response.not.found=Resource Not Found -response.conflict=Resource Conflict -response.unauthorized.full=Full authentication is required to access this resource - -# Business Error -tenant.not.found=Tenant not found -data.not.found={0} with id {1} not found - -# User Related -user.not.found=User not found -user.username.exists=Username already exists -user.email.exists=Email already exists -user.login.error=Invalid username or password - -# System Exception Messages -system.optimistic.lock.error=Data has been modified by another user, please refresh and try again -system.pessimistic.lock.error=Data is being operated by another user, please try again later -system.concurrent.update.error=Concurrent update conflict, please try again -system.retry.exceeded.error=Operation retry limit exceeded, please try again later - -# Entity Not Found Messages -entity.not.found.id=Entity with id {0} not found -entity.not.found.message={0} -entity.not.found.name.id={0} with id {1} not found - -# Dependency Injection -dependency.injection.service.not.found=No service found for entity {0} (tried bean name: {1}) -dependency.injection.repository.not.found=No repository found for entity {0}: {1} -dependency.injection.converter.not.found=No converter found for entity {0}: {1} -dependency.injection.entitypath.failed=Failed to initialize EntityPath for entity {0}: {1} - -# JWT -jwt.token.expired=Login expired, please login again -jwt.token.invalid=Invalid token -jwt.token.missing=No token provided - -# Role related error messages -role.not.found=Role not found -role.code.exists=Role code already exists -role.name.exists=Role name already exists -role.in.use=Role is in use and cannot be deleted -role.admin.cannot.delete=Cannot delete admin role -role.admin.cannot.update=Cannot update admin role -role.tag.name.exists=Tag name already exists -role.tag.not.found=Tag not found -role.tag.in.use=Tag is in use and cannot be deleted \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/api/ExternalSystemApiControllerTest.java b/backend/src/test/java/com/qqchen/deploy/backend/api/ExternalSystemApiControllerTest.java new file mode 100644 index 00000000..f336bf17 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/api/ExternalSystemApiControllerTest.java @@ -0,0 +1,72 @@ +package com.qqchen.deploy.backend.api; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.model.ExternalSystemDTO; +import com.qqchen.deploy.backend.service.IExternalSystemService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@WithMockUser(username = "admin", roles = {"ADMIN"}) +class ExternalSystemApiControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private IExternalSystemService externalSystemService; + + @Test + void testConnection_WhenSuccess_ShouldReturnTrue() throws Exception { + // Mock + when(externalSystemService.testConnection(1L)).thenReturn(true); + + // 执行并验证 + mockMvc.perform(get("/api/v1/external-system/1/test-connection")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value(true)); + } + + @Test + void testConnection_WhenFailed_ShouldReturnFalse() throws Exception { + // Mock + when(externalSystemService.testConnection(1L)).thenReturn(false); + + // 执行并验证 + mockMvc.perform(get("/api/v1/external-system/1/test-connection")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value(false)); + } + + @Test + void syncData_ShouldReturnSuccess() throws Exception { + // 执行并验证 + mockMvc.perform(post("/api/v1/external-system/1/sync")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + } + + @Test + void updateStatus_ShouldReturnSuccess() throws Exception { + // 执行并验证 + mockMvc.perform(put("/api/v1/external-system/1/status") + .param("enabled", "false")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/service/impl/ExternalSystemServiceImplTest.java b/backend/src/test/java/com/qqchen/deploy/backend/service/impl/ExternalSystemServiceImplTest.java new file mode 100644 index 00000000..eeef8f00 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/service/impl/ExternalSystemServiceImplTest.java @@ -0,0 +1,148 @@ +package com.qqchen.deploy.backend.service.impl; + +import com.qqchen.deploy.backend.entity.ExternalSystem; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException; +import com.qqchen.deploy.backend.model.ExternalSystemDTO; +import com.qqchen.deploy.backend.repository.IExternalSystemRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@SpringBootTest +class ExternalSystemServiceImplTest { + + @MockBean + private IExternalSystemRepository externalSystemRepository; + + @Autowired + private ExternalSystemServiceImpl externalSystemService; + + private ExternalSystem system; + private ExternalSystemDTO systemDTO; + + @BeforeEach + void setUp() { + // 准备测试数据 + system = new ExternalSystem(); + system.setId(1L); + system.setName("测试Jenkins"); + system.setType(ExternalSystem.SystemType.JENKINS); + system.setUrl("http://jenkins.test.com"); + system.setAuthType(ExternalSystem.AuthType.BASIC); + system.setUsername("admin"); + system.setPassword("password"); + system.setEnabled(true); + system.setSort(1); + + systemDTO = new ExternalSystemDTO(); + systemDTO.setName("测试Jenkins"); + systemDTO.setType(ExternalSystem.SystemType.JENKINS); + systemDTO.setUrl("http://jenkins.test.com"); + systemDTO.setAuthType(ExternalSystem.AuthType.BASIC); + systemDTO.setUsername("admin"); + systemDTO.setPassword("password"); + systemDTO.setEnabled(true); + systemDTO.setSort(1); + } + + @Test + void validateUniqueConstraints_WhenNameExists_ShouldThrowException() { + // Mock + when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(true); + + // 验证 + assertThatThrownBy(() -> externalSystemService.validateUniqueConstraints(systemDTO)) + .isInstanceOf(UniqueConstraintException.class) + .hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_NAME_EXISTS); + } + + @Test + void validateUniqueConstraints_WhenTypeAndUrlExists_ShouldThrowException() { + // Mock + when(externalSystemRepository.existsByNameAndDeletedFalse(systemDTO.getName())).thenReturn(false); + when(externalSystemRepository.existsByTypeAndUrlAndDeletedFalse(systemDTO.getType(), systemDTO.getUrl())) + .thenReturn(true); + + // 验证 + assertThatThrownBy(() -> externalSystemService.validateUniqueConstraints(systemDTO)) + .isInstanceOf(UniqueConstraintException.class) + .hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_TYPE_URL_EXISTS); + } + + @Test + void testConnection_WhenSystemDisabled_ShouldThrowException() { + // 准备数据 + system.setEnabled(false); + when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system)); + + // 验证 + assertThatThrownBy(() -> externalSystemService.testConnection(1L)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_DISABLED); + } + + @Test + void testConnection_WhenSystemEnabled_ShouldReturnTrue() { + // Mock + when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system)); + + // 执行 + boolean result = externalSystemService.testConnection(1L); + + // 验证 + assertThat(result).isTrue(); + } + + @Test + void syncData_WhenSystemDisabled_ShouldThrowException() { + // 准备数据 + system.setEnabled(false); + when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system)); + + // 验证 + assertThatThrownBy(() -> externalSystemService.syncData(1L)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", ResponseCode.EXTERNAL_SYSTEM_DISABLED); + } + + @Test + void syncData_WhenSuccessful_ShouldUpdateStatus() { + // Mock + when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system)); + when(externalSystemRepository.save(any(ExternalSystem.class))).thenReturn(system); + + // 执行 + externalSystemService.syncData(1L); + + // 验证 + assertThat(system.getSyncStatus()).isEqualTo(ExternalSystem.SyncStatus.SUCCESS); + assertThat(system.getLastSyncTime()).isNotNull(); + verify(externalSystemRepository, times(2)).save(any(ExternalSystem.class)); + } + + @Test + void updateStatus_ShouldUpdateSystemStatus() { + // Mock + when(externalSystemRepository.findById(1L)).thenReturn(Optional.of(system)); + when(externalSystemRepository.save(any(ExternalSystem.class))).thenReturn(system); + + // 执行 + externalSystemService.updateStatus(1L, false); + + // 验证 + assertThat(system.getEnabled()).isFalse(); + verify(externalSystemRepository).save(system); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/service/impl/MenuServiceImplTest.java b/backend/src/test/java/com/qqchen/deploy/backend/service/impl/MenuServiceImplTest.java new file mode 100644 index 00000000..8425418a --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/service/impl/MenuServiceImplTest.java @@ -0,0 +1,235 @@ +package com.qqchen.deploy.backend.service.impl; + +import com.qqchen.deploy.backend.converter.MenuConverter; +import com.qqchen.deploy.backend.entity.Menu; +import com.qqchen.deploy.backend.entity.Permission; +import com.qqchen.deploy.backend.model.MenuDTO; +import com.qqchen.deploy.backend.model.response.MenuPermissionTreeResponse; +import com.qqchen.deploy.backend.model.response.MenuResponse; +import com.qqchen.deploy.backend.model.response.PermissionResponse; +import com.qqchen.deploy.backend.repository.IMenuRepository; +import com.qqchen.deploy.backend.repository.IPermissionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@SpringBootTest +class MenuServiceImplTest { + + @MockBean + private IMenuRepository menuRepository; + + @MockBean + private IPermissionRepository permissionRepository; + + @MockBean + private MenuConverter menuConverter; + + @Autowired + private MenuServiceImpl menuService; + + private Menu rootMenu; + private Menu childMenu; + private Permission permission1; + private Permission permission2; + + @BeforeEach + void setUp() { + // 准备测试数据 + rootMenu = new Menu(); + rootMenu.setId(1L); + rootMenu.setName("根菜单"); + rootMenu.setSort(1); + rootMenu.setType(1); + rootMenu.setParentId(0L); + + childMenu = new Menu(); + childMenu.setId(2L); + childMenu.setName("子菜单"); + childMenu.setSort(1); + childMenu.setType(2); + childMenu.setParentId(1L); + + permission1 = new Permission(); + permission1.setId(1L); + permission1.setMenuId(2L); + permission1.setName("查看"); + permission1.setCode("VIEW"); + permission1.setSort(1); + + permission2 = new Permission(); + permission2.setId(2L); + permission2.setMenuId(2L); + permission2.setName("编辑"); + permission2.setCode("EDIT"); + permission2.setSort(2); + } + + @Test + void getPermissionTree_ShouldReturnCorrectStructure() { + // 准备测试数据 + List menus = Arrays.asList(rootMenu, childMenu); + List permissions = Arrays.asList(permission1, permission2); + + // Mock Repository方法 + when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(menus); + when(permissionRepository.findAllByDeletedFalseOrderBySort()).thenReturn(permissions); + + // Mock Converter方法 + MenuPermissionTreeResponse rootResponse = createMenuPermissionResponse(rootMenu); + MenuPermissionTreeResponse childResponse = createMenuPermissionResponse(childMenu); + when(menuConverter.toMenuPermissionResponse(rootMenu)).thenReturn(rootResponse); + when(menuConverter.toMenuPermissionResponse(childMenu)).thenReturn(childResponse); + when(menuConverter.toPermissionResponseList(Arrays.asList(permission1, permission2))) + .thenReturn(Arrays.asList( + createPermissionResponse(permission1), + createPermissionResponse(permission2) + )); + + // 执行测试 + List result = menuService.getPermissionTree(); + + // 验证结果 + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); // 只有一个根节点 + + MenuPermissionTreeResponse root = result.get(0); + assertThat(root.getId()).isEqualTo(1L); + assertThat(root.getName()).isEqualTo("根菜单"); + assertThat(root.getPermissionChildren()).hasSize(1); // 有一个子节点 + + MenuPermissionTreeResponse child = root.getPermissionChildren().get(0); + assertThat(child.getId()).isEqualTo(2L); + assertThat(child.getName()).isEqualTo("子菜单"); + assertThat(child.getPermissions()).hasSize(2); // 有两个权限 + + PermissionResponse firstPermission = child.getPermissions().get(0); + assertThat(firstPermission.getCode()).isEqualTo("VIEW"); + assertThat(firstPermission.getName()).isEqualTo("查看"); + } + + private MenuPermissionTreeResponse createMenuPermissionResponse(Menu menu) { + MenuPermissionTreeResponse response = new MenuPermissionTreeResponse(); + response.setId(menu.getId()); + response.setName(menu.getName()); + response.setParentId(menu.getParentId()); + response.setType(menu.getType()); + response.setSort(menu.getSort()); + return response; + } + + private PermissionResponse createPermissionResponse(Permission permission) { + PermissionResponse response = new PermissionResponse(); + response.setId(permission.getId()); + response.setCode(permission.getCode()); + response.setName(permission.getName()); + response.setType(permission.getType()); + response.setSort(permission.getSort()); + return response; + } + + @Test + void getPermissionTree_WithEmptyData_ShouldReturnEmptyList() { + // Mock空数据 + when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(List.of()); + when(permissionRepository.findAllByDeletedFalseOrderBySort()).thenReturn(List.of()); + + // 执行测试 + List result = menuService.getPermissionTree(); + + // 验证结果 + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + } + + @Test + void getMenuTree_ShouldReturnCorrectStructure() { + // 准备测试数据 + List menus = Arrays.asList(rootMenu, childMenu); + + // 准备DTO对象 + MenuDTO rootDto = convertToDto(rootMenu); + MenuDTO childDto = convertToDto(childMenu); + rootDto.setChildren(new ArrayList<>(Arrays.asList(childDto))); + List dtoList = new ArrayList<>(Arrays.asList(rootDto, childDto)); + + // 准备Response对象 + MenuResponse rootResponse = createMenuResponse(rootMenu); + MenuResponse childResponse = createMenuResponse(childMenu); + rootResponse.setChildren(new ArrayList<>(Arrays.asList(childResponse))); + List responseList = new ArrayList<>(Arrays.asList(rootResponse)); + + // Mock Repository方法 + when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(menus); + + // Mock Converter方法 + when(menuConverter.toDtoList(menus)).thenReturn(dtoList); + when(menuConverter.toResponseList(List.of(rootDto))).thenReturn(responseList); + + // 执行测试 + List result = menuService.getMenuTree(); + + // 验证结果 + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); // 只有一个根节点 + + MenuResponse root = result.get(0); + assertThat(root.getId()).isEqualTo(1L); + assertThat(root.getName()).isEqualTo("根菜单"); + assertThat(root.getType()).isEqualTo(1); + assertThat(root.getParentId()).isEqualTo(0L); + assertThat(root.getChildren()).hasSize(1); // 有一个子节点 + + MenuResponse child = root.getChildren().get(0); + assertThat(child.getId()).isEqualTo(2L); + assertThat(child.getName()).isEqualTo("子菜单"); + assertThat(child.getType()).isEqualTo(2); + assertThat(child.getParentId()).isEqualTo(1L); + assertThat(child.getChildren()).isEmpty(); // 没有子节点 + } + + @Test + void getMenuTree_WithEmptyData_ShouldReturnEmptyList() { + // Mock空数据 + when(menuRepository.findByDeletedFalseOrderBySort()).thenReturn(List.of()); + + // 执行测试 + List result = menuService.getMenuTree(); + + // 验证结果 + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + } + + private MenuResponse createMenuResponse(Menu menu) { + MenuResponse response = new MenuResponse(); + response.setId(menu.getId()); + response.setName(menu.getName()); + response.setParentId(menu.getParentId()); + response.setType(menu.getType()); + response.setSort(menu.getSort()); + response.setChildren(new ArrayList<>()); + return response; + } + + private MenuDTO convertToDto(Menu menu) { + MenuDTO dto = new MenuDTO(); + dto.setId(menu.getId()); + dto.setName(menu.getName()); + dto.setParentId(menu.getParentId()); + dto.setType(menu.getType()); + dto.setSort(menu.getSort()); + dto.setChildren(new ArrayList<>()); + return dto; + } +} \ No newline at end of file