增加属于自己的cursorrules

This commit is contained in:
dengqichen 2024-12-02 14:05:31 +08:00
parent 0e031d0cfe
commit 7121e83fbb
29 changed files with 2800 additions and 539 deletions

526
backend/.cursorrules Normal file
View File

@ -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 DTOBaseDTO、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<ExternalSystem, ExternalSystemDTO, Long>
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
- 新增接口命名规范:
- 三方接口模块名ApiControllerExternalSystemApiController
- 二方接口模块名ControllerExternalSystemController
- 示例:
```java
@Slf4j
@RestController
@RequestMapping("/api/v1/external-system")
@Tag(name = "外部系统管理", description = "外部系统管理相关接口")
public class ExternalSystemApiController extends BaseController<ExternalSystem, ExternalSystemDTO, Long, ExternalSystemQuery> {
// 特定业务方法实现
}
```
### Repository层规范
- 继承IBaseRepository
- 定义特定的查询方法
- 示例:
```java
@Repository
public interface IExternalSystemRepository extends IBaseRepository<ExternalSystem, Long> {
boolean existsByNameAndDeletedFalse(String name);
}
```
### Converter规范
- 继承BaseConverter遵循以下规则
1. 简单场景(字段完全匹配)示例:
```java
@Mapper(config = BaseConverter.class)
public interface ExternalSystemConverter extends BaseConverter<ExternalSystem, ExternalSystemDTO> {
// 字段完全匹配时无需额外配置
}
```
2. 复杂场景(需要特殊映射)示例:
```java
@Mapper(config = BaseConverter.class)
public interface UserConverter extends BaseConverter<User, UserDTO> {
@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-2299JWT相关错误
- 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 Token2小时
- Refresh Token7天
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<User, UserDTO, Long> 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. 缓存穿透处理

View File

@ -1,136 +1,748 @@
# Deploy Ease Platform
# 问题解决方案 Deploy Ease Platform 是一个现代化的部署管理平台,旨在简化和自动化应用程序的部署流程。
##### 1、Cannot compare left expression of type 'java.io.Serializable' with right expression of type 'java.lang.Long'
```
使用@Query查询因为无法解决序列化问题
```
## 已实现功能
` ### 1. 外部系统管理
分层结构从上到下: - [x] 基础功能
` - 创建外部系统支持Jenkins、Git等类型
``` - 更新系统信息
Controller (表现层) - 删除系统
- 查询系统详情
Service (业务层) - 分页查询系统列表
- 条件筛选查询
Repository (数据访问层) - [x] Jenkins集成
``` - 连接测试
` - 任务同步
对于外部集成: - 状态检查
` - 支持用户名密码认证
``` - 支持Token认证
Controller - [x] Git集成
- 仓库连接测试
Service ←→ Integration Service (集成层) - 代码分支同步
- 提交历史查询
Repository - 支持用户名密码认证
``` - 支持SSH密钥认证
- 支持Token认证
``` ### 2. 用户权限管理
GET /api/users - 查询所有用户 - [x] 用户管理
GET /api/users/list?enabled=true - 查询所有启用的用户 - 用户注册
GET /api/users/page?pageNum=1&pageSize=10 - 分页查询用户 - 用户登录
``` - 密码重置
- 用户信息修改
- 用户状态管理
- 部门分配
- [x] 角色管理
- 角色创建与维护
- 角色标签管理
- 角色权限分配
- 用户角色分配
- [x] 权限管理
- 权限创建与维护
- 权限分类管理
- 权限与菜单关联
- 权限检查与控制
`查询日期示例` ### 3. 菜单管理
``` - [x] 基础功能
UserQuery query = new UserQuery(); - 菜单树维护
query.setCreateTimeRange( - 菜单排序
LocalDateTime.now().minusDays(7), // 一周内 - 菜单显示控制
LocalDateTime.now() - [x] 权限集成
); - 菜单权限配置
query.setEnabled(true); - 按钮权限配置
query.setCreateBy("admin"); - 数据权限配置
- [x] 前端集成
- 动态路由生成
- 菜单组件加载
- 权限指令集成
### 4. 部门管理
- [x] 基础功能
- 部门树维护
- 部门编码管理
- 部门描述维护
- 部门排序
- [x] 人员管理
- 部门负责人设置
- 部门人员管理
- 部门权限控制
@QueryField(type = QueryType.IN) ### 5. 系统功能
private String status; // 可以传入 "ACTIVE,PENDING,CLOSED" - [x] 认证授权
- JWT令牌认证
- 权限拦截器
- 角色检查器
- 权限注解支持
- [x] 数据审计
- 创建人记录
- 创建时间记录
- 更新人记录
- 更新时间记录
- 版本控制
- [x] 异常处理
- 统一异常处理
- 业务异常处理
- 系统异常处理
- 参数校验异常处理
- [x] 数据库支持
- Flyway数据库版本控制
- 基础表结构
- 初始化数据
- 软删除支持
``` ### 6. 开发支持
``` - [x] 基础框架
QueryDSL使用方法 - 统一响应格式
@Service - 统一分页格式
@Transactional - 基础CRUD封装
public class UserServiceImpl extends BaseServiceImpl<User, Long> implements UserService { - DTO自动转换
- [x] 代码规范
- 开发规范文档
- 接口文档规范
- 测试规范
- 注释规范
- [x] 测试支持
- 单元测试框架
- 集成测试支持
- 测试数据构建
- Mock支持
private final UserRoleRepository userRoleRepository; ## 技术栈
private final QUserRole qUserRole = QUserRole.userRole;
public UserServiceImpl(UserRepository userRepository, UserRoleRepository userRoleRepository) { ### 后端技术栈
super(userRepository); - Spring Boot 3.x
this.userRoleRepository = userRoleRepository; - 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<UserRole> getUserRoles(Long userId) { #### 1.2 创建外部系统
// 使用QueryDSL构建查询条件 ```http
Predicate predicate = qUserRole.user.id.eq(userId); POST /api/v1/external-system
return StreamSupport.stream(
userRoleRepository.findAll(predicate).spliterator(), 请求参数:
false {
).collect(Collectors.toSet()); "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) { #### 1.3 更新外部系统
User user = findById(userId); ```http
Role role = roleRepository.findById(roleId) PUT /api/v1/external-system/{id}
.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); "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": "技术部"
} }
} }
} }
``` ```
``` #### 2.2 获取当前用户信息
-- 插入正常用户数据 ```http
INSERT INTO sys_user ( GET /api/v1/user/current
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),
-- 普通用户 响应结果:
('user01', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '张三', 'zhangsan@example.com', '13800138001', {
true, false, 1, '技术部', "success": true,
'admin', '2024-01-02 10:00:00', null, null, 0), "code": 200,
"message": "操作成功",
('user02', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '李四', 'lisi@example.com', '13800138002', "data": {
true, false, 2, '市场部', "id": 1,
'admin', '2024-01-03 11:00:00', 'admin', '2024-01-04 15:00:00', 1), "username": "admin",
"nickname": "管理员",
-- 已禁用的用户 "email": "admin@example.com",
('disabled_user', '$2a$10$mW/yJPHjyueQ1g26YxiZNOtr6bKVF4P/w/VHLVHHhxslY.YlXhbcm', '王五', 'wangwu@example.com', '13800138003', "phone": "13800138000",
false, false, 2, '市场部', "departmentId": 1,
'admin', '2024-01-05 14:00:00', 'admin', '2024-01-06 16:00:00', 1), "departmentName": "技术部"
}
-- 已删除的用户 }
('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);
``` ```
### 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)
```

View File

@ -175,6 +175,21 @@
<groupId>org.flywaydb</groupId> <groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId> <artifactId>flyway-mysql</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -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<ExternalSystem, ExternalSystemDTO, Long, ExternalSystemQuery> {
@Resource
private IExternalSystemService externalSystemService;
@Operation(summary = "测试连接")
@GetMapping("/{id}/test-connection")
public Response<Boolean> testConnection(
@Parameter(description = "系统ID", required = true) @PathVariable Long id
) {
return Response.success(externalSystemService.testConnection(id));
}
@Operation(summary = "同步数据")
@PostMapping("/{id}/sync")
public Response<Void> syncData(
@Parameter(description = "系统ID", required = true) @PathVariable Long id
) {
externalSystemService.syncData(id);
return Response.success();
}
@Operation(summary = "更新状态")
@PutMapping("/{id}/status")
public Response<Void> 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<ExternalSystemDTO> data) {
// TODO: 实现导出功能
}
}

View File

@ -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<ExternalSystem, ExternalSystemDTO> {
// MapStruct会自动实现所有方法包括从BaseConverter继承的方法
// 由于ExternalSystem和ExternalSystemDTO的字段名完全匹配无需额外的映射配置
}

View File

@ -1,10 +1,12 @@
package com.qqchen.deploy.backend.converter; package com.qqchen.deploy.backend.converter;
import com.qqchen.deploy.backend.entity.Menu; 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.framework.converter.BaseConverter;
import com.qqchen.deploy.backend.model.MenuDTO; import com.qqchen.deploy.backend.model.MenuDTO;
import com.qqchen.deploy.backend.model.response.MenuPermissionTreeResponse; import com.qqchen.deploy.backend.model.response.MenuPermissionTreeResponse;
import com.qqchen.deploy.backend.model.response.MenuResponse; import com.qqchen.deploy.backend.model.response.MenuResponse;
import com.qqchen.deploy.backend.model.response.PermissionResponse;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@ -25,4 +27,8 @@ public interface MenuConverter extends BaseConverter<Menu, MenuDTO> {
List<MenuResponse> toResponseList(List<MenuDTO> dtoList); List<MenuResponse> toResponseList(List<MenuDTO> dtoList);
MenuPermissionTreeResponse toMenuPermissionResponse(Menu menu); MenuPermissionTreeResponse toMenuPermissionResponse(Menu menu);
PermissionResponse toPermissionResponse(Permission permission);
List<PermissionResponse> toPermissionResponseList(List<Permission> permissions);
} }

View File

@ -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<Long> {
/**
* 系统名称
*/
@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
}
}

View File

@ -75,10 +75,4 @@ public class Menu extends Entity<Long> {
@ManyToMany(mappedBy = "menus") @ManyToMany(mappedBy = "menus")
private Set<Role> roles = new HashSet<>(); private Set<Role> roles = new HashSet<>();
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "sys_permission",
joinColumns = @JoinColumn(name = "menu_id")
)
private Set<Permission> permissions = new HashSet<>();
} }

View File

@ -63,7 +63,14 @@ public enum ResponseCode {
PERMISSION_CODE_EXISTS(2401, "permission.code.exists"), PERMISSION_CODE_EXISTS(2401, "permission.code.exists"),
PERMISSION_NAME_EXISTS(2402, "permission.name.exists"), PERMISSION_NAME_EXISTS(2402, "permission.name.exists"),
PERMISSION_ALREADY_ASSIGNED(2403, "permission.already.assigned"), 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 int code;
private final String messageKey; // 国际化消息key private final String messageKey; // 国际化消息key

View File

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

View File

@ -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<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> 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;
}
}

View File

@ -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<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> 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;
}
}

View File

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

View File

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

View File

@ -1,16 +1,30 @@
package com.qqchen.deploy.backend.model.response; 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.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.List; import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class MenuPermissionTreeResponse extends MenuResponse { public class MenuPermissionTreeResponse extends MenuResponse {
/** /**
* 菜单下的权限列表 * 子菜单列表覆盖父类的children因为类型不同
*/
@JsonIgnore
@Override
public List<MenuResponse> getChildren() {
return super.getChildren();
}
/**
* 子菜单列表
*/
private List<MenuPermissionTreeResponse> permissionChildren;
/**
* 权限列表
*/ */
private List<PermissionResponse> permissions; private List<PermissionResponse> permissions;
} }

View File

@ -1,21 +1,31 @@
package com.qqchen.deploy.backend.model.response; package com.qqchen.deploy.backend.model.response;
import com.qqchen.deploy.backend.framework.dto.BaseResponse;
import lombok.Data; import lombok.Data;
@Data @Data
public class PermissionResponse extends BaseResponse { public class PermissionResponse {
/**
private Long menuId; * 权限ID
*/
private String menuName; private Long id;
/**
* 权限编码
*/
private String code; private String code;
/**
* 权限名称
*/
private String name; private String name;
/**
* 权限类型
*/
private String type; private String type;
/**
* 排序
*/
private Integer sort; private Integer sort;
} }

View File

@ -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<ExternalSystem, Long> {
/**
* 检查系统名称是否存在
*/
boolean existsByNameAndDeletedFalse(String name);
/**
* 检查系统类型和URL组合是否存在
*/
boolean existsByTypeAndUrlAndDeletedFalse(ExternalSystem.SystemType type, String url);
/**
* 查询所有未删除的系统按排序字段排序
*/
List<ExternalSystem> findByDeletedFalseOrderBySort();
}

View File

@ -15,4 +15,27 @@ public interface IPermissionRepository extends IBaseRepository<Permission, Long>
List<Permission> findAllEnabledOrderByMenuAndSort(); List<Permission> findAllEnabledOrderByMenuAndSort();
List<Permission> findByIdIn(List<Long> ids); List<Permission> findByIdIn(List<Long> ids);
/**
* 查询所有未删除的权限按排序字段排序
*
* @return 权限列表
*/
List<Permission> findAllByDeletedFalseOrderBySort();
/**
* 根据菜单ID查询权限列表
*
* @param menuId 菜单ID
* @return 权限列表
*/
List<Permission> findByMenuIdAndDeletedFalseOrderBySort(Long menuId);
/**
* 检查权限编码是否存在
*
* @param code 权限编码
* @return 是否存在
*/
boolean existsByCodeAndDeletedFalse(String code);
} }

View File

@ -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<ExternalSystem, ExternalSystemDTO, Long> {
/**
* 测试连接
*
* @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);
}

View File

@ -22,10 +22,6 @@ public interface IMenuService extends IBaseService<Menu, MenuDTO, Long> {
*/ */
List<MenuResponse> getUserMenus(); List<MenuResponse> getUserMenus();
/**
* 获取菜单树
*/
List<MenuDTO> getTree();
List<MenuPermissionTreeResponse> getPermissionTree(); List<MenuPermissionTreeResponse> getPermissionTree();

View File

@ -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<ExternalSystem, ExternalSystemDTO, Long>
implements IExternalSystemService {
@Resource
private IExternalSystemRepository externalSystemRepository;
@Resource
private List<IExternalSystemIntegration> systemIntegrations;
private Map<ExternalSystem.SystemType, IExternalSystemIntegration> 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: 实现禅道数据同步
}
}

View File

@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Optional;
import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DATABASE; import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DATABASE;
@ -42,9 +43,6 @@ public class MenuServiceImpl extends BaseServiceImpl<Menu, MenuDTO, Long> implem
@Resource @Resource
private MenuConverter menuConverter; private MenuConverter menuConverter;
@Resource
private PermissionConverter permissionConverter;
@Resource @Resource
private IUserService userService; private IUserService userService;
@ -79,22 +77,55 @@ public class MenuServiceImpl extends BaseServiceImpl<Menu, MenuDTO, Long> implem
} }
@Override @Override
public List<MenuDTO> getTree() { public List<MenuPermissionTreeResponse> getPermissionTree() {
// 1. 获取所有菜单
List<Menu> menus = menuRepository.findByDeletedFalseOrderBySort(); List<Menu> menus = menuRepository.findByDeletedFalseOrderBySort();
return buildTree(menus);
// 2. 获取所有权限
List<Permission> permissions = permissionRepository.findAllByDeletedFalseOrderBySort();
// 3. 构建权限Map (menuId -> List<Permission>)
Map<Long, List<Permission>> permissionMap = permissions.stream()
.collect(Collectors.groupingBy(Permission::getMenuId));
// 4. 构建菜单树
return buildPermissionTree(menus, permissionMap);
} }
@Override private List<MenuPermissionTreeResponse> buildPermissionTree(List<Menu> menus,
public List<MenuPermissionTreeResponse> getPermissionTree() { Map<Long, List<Permission>> permissionMap) {
List<Menu> menus = menuRepository.findByDeletedFalseOrderBySort(); // 1. 转换所有菜单为树形节点
List<Permission> permissions = permissionRepository.findAllEnabledOrderByMenuAndSort(); List<MenuPermissionTreeResponse> nodes = menus.stream()
List<MenuPermissionTreeResponse> tree = new ArrayList<>(); .map(menu -> convertToTreeNode(menu, permissionMap))
menus.forEach(menu -> { .collect(Collectors.toList());
MenuPermissionTreeResponse menuPermissionResponse = menuConverter.toMenuPermissionResponse(menu);
List<Permission> list = permissions.stream().filter(permission -> permission.getMenuId().equals(menu.getId())).toList(); // 2. 构建父子关系
menuPermissionResponse.setPermissions(permissionConverter.toResponseList(list)); Map<Long, List<MenuPermissionTreeResponse>> childrenMap = nodes.stream()
.collect(Collectors.groupingBy(node ->
Optional.ofNullable(node.getParentId()).orElse(0L)));
// 3. 设置子节点并排序
nodes.forEach(node -> {
List<MenuPermissionTreeResponse> children = childrenMap.getOrDefault(node.getId(), new ArrayList<>());
children.sort(Comparator.comparing(MenuPermissionTreeResponse::getSort));
node.setPermissionChildren(children);
}); });
return null;
// 4. 返回排序后的顶层节点
List<MenuPermissionTreeResponse> rootNodes = childrenMap.getOrDefault(0L, new ArrayList<>());
rootNodes.sort(Comparator.comparing(MenuPermissionTreeResponse::getSort));
return rootNodes;
}
private MenuPermissionTreeResponse convertToTreeNode(Menu menu,
Map<Long, List<Permission>> permissionMap) {
MenuPermissionTreeResponse node = menuConverter.toMenuPermissionResponse(menu);
// 设置权限列表
List<Permission> permissions = permissionMap.getOrDefault(menu.getId(), new ArrayList<>());
node.setPermissions(menuConverter.toPermissionResponseList(permissions));
return node;
} }
private List<MenuDTO> buildTree(List<Menu> menus) { private List<MenuDTO> buildTree(List<Menu> menus) {
@ -158,10 +189,8 @@ public class MenuServiceImpl extends BaseServiceImpl<Menu, MenuDTO, Long> implem
if (menus == null || menus.isEmpty()) { if (menus == null || menus.isEmpty()) {
return; return;
} }
// 根据sort字段排序 // 根据sort字段排序
menus.sort(Comparator.comparing(MenuDTO::getSort)); menus.sort(Comparator.comparing(MenuDTO::getSort));
// 递归排序子菜单 // 递归排序子菜单
for (MenuDTO menu : menus) { for (MenuDTO menu : menus) {
if (menu.getChildren() != null && !menu.getChildren().isEmpty()) { if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {

View File

@ -1,258 +1,275 @@
-- 创建租户表 -- --------------------------------------------------------------------------------------
-- 系统基础表
-- --------------------------------------------------------------------------------------
-- 租户表
CREATE TABLE IF NOT EXISTS sys_tenant ( CREATE TABLE IF NOT EXISTS sys_tenant (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL, create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL, create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL, update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL, update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
address VARCHAR(255) NULL,
code VARCHAR(255) NOT NULL, code VARCHAR(50) NOT NULL COMMENT '租户编码',
contact_name VARCHAR(255) NULL, name VARCHAR(100) NOT NULL COMMENT '租户名称',
contact_phone VARCHAR(255) NULL, address VARCHAR(255) NULL COMMENT '租户地址',
email VARCHAR(255) NULL, contact_name VARCHAR(50) NULL COMMENT '联系人姓名',
enabled BIT NOT NULL DEFAULT 1, contact_phone VARCHAR(20) NULL COMMENT '联系人电话',
name VARCHAR(255) NOT NULL, email VARCHAR(100) NULL COMMENT '联系人邮箱',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
CONSTRAINT UK_tenant_code UNIQUE (code) CONSTRAINT UK_tenant_code UNIQUE (code)
); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表';
-- 创建部门表 -- 部门表
CREATE TABLE sys_department ( CREATE TABLE sys_department (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL, create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL, create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL, update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL, update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
code VARCHAR(255) NOT NULL,
description VARCHAR(255) NULL, code VARCHAR(50) NOT NULL COMMENT '部门编码',
enabled BIT NOT NULL DEFAULT 1, name VARCHAR(100) NOT NULL COMMENT '部门名称',
leader_id BIGINT NULL, description VARCHAR(255) NULL COMMENT '部门描述',
leader_name VARCHAR(255) NULL, leader_id BIGINT NULL COMMENT '部门负责人ID',
name VARCHAR(255) NOT NULL, leader_name VARCHAR(50) NULL COMMENT '部门负责人姓名',
parent_id BIGINT NULL, parent_id BIGINT NULL COMMENT '父部门ID',
sort INT NOT NULL DEFAULT 0, sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
CONSTRAINT UK_department_code UNIQUE (code) CONSTRAINT UK_department_code UNIQUE (code)
); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表';
-- 创建用户表 -- 用户表
CREATE TABLE IF NOT EXISTS sys_user ( CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL, create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL, create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL, update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL, update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
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)
);
-- 创建系统参数表 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 ( CREATE TABLE sys_param (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255), create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6), create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6), update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
code VARCHAR(100) NOT NULL COMMENT '参数编码', code VARCHAR(100) NOT NULL COMMENT '参数编码',
name VARCHAR(100) NOT NULL COMMENT '参数名称', name VARCHAR(100) NOT NULL COMMENT '参数名称',
value TEXT COMMENT '参数值', value TEXT COMMENT '参数值',
type VARCHAR(50) NOT NULL COMMENT '参数类型', type VARCHAR(50) NOT NULL COMMENT '参数类型',
description VARCHAR(255) COMMENT '参数描述', description VARCHAR(255) NULL COMMENT '参数描述',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
CONSTRAINT UK_sys_param_code UNIQUE (code) CONSTRAINT UK_sys_param_code UNIQUE (code)
) COMMENT '系统参数表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统参数表';
-- --------------------------------------------------------------------------------------
-- 权限管理相关表
-- --------------------------------------------------------------------------------------
-- 菜单表 -- 菜单表
CREATE TABLE sys_menu ( CREATE TABLE sys_menu (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255), create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6), create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6), update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
name VARCHAR(100) NOT NULL COMMENT '菜单名称', name VARCHAR(100) NOT NULL COMMENT '菜单名称',
path VARCHAR(200) COMMENT '路由路径', path VARCHAR(200) NULL COMMENT '路由路径',
component VARCHAR(255) COMMENT '组件路径', component VARCHAR(255) NULL COMMENT '组件路径',
icon VARCHAR(100) COMMENT '菜单图标', icon VARCHAR(100) NULL COMMENT '菜单图标',
permission VARCHAR(100) COMMENT '权限标识', permission VARCHAR(100) NULL COMMENT '权限标识',
type INT NOT NULL COMMENT '菜单类型1目录 2菜单 3按钮', type TINYINT NOT NULL COMMENT '菜单类型1目录 2菜单 3按钮',
parent_id BIGINT COMMENT '父菜单ID', parent_id BIGINT NULL COMMENT '父菜单ID',
sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序', sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序',
hidden BIT NOT NULL DEFAULT 0 COMMENT '是否隐藏0显示 1隐藏', hidden BIT NOT NULL DEFAULT 0 COMMENT '是否隐藏0显示1隐藏',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用 1启用' enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
) COMMENT '菜单表';
INDEX IDX_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';
-- 角色表 -- 角色表
CREATE TABLE sys_role ( CREATE TABLE sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255), create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6), create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6), update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
code VARCHAR(100) NOT NULL COMMENT '角色编码', code VARCHAR(100) NOT NULL COMMENT '角色编码',
name VARCHAR(100) NOT NULL COMMENT '角色名称', name VARCHAR(100) NOT NULL COMMENT '角色名称',
type INT NOT NULL DEFAULT 2 COMMENT '角色类型1系统角色 2自定义角色', type TINYINT NOT NULL DEFAULT 2 COMMENT '角色类型1系统角色2自定义角色',
description VARCHAR(255) COMMENT '角色描述', description VARCHAR(255) NULL COMMENT '角色描述',
sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序', sort INT NOT NULL DEFAULT 0 COMMENT '显示顺序',
CONSTRAINT UK_role_code UNIQUE (code) CONSTRAINT UK_role_code UNIQUE (code)
) COMMENT '角色表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
-- 角色标签表 -- 角色标签表
CREATE TABLE sys_role_tag ( CREATE TABLE sys_role_tag (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255), create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6), create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6), update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
name VARCHAR(50) NOT NULL COMMENT '标签名称', name VARCHAR(50) NOT NULL COMMENT '标签名称',
color VARCHAR(20) COMMENT '标签颜色' color VARCHAR(20) NULL COMMENT '标签颜色(十六进制颜色码)'
) COMMENT '角色标签表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表';
-- 角色标签关联表 -- 角色标签关联表
CREATE TABLE sys_role_tag_relation ( CREATE TABLE sys_role_tag_relation (
role_id BIGINT NOT NULL COMMENT '角色ID', role_id BIGINT NOT NULL COMMENT '角色ID',
tag_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), PRIMARY KEY (role_id, tag_id),
-- CONSTRAINT FK_role_tag_tag FOREIGN KEY (tag_id) REFERENCES sys_role_tag (id) CONSTRAINT FK_role_tag_role FOREIGN KEY (role_id) REFERENCES sys_role (id),
) COMMENT '角色标签关联表'; 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 ( CREATE TABLE sys_user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255), create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6), create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6), update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
user_id BIGINT NOT NULL COMMENT '用户ID', user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID', role_id BIGINT NOT NULL COMMENT '角色ID',
CONSTRAINT UK_user_role UNIQUE (user_id, role_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_user FOREIGN KEY (user_id) REFERENCES sys_user (id),
-- CONSTRAINT FK_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role (id) CONSTRAINT FK_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role (id)
) COMMENT '用户角色关联表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
-- 角色菜单关联表 -- 角色菜单关联表
CREATE TABLE sys_role_menu ( CREATE TABLE sys_role_menu (
role_id BIGINT NOT NULL COMMENT '角色ID', role_id BIGINT NOT NULL COMMENT '角色ID',
menu_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), PRIMARY KEY (role_id, menu_id),
-- CONSTRAINT FK_role_menu_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id) CONSTRAINT FK_role_menu_role FOREIGN KEY (role_id) REFERENCES sys_role (id),
) COMMENT '角色菜单关联表'; 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 ( CREATE TABLE sys_permission_template (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255), create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6), create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL DEFAULT 0, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255), update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6), update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
code VARCHAR(100) NOT NULL COMMENT '模板编码', code VARCHAR(100) NOT NULL COMMENT '模板编码',
name VARCHAR(100) NOT NULL COMMENT '模板名称', name VARCHAR(100) NOT NULL COMMENT '模板名称',
type INT NOT NULL DEFAULT 1 COMMENT '模板类型1系统模板 2自定义模板', type TINYINT NOT NULL DEFAULT 1 COMMENT '模板类型1系统模板2自定义模板',
description VARCHAR(255) COMMENT '模板描述', description VARCHAR(255) NULL COMMENT '模板描述',
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
CONSTRAINT UK_template_code UNIQUE (code) CONSTRAINT UK_template_code UNIQUE (code)
) COMMENT '权限模板表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限模板表';
-- 模板菜单关联表 -- 模板菜单关联表
CREATE TABLE sys_template_menu ( CREATE TABLE sys_template_menu (
template_id BIGINT NOT NULL COMMENT '模板ID', template_id BIGINT NOT NULL COMMENT '模板ID',
menu_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 ( CREATE TABLE sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL, create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL, create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL, update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL, update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
menu_id BIGINT NOT NULL COMMENT '关联的菜单ID', menu_id BIGINT NOT NULL COMMENT '关联的菜单ID',
code VARCHAR(100) NOT NULL COMMENT '权限编码', code VARCHAR(100) NOT NULL COMMENT '权限编码',
name VARCHAR(100) NOT NULL COMMENT '权限名称', name VARCHAR(100) NOT NULL COMMENT '权限名称',
type VARCHAR(50) NOT NULL DEFAULT 'FUNCTION' COMMENT '权限类型MENU/FUNCTION/API', type VARCHAR(50) NOT NULL DEFAULT 'FUNCTION' COMMENT '权限类型MENU菜单FUNCTION功能API接口',
sort INT NULL COMMENT '排序', sort INT NULL DEFAULT 0 COMMENT '排序',
UNIQUE KEY UK_CODE (CODE), CONSTRAINT UK_permission_code UNIQUE (code),
KEY IDX_MENU_ID (MENU_ID) CONSTRAINT FK_permission_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (id),
) COMMENT '系统权限表'; INDEX IDX_menu_id (menu_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统权限表';
-- 创建三方系统表 -- --------------------------------------------------------------------------------------
-- 外部系统集成相关表
-- --------------------------------------------------------------------------------------
-- 外部系统表
CREATE TABLE sys_external_system ( CREATE TABLE sys_external_system (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(255) NULL, create_by VARCHAR(255) NULL COMMENT '创建人',
create_time DATETIME(6) NULL, create_time DATETIME(6) NULL COMMENT '创建时间',
deleted BIT NOT NULL, deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0未删除1已删除',
update_by VARCHAR(255) NULL, update_by VARCHAR(255) NULL COMMENT '更新人',
update_time DATETIME(6) NULL, update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
name VARCHAR(255) NOT NULL COMMENT '系统名称', name VARCHAR(100) NOT NULL COMMENT '系统名称',
type VARCHAR(50) NOT NULL COMMENT '系统类型JENKINS/GIT/ZENTAO等', type VARCHAR(50) NOT NULL COMMENT '系统类型JENKINS/GIT/ZENTAO等',
url VARCHAR(255) NOT NULL COMMENT '系统访问地址', url VARCHAR(255) NOT NULL COMMENT '系统访问地址',
remark TEXT NULL COMMENT '备注说明', remark TEXT NULL COMMENT '备注说明',
sort INT NULL COMMENT '排序', sort INT NULL DEFAULT 0 COMMENT '排序',
enabled BIT NOT NULL DEFAULT TRUE COMMENT '是否启用', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用0禁用1启用',
auth_type VARCHAR(50) NOT NULL COMMENT '认证方式BASIC/TOKEN/OAUTH等', auth_type VARCHAR(50) NOT NULL COMMENT '认证方式BASIC基础认证TOKEN令牌认证OAUTHOAuth认证',
username VARCHAR(255) NULL COMMENT '用户名', username VARCHAR(100) NULL COMMENT '用户名',
password VARCHAR(255) NULL COMMENT '密码/密钥', password VARCHAR(255) NULL COMMENT '密码/密钥',
token VARCHAR(255) NULL COMMENT '访问令牌', token VARCHAR(255) NULL COMMENT '访问令牌',
sync_status ENUM ('SUCCESS', 'FAILED', 'RUNNING') NULL COMMENT '最后同步状态', sync_status ENUM ('SUCCESS', 'FAILED', 'RUNNING') NULL COMMENT '最后同步状态',
last_sync_time DATETIME(6) NULL COMMENT '最后同步时间', last_sync_time DATETIME(6) NULL COMMENT '最后同步时间',
config JSON NULL COMMENT '系统特有配置JSON格式', config JSON NULL COMMENT '系统特有配置JSON格式',
UNIQUE KEY UK_NAME (NAME), CONSTRAINT UK_external_system_name UNIQUE (name),
UNIQUE KEY UK_TYPE_URL (TYPE, URL) CONSTRAINT UK_external_system_type_url UNIQUE (type, url),
) COMMENT '第三方系统配置'; INDEX IDX_type (type),
INDEX IDX_enabled (enabled)
-- 创建角色权限关联表 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 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 '角色权限关联表';

View File

@ -1,22 +1,39 @@
-- 初始化租户 -- --------------------------------------------------------------------------------------
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', INSERT INTO sys_tenant (id, create_time, code, name, address, contact_name, contact_phone, email, enabled)
'admin@deploy-ease.com', 1, '默认租户'); 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) INSERT INTO sys_param (id, create_time, code, name, value, type, description, enabled)
VALUES (1, 'USER_STATUS_ENUM', '用户状态枚举', '[{"code":"0","name":"禁用"},{"code":"1","name":"启用"}]', 'ENUM', '用户状态枚举值', TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE); 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) INSERT INTO sys_menu (id, name, path, component, icon, type, parent_id, sort, hidden, enabled, create_by, create_time, version, deleted)
@ -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); (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) INSERT INTO sys_role (id, create_time, code, name, type, description, sort)
VALUES (1, 'SUPER_ADMIN', '超级管理员', 1, '系统超级管理员', 1, 'system', '2024-01-01 00:00:00', 0, FALSE), VALUES
(2, 'DEV_MANAGER', '开发主管', 2, '开发团队主管', 2, 'system', '2024-01-01 00:00:00', 0, FALSE), (1, NOW(), 'SUPER_ADMIN', '超级管理员', 1, '系统超级管理员,拥有所有权限', 1),
(3, 'OPS_MANAGER', '运维主管', 2, '运维团队主管', 3, 'system', '2024-01-01 00:00:00', 0, FALSE); (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) INSERT INTO sys_role_tag (id, create_time, name, color)
VALUES (1, 'TECH', '技术部', '技术研发部门', TRUE, 1, NULL, 'system', '2024-01-01 00:00:00', 0, FALSE), VALUES
(2, 'DEV', '研发组', '研发团队', TRUE, 1, 1, 'system', '2024-01-01 00:00:00', 0, FALSE), (1, NOW(), '系统内置', '#ff4d4f'),
(3, 'OPS', '运维组', '运维团队', TRUE, 2, 1, 'system', '2024-01-01 00:00:00', 0, FALSE); (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) INSERT INTO sys_role_tag_relation (role_id, tag_id)
VALUES (1, 3), -- 超级管理员 -> 安全标签 VALUES
(2, 1), -- 开发主管 -> 研发标签 (1, 1),
(3, 2); (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) INSERT INTO sys_role_menu (role_id, menu_id)
VALUES VALUES
-- 超级管理员拥有所有菜单权限 (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), -- 系统管理员拥有系统管理相关权限
(1, 1), (3, 70); -- 普通用户拥有三方系统权限
(1, 2),
(1, 3), -- 初始化权限模板
(1, 4), INSERT INTO sys_permission_template (id, create_time, code, name, type, description, enabled)
(1, 5), VALUES
(1, 70), (1, NOW(), 'FULL_PERMISSION', '完整权限模板', 1, '包含所有系统权限的模板', 1),
-- 开发主管权限 (2, NOW(), 'BASIC_PERMISSION', '基础权限模板', 1, '包含基本操作权限的模板', 1);
(2, 70),
-- 运维主管权限 -- 初始化模板菜单关联
(3, 70); 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) INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort)
VALUES VALUES
-- 用户管理权限 -- 用户管理权限
(21, 2, 'system:user:add', '用户新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (1, NOW(), 2, 'system:user:list', '用户列表', 'FUNCTION', 1),
(22, 2, 'system:user:edit', '用户编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (2, NOW(), 2, 'system:user:create', '用户创建', 'FUNCTION', 2),
(23, 2, 'system:user:delete', '用户删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (3, NOW(), 2, 'system:user:update', '用户修改', 'FUNCTION', 3),
(24, 2, 'system:user:reset-password', '重置密码', 'FUNCTION', 4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (4, NOW(), 2, 'system:user:delete', '用户删除', 'FUNCTION', 4),
(25, 2, 'system:user:assign-roles', '分配角色', 'FUNCTION', 5, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 角色管理权限 -- 角色管理权限
(31, 3, 'system:role:add', '角色新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (5, NOW(), 3, 'system:role:list', '角色列表', 'FUNCTION', 1),
(32, 3, 'system:role:edit', '角色编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (6, NOW(), 3, 'system:role:create', '角色创建', 'FUNCTION', 2),
(33, 3, 'system:role:delete', '角色删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (7, NOW(), 3, 'system:role:update', '角色修改', 'FUNCTION', 3),
(34, 3, 'system:role:assign-tags', '分配标签', 'FUNCTION', 4, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (8, NOW(), 3, 'system:role:delete', '角色删除', 'FUNCTION', 4),
(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),
-- 菜单管理权限 -- 三方系统权限
(41, 4, 'system:menu:add', '菜单新增', 'FUNCTION', 1, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (9, NOW(), 70, 'system:external:list', '三方系统列表', 'FUNCTION', 1),
(42, 4, 'system:menu:edit', '菜单编辑', 'FUNCTION', 2, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (10, NOW(), 70, 'system:external:create', '三方系统创建', 'FUNCTION', 2),
(43, 4, 'system:menu:delete', '菜单删除', 'FUNCTION', 3, 'system', '2024-01-01 00:00:00', 'system', '2024-01-01 00:00:00', 0, FALSE), (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), INSERT INTO sys_external_system (id, create_time, name, type, url, auth_type, username, password, enabled, sort)
(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 VALUES
-- 用户管理权限 (1, NOW(), 'Jenkins测试环境', 'JENKINS', 'http://jenkins-test.example.com', 'BASIC', 'admin', 'jenkins123', 1, 1),
(1, 21), (2, NOW(), 'Jenkins生产环境', 'JENKINS', 'http://jenkins-prod.example.com', 'BASIC', 'admin', 'jenkins123', 1, 2),
(1, 22), (3, NOW(), 'GitLab', 'GIT', 'http://gitlab.example.com', 'TOKEN', NULL, NULL, 1, 3),
(1, 23), (4, NOW(), '禅道', 'ZENTAO', 'http://zentao.example.com', 'BASIC', 'admin', 'zentao123', 1, 4);
(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, '禅道项目管理系统');

View File

@ -64,3 +64,10 @@ permission.code.exists=权限编码{0}已存在
permission.name.exists=权限名称{0}已存在 permission.name.exists=权限名称{0}已存在
permission.already.assigned=该权限已分配给角色 permission.already.assigned=该权限已分配给角色
permission.assign.failed=权限分配失败 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=不支持的系统类型

View File

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

View File

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

View File

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

View File

@ -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<Menu> menus = Arrays.asList(rootMenu, childMenu);
List<Permission> 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<MenuPermissionTreeResponse> 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<MenuPermissionTreeResponse> result = menuService.getPermissionTree();
// 验证结果
assertThat(result).isNotNull();
assertThat(result).isEmpty();
}
@Test
void getMenuTree_ShouldReturnCorrectStructure() {
// 准备测试数据
List<Menu> menus = Arrays.asList(rootMenu, childMenu);
// 准备DTO对象
MenuDTO rootDto = convertToDto(rootMenu);
MenuDTO childDto = convertToDto(childMenu);
rootDto.setChildren(new ArrayList<>(Arrays.asList(childDto)));
List<MenuDTO> dtoList = new ArrayList<>(Arrays.asList(rootDto, childDto));
// 准备Response对象
MenuResponse rootResponse = createMenuResponse(rootMenu);
MenuResponse childResponse = createMenuResponse(childMenu);
rootResponse.setChildren(new ArrayList<>(Arrays.asList(childResponse)));
List<MenuResponse> 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<MenuResponse> 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<MenuResponse> 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;
}
}