feat: 添加系统配置、审计中心、通知中心、文件管理模块
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
# E2E测试用例设计文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目是一个基于Spring Boot 3.5.9 + WebFlux的响应式管理系统API,采用PostgreSQL数据库,支持用户、角色、字典等核心业务功能。
|
||||
|
||||
## 测试目标
|
||||
|
||||
1. 验证关键用户场景的端到端流程
|
||||
2. 确保API接口的正确性和稳定性
|
||||
3. 验证认证授权机制
|
||||
4. 测试数据一致性和完整性
|
||||
5. 验证错误处理和边界条件
|
||||
|
||||
## 测试环境
|
||||
|
||||
- **测试框架**: Python + Playwright
|
||||
- **API基础URL**: http://localhost:8080
|
||||
- **测试数据库**: PostgreSQL (Test环境)
|
||||
- **测试数据**: 自动化准备和清理
|
||||
|
||||
## 测试用例设计
|
||||
|
||||
### 1. 认证授权测试
|
||||
|
||||
#### 1.1 用户登录流程
|
||||
**用例ID**: TC-AUTH-001
|
||||
**优先级**: P0
|
||||
**前置条件**: 系统已启动,数据库中存在测试用户
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/auth/login`
|
||||
2. 提供正确的用户名和密码
|
||||
3. 验证返回的access_token和refresh_token
|
||||
4. 验证token格式正确性
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回包含accessToken和refreshToken
|
||||
- token格式符合JWT标准
|
||||
|
||||
**测试数据**:
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Token刷新流程
|
||||
**用例ID**: TC-AUTH-002
|
||||
**优先级**: P0
|
||||
**前置条件**: 已获得有效的refresh_token
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/auth/refresh`
|
||||
2. 提供有效的refresh_token
|
||||
3. 验证返回新的access_token
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回新的accessToken
|
||||
- refreshToken保持不变或更新
|
||||
|
||||
#### 1.3 用户登出流程
|
||||
**用例ID**: TC-AUTH-003
|
||||
**优先级**: P1
|
||||
**前置条件**: 已登录,持有有效token
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/auth/logout`
|
||||
2. 在Header中携带Authorization: Bearer {token}
|
||||
3. 验证登出成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回成功消息
|
||||
- token被加入黑名单
|
||||
|
||||
#### 1.4 无效登录测试
|
||||
**用例ID**: TC-AUTH-004
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 使用错误的用户名或密码登录
|
||||
2. 验证错误响应
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 401
|
||||
- 返回认证失败消息
|
||||
|
||||
### 2. 用户管理测试
|
||||
|
||||
#### 2.1 创建用户
|
||||
**用例ID**: TC-USER-001
|
||||
**优先级**: P0
|
||||
**前置条件**: 已登录,具有ADMIN权限
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/users`
|
||||
2. 提供用户信息
|
||||
3. 验证用户创建成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 201
|
||||
- 返回创建的用户信息
|
||||
- 用户ID已生成
|
||||
- 密码已加密
|
||||
|
||||
**测试数据**:
|
||||
```json
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"email": "test@example.com",
|
||||
"roleId": 2,
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 查询单个用户
|
||||
**用例ID**: TC-USER-002
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/users/{id}`
|
||||
2. 验证返回正确的用户信息
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回完整的用户信息
|
||||
|
||||
#### 2.3 查询所有用户
|
||||
**用例ID**: TC-USER-003
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/users`
|
||||
2. 验证返回用户列表
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回用户数组
|
||||
- 包含分页信息(如已实现)
|
||||
|
||||
#### 2.4 更新用户
|
||||
**用例ID**: TC-USER-004
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送PUT请求到 `/api/users/{id}`
|
||||
2. 提供更新的用户信息
|
||||
3. 验证更新成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回更新后的用户信息
|
||||
|
||||
#### 2.5 删除用户
|
||||
**用例ID**: TC-USER-005
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送DELETE请求到 `/api/users/{id}`
|
||||
2. 验证删除成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 204
|
||||
- 用户已被物理删除
|
||||
|
||||
#### 2.6 逻辑删除用户
|
||||
**用例ID**: TC-USER-006
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送DELETE请求到 `/api/users/{id}/logical`
|
||||
2. 验证逻辑删除成功
|
||||
3. 查询已删除用户(带includeDeleted=true)
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 用户被标记为已删除
|
||||
- 可以通过includeDeleted参数查询到
|
||||
|
||||
#### 2.7 恢复已删除用户
|
||||
**用例ID**: TC-USER-007
|
||||
**优先级**: P1
|
||||
**前置条件**: 用户已被逻辑删除
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/users/{id}/restore`
|
||||
2. 验证恢复成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 用户状态恢复正常
|
||||
|
||||
#### 2.8 用户名唯一性检查
|
||||
**用例ID**: TC-USER-008
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/users/check/username?username=existing`
|
||||
2. 验证返回true
|
||||
3. 使用不存在的用户名验证返回false
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回正确的布尔值
|
||||
|
||||
#### 2.9 邮箱唯一性检查
|
||||
**用例ID**: TC-USER-009
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/users/check/email?email=existing@example.com`
|
||||
2. 验证返回true
|
||||
3. 使用不存在的邮箱验证返回false
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回正确的布尔值
|
||||
|
||||
### 3. 角色管理测试
|
||||
|
||||
#### 3.1 创建角色
|
||||
**用例ID**: TC-ROLE-001
|
||||
**优先级**: P0
|
||||
**前置条件**: 已登录,具有ADMIN权限
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/roles`
|
||||
2. 提供角色信息
|
||||
3. 验证角色创建成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 201
|
||||
- 返回创建的角色信息
|
||||
|
||||
**测试数据**:
|
||||
```json
|
||||
{
|
||||
"name": "TEST_ROLE",
|
||||
"description": "测试角色",
|
||||
"permissions": "READ,WRITE"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 查询角色
|
||||
**用例ID**: TC-ROLE-002
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/roles/{id}`
|
||||
2. 验证返回正确的角色信息
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回完整的角色信息
|
||||
|
||||
#### 3.3 按名称查询角色
|
||||
**用例ID**: TC-ROLE-003
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/roles/name/{name}`
|
||||
2. 验证返回正确的角色信息
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回指定名称的角色
|
||||
|
||||
#### 3.4 查询所有角色
|
||||
**用例ID**: TC-ROLE-004
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/roles`
|
||||
2. 验证返回角色列表
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回角色数组
|
||||
|
||||
#### 3.5 更新角色
|
||||
**用例ID**: TC-ROLE-005
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送PUT请求到 `/api/roles/{id}`
|
||||
2. 提供更新的角色信息
|
||||
3. 验证更新成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回更新后的角色信息
|
||||
|
||||
#### 3.6 删除角色
|
||||
**用例ID**: TC-ROLE-006
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送DELETE请求到 `/api/roles/{id}`
|
||||
2. 验证删除成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 204
|
||||
|
||||
#### 3.7 逻辑删除角色
|
||||
**用例ID**: TC-ROLE-007
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送DELETE请求到 `/api/roles/{id}/logical`
|
||||
2. 验证逻辑删除成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 角色被标记为已删除
|
||||
|
||||
#### 3.8 恢复已删除角色
|
||||
**用例ID**: TC-ROLE-008
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/roles/{id}/restore`
|
||||
2. 验证恢复成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 角色状态恢复正常
|
||||
|
||||
#### 3.9 角色名唯一性检查
|
||||
**用例ID**: TC-ROLE-009
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/roles/check/name?name=existing`
|
||||
2. 验证返回true
|
||||
3. 使用不存在的角色名验证返回false
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回正确的布尔值
|
||||
|
||||
### 4. 字典管理测试
|
||||
|
||||
#### 4.1 创建字典
|
||||
**用例ID**: TC-DICT-001
|
||||
**优先级**: P0
|
||||
**前置条件**: 已登录,具有ADMIN权限
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/dictionaries`
|
||||
2. 提供字典信息
|
||||
3. 验证字典创建成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 201
|
||||
- 返回创建的字典信息
|
||||
|
||||
**测试数据**:
|
||||
```json
|
||||
{
|
||||
"type": "USER_STATUS",
|
||||
"code": "ACTIVE",
|
||||
"name": "激活",
|
||||
"value": "1",
|
||||
"remark": "用户激活状态",
|
||||
"sort": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 查询字典
|
||||
**用例ID**: TC-DICT-002
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/dictionaries/{id}`
|
||||
2. 验证返回正确的字典信息
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回完整的字典信息
|
||||
|
||||
#### 4.3 按类型查询字典
|
||||
**用例ID**: TC-DICT-003
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/dictionaries/type/{type}`
|
||||
2. 验证返回指定类型的字典列表
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回指定类型的字典数组
|
||||
- 按sort字段排序
|
||||
|
||||
#### 4.4 查询所有字典
|
||||
**用例ID**: TC-DICT-004
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/dictionaries`
|
||||
2. 验证返回字典列表
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回字典数组
|
||||
|
||||
#### 4.5 更新字典
|
||||
**用例ID**: TC-DICT-005
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送PUT请求到 `/api/dictionaries/{id}`
|
||||
2. 提供更新的字典信息
|
||||
3. 验证更新成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回更新后的字典信息
|
||||
|
||||
#### 4.6 删除字典
|
||||
**用例ID**: TC-DICT-006
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 发送DELETE请求到 `/api/dictionaries/{id}`
|
||||
2. 验证删除成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 204
|
||||
|
||||
#### 4.7 字典类型和编码唯一性检查
|
||||
**用例ID**: TC-DICT-007
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/dictionaries/check/exists?type=TYPE&code=CODE`
|
||||
2. 验证返回true或false
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回正确的布尔值
|
||||
|
||||
### 5. OAuth2客户端管理测试
|
||||
|
||||
#### 5.1 创建OAuth2客户端
|
||||
**用例ID**: TC-OAUTH2-001
|
||||
**优先级**: P1
|
||||
**前置条件**: 已登录,具有ADMIN权限
|
||||
**测试步骤**:
|
||||
1. 发送POST请求到 `/api/oauth2/clients`
|
||||
2. 提供客户端信息
|
||||
3. 验证客户端创建成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 201
|
||||
- 返回创建的客户端信息
|
||||
- clientSecret已加密
|
||||
|
||||
**测试数据**:
|
||||
```json
|
||||
{
|
||||
"clientId": "test-client",
|
||||
"clientSecret": "secret123",
|
||||
"clientName": "Test Client",
|
||||
"webServerRedirectUri": "http://localhost:8080/callback",
|
||||
"scope": "read,write",
|
||||
"authorizedGrantTypes": "authorization_code,refresh_token",
|
||||
"accessTokenValiditySeconds": 7200,
|
||||
"refreshTokenValiditySeconds": 2592000,
|
||||
"autoApprove": false,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 查询OAuth2客户端
|
||||
**用例ID**: TC-OAUTH2-002
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/oauth2/clients/{id}`
|
||||
2. 验证返回正确的客户端信息
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回完整的客户端信息
|
||||
|
||||
#### 5.3 按clientId查询OAuth2凭证
|
||||
**用例ID**: TC-OAUTH2-003
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/oauth2/clients/client-id/{clientId}`
|
||||
2. 验证返回正确的客户端信息
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回指定clientId的客户端
|
||||
|
||||
#### 5.4 查询所有OAuth2客户端
|
||||
**用例ID**: TC-OAUTH2-004
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送GET请求到 `/api/oauth2/clients`
|
||||
2. 验证返回客户端列表
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回客户端数组
|
||||
|
||||
#### 5.5 更新OAuth2客户端
|
||||
**用例ID**: TC-OAUTH2-005
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送PUT请求到 `/api/oauth2/clients/{id}`
|
||||
2. 提供更新的客户端信息
|
||||
3. 验证更新成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 返回更新后的客户端信息
|
||||
|
||||
#### 5.6 删除OAuth2客户端
|
||||
**用例ID**: TC-OAUTH2-006
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送DELETE请求到 `/api/oauth2/clients/{id}`
|
||||
2. 验证删除成功
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 204
|
||||
|
||||
### 6. 权限验证测试
|
||||
|
||||
#### 6.1 无token访问受保护资源
|
||||
**用例ID**: TC-PERM-001
|
||||
**优先级**: P0
|
||||
**测试步骤**:
|
||||
1. 不携带token访问需要认证的API
|
||||
2. 验证返回401
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 401
|
||||
- 返回认证失败消息
|
||||
|
||||
#### 6.2 使用过期token访问
|
||||
**用例ID**: TC-PERM-002
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 使用已过期的token访问API
|
||||
2. 验证返回401
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 401
|
||||
- 返回token无效消息
|
||||
|
||||
#### 6.3 使用已登出的token访问
|
||||
**用例ID**: TC-PERM-003
|
||||
**优先级**: P1
|
||||
**前置条件**: token已被登出
|
||||
**测试步骤**:
|
||||
1. 使用已加入黑名单的token访问API
|
||||
2. 验证返回401
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 401
|
||||
- 返回token无效消息
|
||||
|
||||
#### 6.4 无权限访问资源
|
||||
**用例ID**: TC-PERM-004
|
||||
**优先级**: P1
|
||||
**前置条件**: 用户没有访问资源的权限
|
||||
**测试步骤**:
|
||||
1. 使用普通用户token访问需要ADMIN权限的资源
|
||||
2. 验证返回403
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 403
|
||||
- 返回权限不足消息
|
||||
|
||||
### 7. 边界条件和异常测试
|
||||
|
||||
#### 7.1 创建重复用户名
|
||||
**用例ID**: TC-EDGE-001
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 创建用户A
|
||||
2. 使用相同的用户名创建用户B
|
||||
3. 验证返回错误
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 400或409
|
||||
- 返回用户名已存在错误
|
||||
|
||||
#### 7.2 创建重复邮箱
|
||||
**用例ID**: TC-EDGE-002
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 创建用户A
|
||||
2. 使用相同的邮箱创建用户B
|
||||
3. 验证返回错误
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 400或409
|
||||
- 返回邮箱已存在错误
|
||||
|
||||
#### 7.3 查询不存在的资源
|
||||
**用例ID**: TC-EDGE-003
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 查询不存在的用户ID
|
||||
2. 验证返回404
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 404
|
||||
- 返回资源未找到消息
|
||||
|
||||
#### 7.4 无效的请求参数
|
||||
**用例ID**: TC-EDGE-004
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 发送缺少必填字段的请求
|
||||
2. 验证返回400
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 400
|
||||
- 返回参数验证错误
|
||||
|
||||
#### 7.5 超长字段输入
|
||||
**用例ID**: TC-EDGE-005
|
||||
**优先级**: P2
|
||||
**测试步骤**:
|
||||
1. 发送超长用户名或邮箱
|
||||
2. 验证返回400
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 400
|
||||
- 返回字段长度超限错误
|
||||
|
||||
### 8. 性能测试
|
||||
|
||||
#### 8.1 并发登录测试
|
||||
**用例ID**: TC-PERF-001
|
||||
**优先级**: P2
|
||||
**测试步骤**:
|
||||
1. 模拟100个并发登录请求
|
||||
2. 验证所有请求都能正常响应
|
||||
3. 记录响应时间
|
||||
|
||||
**预期结果**:
|
||||
- 所有请求成功
|
||||
- 平均响应时间 < 500ms
|
||||
- 无错误发生
|
||||
|
||||
#### 8.2 批量查询性能测试
|
||||
**用例ID**: TC-PERF-002
|
||||
**优先级**: P2
|
||||
**测试步骤**:
|
||||
1. 创建1000个测试用户
|
||||
2. 查询所有用户
|
||||
3. 记录响应时间
|
||||
|
||||
**预期结果**:
|
||||
- HTTP状态码: 200
|
||||
- 响应时间 < 1000ms
|
||||
- 返回完整数据
|
||||
|
||||
### 9. 数据一致性测试
|
||||
|
||||
#### 9.1 缓存一致性测试
|
||||
**用例ID**: TC-CONSIST-001
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 查询用户A(首次查询,从数据库)
|
||||
2. 更新用户A
|
||||
3. 再次查询用户A(应从缓存获取)
|
||||
4. 验证返回更新后的数据
|
||||
|
||||
**预期结果**:
|
||||
- 第二次查询返回更新后的数据
|
||||
- 缓存被正确失效
|
||||
|
||||
#### 9.2 审计日志测试
|
||||
**用例ID**: TC-CONSIST-002
|
||||
**优先级**: P1
|
||||
**测试步骤**:
|
||||
1. 创建用户
|
||||
2. 更新用户
|
||||
3. 删除用户
|
||||
4. 查询审计日志
|
||||
5. 验证所有操作都被记录
|
||||
|
||||
**预期结果**:
|
||||
- 所有操作都被记录
|
||||
- 审计日志包含完整的变更信息
|
||||
|
||||
## 测试执行计划
|
||||
|
||||
### 测试优先级
|
||||
- P0: 核心功能,必须全部通过
|
||||
- P1: 重要功能,应该全部通过
|
||||
- P2: 辅助功能,尽量通过
|
||||
|
||||
### 测试顺序
|
||||
1. 认证授权测试
|
||||
2. 用户管理测试
|
||||
3. 角色管理测试
|
||||
4. 字典管理测试
|
||||
5. OAuth2客户端管理测试
|
||||
6. 权限验证测试
|
||||
7. 边界条件和异常测试
|
||||
8. 性能测试
|
||||
9. 数据一致性测试
|
||||
|
||||
### 测试数据准备
|
||||
- 自动化创建测试用户、角色、字典数据
|
||||
- 测试完成后自动清理
|
||||
- 使用事务回滚确保测试隔离
|
||||
|
||||
## 测试报告
|
||||
|
||||
测试报告应包含以下内容:
|
||||
1. 测试执行摘要
|
||||
2. 通过/失败用例统计
|
||||
3. 失败用例详情
|
||||
4. 性能指标
|
||||
5. 缺陷列表
|
||||
6. 测试覆盖率
|
||||
|
||||
## 缺陷分类
|
||||
|
||||
- 严重: 系统崩溃、数据丢失
|
||||
- 高: 核心功能不可用
|
||||
- 中: 功能部分不可用
|
||||
- 低: 界面、文案问题
|
||||
@@ -0,0 +1,339 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
description: 6A工作流
|
||||
---
|
||||
|
||||
# 6A 工作流执行规范
|
||||
|
||||
## 概述
|
||||
|
||||
6A 工作流是一种系统化的软件开发方法论,通过六个阶段确保项目高质量交付:
|
||||
|
||||
Align(对齐) → Architect(架构) → Atomize(原子化) → Approve(审批) → Automate(自动化) → Assess(评估)
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1: Align 对齐阶段
|
||||
|
||||
### 🎯 目标
|
||||
|
||||
```
|
||||
|
||||
|
||||
模糊需求 → 精确规范
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 📋 执行步骤
|
||||
|
||||
1.**项目上下文分析**
|
||||
|
||||
- 分析现有项目结构、技术栈、架构模式、依赖关系
|
||||
- 分析现有代码模式、文档和约定
|
||||
- 理解业务域和数据模型
|
||||
|
||||
2.**需求理解确认**
|
||||
|
||||
- 创建 `.trae/docs/任务名/ALIGNMENT_[任务名].md`
|
||||
- 包含项目和任务特性规范
|
||||
- 包含原始需求、边界确认、需求理解、疑问澄清
|
||||
|
||||
3.**智能决策策略**
|
||||
|
||||
- 自动识别歧义和不确定性
|
||||
- 生成结构化问题清单(按优先级排序)
|
||||
- 优先基于现有项目内容和行业知识进行决策
|
||||
- 有人员倾向或不确定的问题主动中断并询问
|
||||
- 基于回答更新理解和规范
|
||||
|
||||
4.**中断并询问关键决策点**
|
||||
|
||||
- 主动中断询问,迭代执行智能决策策略
|
||||
|
||||
5.**最终共识**
|
||||
|
||||
- 生成 `.trae/docs/任务名/CONSENSUS_[任务名].md` 包含:
|
||||
- 明确的需求描述和验收标准
|
||||
- 技术实现方案、技术约束和集成方案
|
||||
- 任务边界限制和验收标准
|
||||
- 确认所有不确定性已解决
|
||||
|
||||
### ✅ 质量门控
|
||||
|
||||
- [ ] 需求边界清晰无歧义
|
||||
- [ ] 技术方案与现有架构对齐
|
||||
- [ ] 验收标准具体可测试
|
||||
- [ ] 所有关键假设已确认
|
||||
- [ ] 项目特性规范已对齐
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2: Architect 架构阶段
|
||||
|
||||
### 🎯 目标
|
||||
|
||||
```
|
||||
|
||||
|
||||
共识文档 → 系统架构 → 模块设计 → 接口规范
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 📋 执行步骤
|
||||
|
||||
1.**系统分层设计**
|
||||
|
||||
- 基于 CONSENSUS、ALIGNMENT 文档设计架构
|
||||
- 生成 `.trae/docs/任务名/DESIGN_[任务名].md` 包含:
|
||||
- 整体架构图(mermaid 绘制)
|
||||
- 分层设计和核心组件
|
||||
- 模块依赖关系图
|
||||
- 接口契约定义
|
||||
- 数据流向图
|
||||
- 异常处理策略
|
||||
|
||||
2.**设计原则**
|
||||
|
||||
- 严格按照任务范围,避免过度设计
|
||||
- 确保与现有系统架构一致
|
||||
- 复用现有组件和模式
|
||||
|
||||
### ✅ 质量门控
|
||||
|
||||
- [ ] 架构图清晰准确
|
||||
- [ ] 接口定义完整
|
||||
- [ ] 与现有系统无冲突
|
||||
- [ ] 设计可行性验证
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3: Atomize 原子化阶段
|
||||
|
||||
### 🎯 目标
|
||||
|
||||
```
|
||||
|
||||
|
||||
架构设计 → 拆分任务 → 明确接口 → 依赖关系
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 📋 执行步骤
|
||||
|
||||
1.**子任务拆分**
|
||||
|
||||
- 基于 DESIGN 文档生成 `.trae/docs/任务名/TASK_[任务名].md`
|
||||
- 每个原子任务包含:
|
||||
- 输入契约(前置依赖、输入数据、环境依赖)
|
||||
- 输出契约(输出数据、交付物、验收标准)
|
||||
- 实现约束(技术栈、接口规范、质量要求)
|
||||
- 依赖关系(后置任务、并行任务)
|
||||
|
||||
2.**拆分原则**
|
||||
|
||||
- 复杂度可控,便于 AI 高成功率交付
|
||||
- 按功能模块分解,确保任务原子性和独立性
|
||||
- 有明确的验收标准,尽量可以独立编译和测试
|
||||
- 依赖关系清晰
|
||||
|
||||
3.**生成任务依赖图**
|
||||
|
||||
- 使用 mermaid 绘制任务依赖关系图
|
||||
|
||||
### ✅ 质量门控
|
||||
|
||||
- [ ] 任务覆盖完整需求
|
||||
- [ ] 依赖关系无循环
|
||||
- [ ] 每个任务都可独立验证
|
||||
- [ ] 复杂度评估合理
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4: Approve 审批阶段
|
||||
|
||||
### 🎯 目标
|
||||
|
||||
```
|
||||
|
||||
|
||||
原子任务 → 人工审查 → 迭代修改 → 按文档执行
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 📋 执行步骤
|
||||
|
||||
1.**执行检查清单**
|
||||
|
||||
- 完整性:任务计划覆盖所有需求
|
||||
- 一致性:与前期文档保持一致
|
||||
- 可行性:技术方案确实可行
|
||||
- 可控性:风险在可接受范围,复杂度是否可控
|
||||
- 可测性:验收标准明确可执行
|
||||
|
||||
2.**最终确认清单**
|
||||
|
||||
- 明确的实现需求(无歧义)
|
||||
- 明确的子任务定义
|
||||
- 明确的边界和限制
|
||||
- 明确的验收标准
|
||||
- 代码、测试、文档质量标准
|
||||
|
||||
---
|
||||
|
||||
## 阶段 5: Automate 自动化执行
|
||||
|
||||
### 🎯 目标
|
||||
|
||||
```
|
||||
|
||||
|
||||
按节点执行 → 编写测试 → 实现代码 → 文档同步
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 📋 执行步骤
|
||||
|
||||
1.**逐步实施子任务**
|
||||
|
||||
- 创建 `.trae/docs/任务名/ACCEPTANCE_[任务名].md` 记录完成情况
|
||||
|
||||
2.**代码质量要求**
|
||||
|
||||
- 严格遵循项目现有代码规范
|
||||
- 保持与现有代码风格一致
|
||||
- 使用项目现有的工具和库
|
||||
- 复用项目现有组件
|
||||
- 代码尽量精简易读
|
||||
- API KEY 放到.env 文件中并且不要提交 git
|
||||
|
||||
3.**异常处理**
|
||||
|
||||
- 遇到不确定问题立刻中断执行
|
||||
- 在 TASK 文档中记录问题详细信息和位置
|
||||
- 寻求人工澄清后继续
|
||||
|
||||
4.**逐步实施流程**
|
||||
|
||||
按任务依赖顺序执行,对每个子任务执行:
|
||||
|
||||
- 执行前检查(验证输入契约、环境准备、依赖满足)
|
||||
- 实现核心逻辑(按设计文档编写代码)
|
||||
- 编写单元测试(边界条件、异常情况)
|
||||
- 运行验证测试
|
||||
- 更新相关文档
|
||||
- 每完成一个任务立即验证
|
||||
|
||||
---
|
||||
|
||||
## 阶段 6: Assess 评估阶段
|
||||
|
||||
### 🎯 目标
|
||||
|
||||
```
|
||||
|
||||
|
||||
执行结果 → 质量评估 → 文档更新 → 交付确认
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 📋 执行步骤
|
||||
|
||||
1.**验证执行结果**
|
||||
|
||||
- 更新 `.trae/docs/任务名/ACCEPTANCE_[任务名].md`
|
||||
- 整体验收检查:
|
||||
- 所有需求已实现
|
||||
- 验收标准全部满足
|
||||
- 项目编译通过
|
||||
- 所有测试通过
|
||||
- 功能完整性验证
|
||||
- 实现与设计文档一致
|
||||
|
||||
2.**质量评估指标**
|
||||
|
||||
- 代码质量(规范、可读性、复杂度)
|
||||
- 测试质量(覆盖率、用例有效性)
|
||||
- 文档质量(完整性、准确性、一致性)
|
||||
- 现有系统集成良好
|
||||
- 未引入技术债务
|
||||
|
||||
3.**最终交付物**
|
||||
|
||||
- 生成 `.trae/docs/任务名/FINAL_[任务名].md`(项目总结报告)
|
||||
- 生成 `.trae/docs/任务名/TODO_[任务名].md`(待办事宜和缺少的配置等)
|
||||
|
||||
4.**TODO 询问**
|
||||
|
||||
- 询问用户 TODO 的解决方式
|
||||
- 精简明确待办事宜和缺少的配置
|
||||
- 提供有用的操作指引
|
||||
|
||||
---
|
||||
|
||||
## 技术执行规范
|
||||
|
||||
### 🔐 安全规范
|
||||
|
||||
- API 密钥等敏感信息使用.env 文件管理
|
||||
|
||||
### 📝 文档同步
|
||||
|
||||
- 代码变更同时更新相关文档
|
||||
|
||||
### 🧪 测试策略
|
||||
|
||||
- 测试优先:先写测试,后写实现
|
||||
- 边界覆盖:覆盖正常流程、边界条件、异常情况
|
||||
|
||||
### 💡 交互体验优化
|
||||
|
||||
#### 进度反馈
|
||||
|
||||
- 显示当前执行阶段
|
||||
- 提供详细的执行步骤
|
||||
- 标示完成情况
|
||||
- 突出需要关注的问题
|
||||
|
||||
#### 异常处理机制
|
||||
|
||||
##### 中断条件
|
||||
|
||||
- 遇到无法自主决策的问题
|
||||
- 觉得需要询问用户的问题
|
||||
- 技术实现出现阻塞
|
||||
- 文档不一致需要确认修正
|
||||
|
||||
##### 恢复策略
|
||||
|
||||
- 保存当前执行状态
|
||||
- 记录问题详细信息
|
||||
- 询问并等待人工干预
|
||||
- 从中断点任务继续执行
|
||||
|
||||
---
|
||||
|
||||
## 附录:文档模板索引
|
||||
|
||||
| 阶段 | 文档名称 | 用途 |
|
||||
|
||||
| --------- | ----------------------- | -------------- |
|
||||
|
||||
| Align | ALIGNMENT\_[任务名].md | 需求理解与确认 |
|
||||
|
||||
| Align | CONSENSUS\_[任务名].md | 最终共识与规范 |
|
||||
|
||||
| Architect | DESIGN\_[任务名].md | 系统架构设计 |
|
||||
|
||||
| Atomize | TASK\_[任务名].md | 原子任务定义 |
|
||||
|
||||
| Automate | ACCEPTANCE\_[任务名].md | 执行过程记录 |
|
||||
|
||||
| Assess | FINAL\_[任务名].md | 项目总结报告 |
|
||||
|
||||
| Assess | TODO\_[任务名].md | 待办事宜清单 |
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"ssh": "Disabled",
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 55432,
|
||||
"driver": "PostgreSQL",
|
||||
"name": "local",
|
||||
"database": "postgres",
|
||||
"username": "postgres"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# novalon-manage-system
|
||||
|
||||
企业级后台管理系统
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
novalon-manage-system/
|
||||
├── novalon-manage-api/ # 后端 API 项目
|
||||
│ └── manage-sys/ # 系统管理模块
|
||||
├── novalon-manage-web/ # 前端 Web 项目
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- Java 21
|
||||
- Spring Boot 3.4.1
|
||||
- Spring Security
|
||||
- JWT Authentication
|
||||
- PostgreSQL
|
||||
|
||||
### 前端
|
||||
- Vue 3 + TypeScript
|
||||
- Ant Design Vue
|
||||
- Pinia
|
||||
- Vite
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 后端
|
||||
|
||||
```bash
|
||||
cd novalon-manage-api
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
### 前端
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
- 用户管理
|
||||
- 角色管理
|
||||
- 菜单管理
|
||||
- 权限管理
|
||||
- 操作日志
|
||||
- 系统配置 (规划中)
|
||||
- 审计中心 (规划中)
|
||||
- 通知中心 (规划中)
|
||||
- 文件管理 (规划中)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,435 @@
|
||||
# 系统配置与审计通知中心实施计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 完成系统配置(字典管理、系统参数)、审计中心(登录日志、操作日志、异常追踪)、通知中心(系统公告、消息推送WebSocket)、文件管理(上传/下载/预览)的数据库持久化与功能完善
|
||||
|
||||
**Architecture:** 基于Spring R2DBC响应式架构,遵循现有分层模式(Handler->Service->Repository->Entity),使用H2/PostgreSQL + Caffeine缓存,消息推送采用WebSocket
|
||||
|
||||
**Tech Stack:** Spring WebFlux, Spring Data R2DBC, H2/PostgreSQL, Caffeine, WebSocket, Lombok
|
||||
|
||||
---
|
||||
|
||||
## 第一阶段:数据层基础设施建设
|
||||
|
||||
### Task 1: 创建数据库表SQL脚本
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/sql/init.sql` (创建以下表)
|
||||
|
||||
**Step 1: 编写SQL脚本**
|
||||
|
||||
```sql
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
dict_name VARCHAR(100) NOT NULL COMMENT '字典名称',
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE COMMENT '字典类型',
|
||||
status VARCHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典数据表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
dict_sort INT DEFAULT 0 COMMENT '字典排序',
|
||||
dict_label VARCHAR(100) NOT NULL COMMENT '字典标签',
|
||||
dict_value VARCHAR(100) NOT NULL COMMENT '字典键值',
|
||||
dict_type VARCHAR(100) NOT NULL COMMENT '字典类型',
|
||||
css_class VARCHAR(100) COMMENT '样式属性',
|
||||
list_class VARCHAR(100) COMMENT '表格回显样式',
|
||||
is_default VARCHAR(1) DEFAULT 'N' COMMENT '是否默认(Y是 N否)',
|
||||
status VARCHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
config_name VARCHAR(100) NOT NULL COMMENT '配置名称',
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键名',
|
||||
config_value VARCHAR(500) NOT NULL COMMENT '配置值',
|
||||
config_type VARCHAR(1) DEFAULT 'N' COMMENT '系统内置(Y是 N否)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 登录日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) COMMENT '用户名',
|
||||
ip VARCHAR(50) COMMENT 'IP地址',
|
||||
location VARCHAR(255) COMMENT '登录位置',
|
||||
browser VARCHAR(50) COMMENT '浏览器类型',
|
||||
os VARCHAR(50) COMMENT '操作系统',
|
||||
status VARCHAR(1) COMMENT '登录状态(0成功 1失败)',
|
||||
message VARCHAR(255) COMMENT '提示消息',
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 异常日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) COMMENT '用户名',
|
||||
title VARCHAR(100) COMMENT '异常标题',
|
||||
exception_name VARCHAR(100) COMMENT '异常名称',
|
||||
method_name VARCHAR(100) COMMENT '方法名称',
|
||||
method_params TEXT COMMENT '方法参数',
|
||||
exception_msg TEXT COMMENT '异常信息',
|
||||
exception_stack TEXT COMMENT '堆栈信息',
|
||||
ip VARCHAR(50) COMMENT 'IP地址',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 系统公告表
|
||||
CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
notice_title VARCHAR(100) NOT NULL COMMENT '公告标题',
|
||||
notice_type VARCHAR(1) DEFAULT '1' COMMENT '公告类型(1通知 2公告)',
|
||||
notice_content TEXT COMMENT '公告内容',
|
||||
status VARCHAR(1) DEFAULT '0' COMMENT '公告状态(0正常 1关闭)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 文件管理表
|
||||
CREATE TABLE IF NOT EXISTS sys_file (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件名',
|
||||
file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
|
||||
file_size VARCHAR(50) COMMENT '文件大小',
|
||||
file_type VARCHAR(50) COMMENT '文件类型',
|
||||
storage_type VARCHAR(20) DEFAULT 'local' COMMENT '存储类型(local/oss/s3)',
|
||||
create_by VARCHAR(50) COMMENT '创建者',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户消息表(消息推送用)
|
||||
CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL COMMENT '接收用户ID',
|
||||
title VARCHAR(100) COMMENT '消息标题',
|
||||
content TEXT COMMENT '消息内容',
|
||||
message_type VARCHAR(1) DEFAULT '1' COMMENT '消息类型(1系统 2通知)',
|
||||
is_read VARCHAR(1) DEFAULT '0' COMMENT '是否已读(0未读 1已读)',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Step 2: 提交SQL脚本**
|
||||
|
||||
```bash
|
||||
git add docs/sql/init.sql
|
||||
git commit -m "feat: 添加系统配置和审计模块数据库表脚本"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 创建字典类型Entity
|
||||
|
||||
**Files:**
|
||||
- Create: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/entity/SysDictTypeEntity.java`
|
||||
- Modify: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/entity/BaseEntity.java`
|
||||
|
||||
**Step 1: 在BaseEntity添加公共字段**
|
||||
|
||||
查看现有BaseEntity确保包含:
|
||||
- id, createdAt, updatedAt, deletedAt
|
||||
|
||||
**Step 2: 创建SysDictTypeEntity**
|
||||
|
||||
```java
|
||||
package cn.novalon.manage.sys.infrastructure.db.entity;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@Table("sys_dict_type")
|
||||
public class SysDictTypeEntity {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("dict_name")
|
||||
private String dictName;
|
||||
|
||||
@Column("dict_type")
|
||||
private String dictType;
|
||||
|
||||
@Column("status")
|
||||
private String status;
|
||||
|
||||
@Column("remark")
|
||||
private String remark;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
// Getters and Setters
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add entity/SysDictTypeEntity.java
|
||||
git commit -m "feat: 添加字典类型Entity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 创建其他Entity(字典数据、系统配置、登录日志、异常日志、公告、文件、消息)
|
||||
|
||||
**Files:**
|
||||
- Create: `infrastructure/db/entity/SysDictDataEntity.java`
|
||||
- Create: `infrastructure/db/entity/SysConfigEntity.java`
|
||||
- Create: `infrastructure/db/entity/SysLoginLogEntity.java`
|
||||
- Create: `infrastructure/db/entity/SysExceptionLogEntity.java`
|
||||
- Create: `infrastructure/db/entity/SysNoticeEntity.java`
|
||||
- Create: `infrastructure/db/entity/SysFileEntity.java`
|
||||
- Create: `infrastructure/db/entity/SysUserMessageEntity.java`
|
||||
|
||||
**每Entity结构:** 继承BaseEntity,添加对应字段的@Column注解和getter/setter
|
||||
|
||||
**示例 - SysConfigEntity:**
|
||||
```java
|
||||
@Table("sys_config")
|
||||
public class SysConfigEntity {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("config_name")
|
||||
private String configName;
|
||||
|
||||
@Column("config_key")
|
||||
private String configKey;
|
||||
|
||||
@Column("config_value")
|
||||
private String configValue;
|
||||
|
||||
@Column("config_type")
|
||||
private String configType;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
// Getters and Setters
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 创建Repository接口
|
||||
|
||||
**Files:**
|
||||
- Create: `infrastructure/db/repository/SysDictTypeRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysDictDataRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysConfigRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysLoginLogRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysExceptionLogRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysNoticeRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysFileRepository.java`
|
||||
- Create: `infrastructure/db/repository/SysUserMessageRepository.java`
|
||||
|
||||
**每Repository:** 继承 ReactiveCrudRepository<Entity, Long>
|
||||
|
||||
**示例:**
|
||||
```java
|
||||
package cn.novalon.manage.sys.infrastructure.db.repository;
|
||||
|
||||
import cn.novalon.manage.sys.infrastructure.db.entity.SysDictTypeEntity;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SysDictTypeRepository extends ReactiveCrudRepository<SysDictTypeEntity, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第二阶段:领域层(Domain + Converter)
|
||||
|
||||
### Task 5: 更新Domain模型
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/domain/SysDictType.java` - 添加dictDataList字段
|
||||
- Modify: `core/domain/SysDictData.java` - 确保字段完整
|
||||
- Modify: `core/domain/SysConfig.java` - 确保字段完整
|
||||
- Modify: `core/domain/SysLoginLog.java` - 确保字段完整
|
||||
- Create: `core/domain/SysExceptionLog.java`
|
||||
- Create: `core/domain/SysUserMessage.java`
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 创建Converter转换器
|
||||
|
||||
**Files:**
|
||||
- Create: `infrastructure/db/converter/SysDictTypeConverter.java`
|
||||
- Create: `infrastructure/db/converter/SysDictDataConverter.java`
|
||||
- Create: `infrastructure/db/converter/SysConfigConverter.java`
|
||||
- Create: `infrastructure/db/converter/SysLoginLogConverter.java`
|
||||
- Create: `infrastructure/db/converter/SysExceptionLogConverter.java`
|
||||
- Create: `infrastructure/db/converter/SysNoticeConverter.java`
|
||||
- Create: `infrastructure/db/converter/SysFileConverter.java`
|
||||
|
||||
---
|
||||
|
||||
## 第三阶段:服务层(Service)
|
||||
|
||||
### Task 7: 创建Service接口和实现
|
||||
|
||||
**Files:**
|
||||
- Create: `core/service/ISysDictTypeService.java` + `impl/SysDictTypeServiceImpl.java`
|
||||
- Create: `core/service/ISysDictDataService.java` + `impl/SysDictDataServiceImpl.java`
|
||||
- Create: `core/service/ISysConfigService.java` + `impl/SysConfigServiceImpl.java`
|
||||
- Create: `core/service/ISysLoginLogService.java` + `impl/SysLoginLogServiceImpl.java`
|
||||
- Create: `core/service/ISysExceptionLogService.java` + `impl/SysExceptionLogServiceImpl.java`
|
||||
- Create: `core/service/ISysNoticeService.java` + `impl/SysNoticeServiceImpl.java`
|
||||
- Create: `core/service/ISysFileService.java` + `impl/SysFileServiceImpl.java`
|
||||
- Create: `core/service/ISysUserMessageService.java` + `impl/SysUserMessageServiceImpl.java`
|
||||
|
||||
**核心方法:**
|
||||
- CRUD操作
|
||||
- 字典: 根据类型查询数据列表
|
||||
- 配置: 根据key查询value
|
||||
- 日志: 分页查询、按条件筛选
|
||||
- 消息: 查询用户未读消息
|
||||
|
||||
---
|
||||
|
||||
## 第四阶段:Handler层(API接口)
|
||||
|
||||
### Task 8: 重构Handler接入数据库
|
||||
|
||||
**Files:**
|
||||
- Modify: `handler/sys/SysDictHandler.java` - 注入Service替换内存Map
|
||||
- Modify: `handler/sys/SysConfigHandler.java` - 注入Service替换内存Map
|
||||
- Modify: `handler/sys/SysLogHandler.java` - 添加登录日志和异常日志查询
|
||||
- Modify: `handler/sys/SysNoticeHandler.java` - 注入Service替换内存Map
|
||||
- Modify: `handler/sys/SysFileHandler.java` - 完善文件预览功能
|
||||
|
||||
---
|
||||
|
||||
## 第五阶段:消息推送(WebSocket)
|
||||
|
||||
### Task 9: WebSocket配置
|
||||
|
||||
**Files:**
|
||||
- Create: `config/WebSocketConfig.java`
|
||||
- Create: `handler/websocket/WebSocketHandler.java`
|
||||
- Create: `service/impl/WebSocketService.java`
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(webSocketHandler(), "/ws")
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketHandler webSocketHandler() {
|
||||
return new WebSocketHandler();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 消息推送Service
|
||||
|
||||
**Files:**
|
||||
- Create: `core/service/IWebSocketService.java`
|
||||
- Create: `core/service/impl/WebSocketServiceImpl.java`
|
||||
|
||||
**功能:**
|
||||
- 发送消息给指定用户
|
||||
- 发送消息给所有在线用户
|
||||
- 用户上线/下线跟踪
|
||||
|
||||
---
|
||||
|
||||
## 第六阶段:完善文件预览
|
||||
|
||||
### Task 11: 文件预览支持
|
||||
|
||||
**Files:**
|
||||
- Modify: `handler/sys/SysFileHandler.java` - 添加预览接口
|
||||
- 创建DTO: `dto/response/FilePreviewResponse.java`
|
||||
|
||||
**预览支持:**
|
||||
- 图片: 返回Base64或直接流
|
||||
- PDF: 返回PDF流
|
||||
- 文本: 返回文本内容
|
||||
|
||||
---
|
||||
|
||||
## 第七阶段:异常日志自动记录
|
||||
|
||||
### Task 12: 全局异常处理器
|
||||
|
||||
**Files:**
|
||||
- Create: `handler/GlobalExceptionHandler.java`
|
||||
- Create: `config/GlobalExceptionHandlerConfig.java`
|
||||
|
||||
**功能:**
|
||||
- 捕获所有Controller异常
|
||||
- 自动记录到sys_exception_log表
|
||||
- 返回统一格式的错误响应
|
||||
|
||||
---
|
||||
|
||||
## 第八阶段:测试
|
||||
|
||||
### Task 13: 单元测试
|
||||
|
||||
**Files:**
|
||||
- Create: `test/service/SysDictTypeServiceTest.java`
|
||||
- Create: `test/service/SysConfigServiceTest.java`
|
||||
- Create: `test/service/SysLoginLogServiceTest.java`
|
||||
- Create: `test/handler/SysDictHandlerTest.java`
|
||||
- Create: `test/handler/SysNoticeHandlerTest.java`
|
||||
|
||||
**测试覆盖:**
|
||||
- Service层CRUD操作
|
||||
- Handler层API响应
|
||||
- 异常场景处理
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. Task 1: SQL脚本
|
||||
2. Task 2-4: Entity + Repository
|
||||
3. Task 5-6: Domain + Converter
|
||||
4. Task 7: Service层
|
||||
5. Task 8: Handler层重构
|
||||
6. Task 9-10: WebSocket消息推送
|
||||
7. Task 11: 文件预览
|
||||
8. Task 12: 异常日志
|
||||
9. Task 13: 测试
|
||||
@@ -0,0 +1,147 @@
|
||||
-- 系统配置与审计通知中心数据库表脚本
|
||||
-- 数据库: H2/PostgreSQL
|
||||
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
dict_name VARCHAR(100) NOT NULL COMMENT '字典名称',
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE COMMENT '字典类型',
|
||||
status VARCHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典数据表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
dict_sort INT DEFAULT 0 COMMENT '字典排序',
|
||||
dict_label VARCHAR(100) NOT NULL COMMENT '字典标签',
|
||||
dict_value VARCHAR(100) NOT NULL COMMENT '字典键值',
|
||||
dict_type VARCHAR(100) NOT NULL COMMENT '字典类型',
|
||||
css_class VARCHAR(100) COMMENT '样式属性',
|
||||
list_class VARCHAR(100) COMMENT '表格回显样式',
|
||||
is_default VARCHAR(1) DEFAULT 'N' COMMENT '是否默认(Y是 N否)',
|
||||
status VARCHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
config_name VARCHAR(100) NOT NULL COMMENT '配置名称',
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键名',
|
||||
config_value VARCHAR(500) NOT NULL COMMENT '配置值',
|
||||
config_type VARCHAR(1) DEFAULT 'N' COMMENT '系统内置(Y是 N否)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 登录日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) COMMENT '用户名',
|
||||
ip VARCHAR(50) COMMENT 'IP地址',
|
||||
location VARCHAR(255) COMMENT '登录位置',
|
||||
browser VARCHAR(50) COMMENT '浏览器类型',
|
||||
os VARCHAR(50) COMMENT '操作系统',
|
||||
status VARCHAR(1) COMMENT '登录状态(0成功 1失败)',
|
||||
message VARCHAR(255) COMMENT '提示消息',
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 异常日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) COMMENT '用户名',
|
||||
title VARCHAR(100) COMMENT '异常标题',
|
||||
exception_name VARCHAR(100) COMMENT '异常名称',
|
||||
method_name VARCHAR(100) COMMENT '方法名称',
|
||||
method_params TEXT COMMENT '方法参数',
|
||||
exception_msg TEXT COMMENT '异常信息',
|
||||
exception_stack TEXT COMMENT '堆栈信息',
|
||||
ip VARCHAR(50) COMMENT 'IP地址',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 系统公告表
|
||||
CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
notice_title VARCHAR(100) NOT NULL COMMENT '公告标题',
|
||||
notice_type VARCHAR(1) DEFAULT '1' COMMENT '公告类型(1通知 2公告)',
|
||||
notice_content TEXT COMMENT '公告内容',
|
||||
status VARCHAR(1) DEFAULT '0' COMMENT '公告状态(0正常 1关闭)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 文件管理表
|
||||
CREATE TABLE IF NOT EXISTS sys_file (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件名',
|
||||
file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
|
||||
file_size VARCHAR(50) COMMENT '文件大小',
|
||||
file_type VARCHAR(50) COMMENT '文件类型',
|
||||
storage_type VARCHAR(20) DEFAULT 'local' COMMENT '存储类型(local/oss/s3)',
|
||||
create_by VARCHAR(50) COMMENT '创建者',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户消息表(消息推送用)
|
||||
CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL COMMENT '接收用户ID',
|
||||
title VARCHAR(100) COMMENT '消息标题',
|
||||
content TEXT COMMENT '消息内容',
|
||||
message_type VARCHAR(1) DEFAULT '1' COMMENT '消息类型(1系统 2通知)',
|
||||
is_read VARCHAR(1) DEFAULT '0' COMMENT '是否已读(0未读 1已读)',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 初始化默认系统配置数据
|
||||
INSERT INTO sys_config (config_name, config_key, config_value, config_type) VALUES
|
||||
('系统名称', 'sys.system.name', 'Novalon管理系统', 'Y'),
|
||||
('系统版本', 'sys.system.version', '1.0.0', 'Y'),
|
||||
('文件上传最大大小', 'sys.file.maxSize', '10485760', 'Y'),
|
||||
('文件上传允许类型', 'sys.file.allowedTypes', 'jpg,jpeg,png,pdf,doc,docx,xls,xlsx', 'Y'),
|
||||
('会话超时时间(分钟)', 'sys.session.timeout', '30', 'Y'),
|
||||
('密码最小长度', 'sys.password.minLength', '6', 'Y');
|
||||
|
||||
-- 初始化默认字典类型
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark) VALUES
|
||||
('用户性别', 'sys_user_sex', '0', '用户性别列表'),
|
||||
('菜单状态', 'sys_show_hide', '0', '菜单显示状态'),
|
||||
('系统开关', 'sys_normal_disable', '0', '系统开关状态'),
|
||||
('任务状态', 'sys_job_status', '0', '定时任务状态'),
|
||||
('任务分组', 'sys_job_group', '0', '定时任务分组'),
|
||||
('系统是否', 'sys_yes_no', '0', '系统是否列表');
|
||||
|
||||
-- 初始化默认字典数据
|
||||
INSERT INTO sys_dict_data (dict_label, dict_value, dict_type, dict_sort, is_default, status) VALUES
|
||||
('男', '0', 'sys_user_sex', 1, 'Y', '0'),
|
||||
('女', '1', 'sys_user_sex', 2, 'N', '0'),
|
||||
('未知', '2', 'sys_user_sex', 3, 'N', '0'),
|
||||
('显示', '0', 'sys_show_hide', 1, 'Y', '0'),
|
||||
('隐藏', '1', 'sys_show_hide', 2, 'N', '0'),
|
||||
('正常', '0', 'sys_normal_disable', 1, 'Y', '0'),
|
||||
('停用', '1', 'sys_normal_disable', 2, 'N', '0'),
|
||||
('是', 'Y', 'sys_yes_no', 1, 'Y', '0'),
|
||||
('否', 'N', 'sys_yes_no', 2, 'N', '0');
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_sys_dict_type_dict_type ON sys_dict_type(dict_type);
|
||||
CREATE INDEX idx_sys_dict_data_dict_type ON sys_dict_data(dict_type);
|
||||
CREATE INDEX idx_sys_config_config_key ON sys_config(config_key);
|
||||
CREATE INDEX idx_sys_login_log_username ON sys_login_log(username);
|
||||
CREATE INDEX idx_sys_login_log_login_time ON sys_login_log(login_time);
|
||||
CREATE INDEX idx_sys_exception_log_create_time ON sys_exception_log(create_time);
|
||||
CREATE INDEX idx_sys_notice_status ON sys_notice(status);
|
||||
CREATE INDEX idx_sys_file_create_by ON sys_file(create_by);
|
||||
CREATE INDEX idx_sys_user_message_user_id ON sys_user_message(user_id);
|
||||
CREATE INDEX idx_sys_user_message_is_read ON sys_user_message(is_read);
|
||||
@@ -0,0 +1,22 @@
|
||||
# E2E测试环境配置
|
||||
|
||||
# API配置
|
||||
API_BASE_URL=http://localhost:8080
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_NAME=manage_system
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=postgres
|
||||
|
||||
# 测试用户凭证
|
||||
TEST_USERNAME=admin
|
||||
TEST_PASSWORD=admin123
|
||||
|
||||
# 浏览器配置
|
||||
HEADLESS_BROWSER=true
|
||||
BROWSER_TYPE=chromium
|
||||
|
||||
# 超时配置(毫秒)
|
||||
REQUEST_TIMEOUT=30000
|
||||
@@ -0,0 +1,55 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Pytest
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Allure
|
||||
allure-results/
|
||||
allure-report/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Playwright
|
||||
.playwright/
|
||||
@@ -0,0 +1,104 @@
|
||||
# E2E测试项目
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目使用Python + Playwright框架对Novalon管理系统进行端到端测试。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Python**: 3.9+
|
||||
- **Playwright**: 1.40+
|
||||
- **Pytest**: 7.0+
|
||||
- **Allure**: 测试报告
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
e2e_tests/
|
||||
├── __init__.py
|
||||
├── conftest.py # Pytest配置和fixtures
|
||||
├── pytest.ini # Pytest配置文件
|
||||
├── requirements.txt # Python依赖
|
||||
├── config/
|
||||
│ ├── __init__.py
|
||||
│ └── settings.py # 配置管理
|
||||
├── pages/
|
||||
│ ├── __init__.py
|
||||
│ ├── base_page.py # 基础页面类
|
||||
│ ├── auth_page.py # 认证相关页面
|
||||
│ ├── user_page.py # 用户管理页面
|
||||
│ ├── role_page.py # 角色管理页面
|
||||
│ └── dictionary_page.py # 字典管理页面
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── base_api.py # 基础API类
|
||||
│ ├── auth_api.py # 认证API
|
||||
│ ├── user_api.py # 用户API
|
||||
│ ├── role_api.py # 角色API
|
||||
│ └── dictionary_api.py # 字典API
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── test_auth.py # 认证测试
|
||||
│ ├── test_user.py # 用户管理测试
|
||||
│ ├── test_role.py # 角色管理测试
|
||||
│ ├── test_dictionary.py # 字典管理测试
|
||||
│ └── test_oauth2.py # OAuth2测试
|
||||
├── utils/
|
||||
│ ├── __init__.py
|
||||
│ ├── data_generator.py # 测试数据生成器
|
||||
│ ├── assertions.py # 断言工具
|
||||
│ └── logger.py # 日志工具
|
||||
└── reports/ # 测试报告目录
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装Python依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 安装Playwright浏览器
|
||||
playwright install
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
pytest
|
||||
|
||||
# 运行特定测试文件
|
||||
pytest tests/test_auth.py
|
||||
|
||||
# 运行特定测试用例
|
||||
pytest tests/test_auth.py::test_login_success
|
||||
|
||||
# 生成Allure报告
|
||||
pytest --alluredir=allure-results
|
||||
allure serve allure-results
|
||||
|
||||
# 并发运行测试
|
||||
pytest -n auto
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
在 `config/settings.py` 中配置:
|
||||
- API基础URL
|
||||
- 测试数据库连接
|
||||
- 测试用户凭证
|
||||
- 超时设置
|
||||
|
||||
## 测试数据管理
|
||||
|
||||
测试数据自动准备和清理:
|
||||
- 每个测试用例独立运行
|
||||
- 使用fixture自动创建和清理测试数据
|
||||
- 支持数据回滚
|
||||
|
||||
## 持续集成
|
||||
|
||||
测试可在CI/CD流程中自动运行:
|
||||
- GitHub Actions
|
||||
- GitLab CI
|
||||
- Jenkins
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
E2E测试项目 - Novalon管理系统
|
||||
使用Playwright进行端到端测试
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@@ -0,0 +1 @@
|
||||
"""API模块"""
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
认证API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class AuthAPI(BaseAPI):
|
||||
"""认证API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/auth")
|
||||
|
||||
async def login(self, username: str, password: str) -> Response:
|
||||
"""用户登录"""
|
||||
return await self.post("/login", json={
|
||||
"username": username,
|
||||
"password": password
|
||||
})
|
||||
|
||||
async def refresh_token(self, refresh_token: str) -> Response:
|
||||
"""刷新token"""
|
||||
return await self.post("/refresh", json={
|
||||
"refreshToken": refresh_token
|
||||
})
|
||||
|
||||
async def logout(self, token: str) -> Response:
|
||||
"""用户登出"""
|
||||
return await self.post("/logout", headers={
|
||||
"Authorization": f"Bearer {token}"
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
基础API类
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from httpx import AsyncClient, Response
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class BaseAPI:
|
||||
"""基础API类"""
|
||||
|
||||
def __init__(self, client: AsyncClient, base_url: str = ""):
|
||||
self.client = client
|
||||
self.base_url = base_url
|
||||
|
||||
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> Response:
|
||||
"""GET请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"GET {url} - Params: {params}")
|
||||
response = await self.client.get(url, params=params, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None, **kwargs) -> Response:
|
||||
"""POST请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"POST {url} - Data: {data} - JSON: {json}")
|
||||
response = await self.client.post(url, data=data, json=json, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None, **kwargs) -> Response:
|
||||
"""PUT请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"PUT {url} - Data: {data} - JSON: {json}")
|
||||
response = await self.client.put(url, data=data, json=json, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def delete(self, endpoint: str, **kwargs) -> Response:
|
||||
"""DELETE请求"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
logger.info(f"DELETE {url}")
|
||||
response = await self.client.delete(url, **kwargs)
|
||||
logger.info(f"Response: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def assert_status_code(self, response: Response, expected_status: int):
|
||||
"""断言状态码"""
|
||||
assert response.status_code == expected_status, f"Expected {expected_status}, got {response.status_code}. Response: {response.text}"
|
||||
|
||||
async def assert_response_contains(self, response: Response, key: str, value: Any = None):
|
||||
"""断言响应包含指定字段"""
|
||||
data = response.json()
|
||||
assert key in data, f"Response does not contain key '{key}'"
|
||||
if value is not None:
|
||||
assert data[key] == value, f"Expected {value}, got {data[key]}"
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
字典管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class DictionaryAPI(BaseAPI):
|
||||
"""字典管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/dictionaries")
|
||||
|
||||
async def create_dictionary(self, dict_data: Dict[str, Any]) -> Response:
|
||||
"""创建字典"""
|
||||
return await self.post("", json=dict_data)
|
||||
|
||||
async def get_dictionary_by_id(self, dict_id: int) -> Response:
|
||||
"""根据ID获取字典"""
|
||||
return await self.get(f"/{dict_id}")
|
||||
|
||||
async def get_dictionaries_by_type(self, dict_type: str) -> Response:
|
||||
"""根据类型获取字典"""
|
||||
return await self.get(f"/type/{dict_type}")
|
||||
|
||||
async def get_all_dictionaries(self) -> Response:
|
||||
"""获取所有字典"""
|
||||
return await self.get("")
|
||||
|
||||
async def update_dictionary(self, dict_id: int, dict_data: Dict[str, Any]) -> Response:
|
||||
"""更新字典"""
|
||||
return await self.put(f"/{dict_id}", json=dict_data)
|
||||
|
||||
async def delete_dictionary(self, dict_id: int) -> Response:
|
||||
"""删除字典"""
|
||||
return await self.delete(f"/{dict_id}")
|
||||
|
||||
async def check_type_and_code_exists(self, dict_type: str, code: str) -> Response:
|
||||
"""检查类型和编码是否存在"""
|
||||
return await self.get("/check/exists", params={"type": dict_type, "code": code})
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
角色管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class RoleAPI(BaseAPI):
|
||||
"""角色管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/roles")
|
||||
|
||||
async def create_role(self, role_data: Dict[str, Any]) -> Response:
|
||||
"""创建角色"""
|
||||
return await self.post("", json=role_data)
|
||||
|
||||
async def get_role_by_id(self, role_id: int) -> Response:
|
||||
"""根据ID获取角色"""
|
||||
return await self.get(f"/{role_id}")
|
||||
|
||||
async def get_role_by_name(self, role_name: str) -> Response:
|
||||
"""根据名称获取角色"""
|
||||
return await self.get(f"/name/{role_name}")
|
||||
|
||||
async def get_all_roles(self, include_deleted: bool = False) -> Response:
|
||||
"""获取所有角色"""
|
||||
return await self.get("", params={"includeDeleted": include_deleted})
|
||||
|
||||
async def update_role(self, role_id: int, role_data: Dict[str, Any]) -> Response:
|
||||
"""更新角色"""
|
||||
return await self.put(f"/{role_id}", json=role_data)
|
||||
|
||||
async def delete_role(self, role_id: int) -> Response:
|
||||
"""删除角色"""
|
||||
return await self.delete(f"/{role_id}")
|
||||
|
||||
async def logical_delete_role(self, role_id: int) -> Response:
|
||||
"""逻辑删除角色"""
|
||||
return await self.delete(f"/{role_id}/logical")
|
||||
|
||||
async def logical_delete_roles(self, role_ids: List[int]) -> Response:
|
||||
"""批量逻辑删除角色"""
|
||||
return await self.post("/logical-delete", json=role_ids)
|
||||
|
||||
async def restore_role(self, role_id: int) -> Response:
|
||||
"""恢复角色"""
|
||||
return await self.post(f"/{role_id}/restore")
|
||||
|
||||
async def restore_roles(self, role_ids: List[int]) -> Response:
|
||||
"""批量恢复角色"""
|
||||
return await self.post("/restore", json=role_ids)
|
||||
|
||||
async def check_name_exists(self, role_name: str) -> Response:
|
||||
"""检查角色名是否存在"""
|
||||
return await self.get("/check/name", params={"name": role_name})
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
用户管理API
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from httpx import AsyncClient, Response
|
||||
from .base_api import BaseAPI
|
||||
|
||||
|
||||
class UserAPI(BaseAPI):
|
||||
"""用户管理API"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
super().__init__(client, "/api/users")
|
||||
|
||||
async def create_user(self, user_data: Dict[str, Any]) -> Response:
|
||||
"""创建用户"""
|
||||
return await self.post("", json=user_data)
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Response:
|
||||
"""根据ID获取用户"""
|
||||
return await self.get(f"/{user_id}")
|
||||
|
||||
async def get_all_users(self, include_deleted: bool = False) -> Response:
|
||||
"""获取所有用户"""
|
||||
return await self.get("", params={"includeDeleted": include_deleted})
|
||||
|
||||
async def update_user(self, user_id: int, user_data: Dict[str, Any]) -> Response:
|
||||
"""更新用户"""
|
||||
return await self.put(f"/{user_id}", json=user_data)
|
||||
|
||||
async def delete_user(self, user_id: int) -> Response:
|
||||
"""删除用户"""
|
||||
return await self.delete(f"/{user_id}")
|
||||
|
||||
async def logical_delete_user(self, user_id: int) -> Response:
|
||||
"""逻辑删除用户"""
|
||||
return await self.delete(f"/{user_id}/logical")
|
||||
|
||||
async def logical_delete_users(self, user_ids: List[int]) -> Response:
|
||||
"""批量逻辑删除用户"""
|
||||
return await self.post("/logical-delete", json=user_ids)
|
||||
|
||||
async def restore_user(self, user_id: int) -> Response:
|
||||
"""恢复用户"""
|
||||
return await self.post(f"/{user_id}/restore")
|
||||
|
||||
async def restore_users(self, user_ids: List[int]) -> Response:
|
||||
"""批量恢复用户"""
|
||||
return await self.post("/restore", json=user_ids)
|
||||
|
||||
async def check_username_exists(self, username: str) -> Response:
|
||||
"""检查用户名是否存在"""
|
||||
return await self.get("/check/username", params={"username": username})
|
||||
|
||||
async def check_email_exists(self, email: str) -> Response:
|
||||
"""检查邮箱是否存在"""
|
||||
return await self.get("/check/email", params={"email": email})
|
||||
@@ -0,0 +1 @@
|
||||
"""配置模块"""
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
配置管理模块
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
|
||||
API_BASE_URL: str = Field(
|
||||
default="http://localhost:8080",
|
||||
description="API基础URL"
|
||||
)
|
||||
|
||||
DATABASE_HOST: str = Field(
|
||||
default="localhost",
|
||||
description="数据库主机"
|
||||
)
|
||||
|
||||
DATABASE_PORT: int = Field(
|
||||
default=55432,
|
||||
description="数据库端口"
|
||||
)
|
||||
|
||||
DATABASE_NAME: str = Field(
|
||||
default="manage_system",
|
||||
description="数据库名称"
|
||||
)
|
||||
|
||||
DATABASE_USERNAME: str = Field(
|
||||
default="postgres",
|
||||
description="数据库用户名"
|
||||
)
|
||||
|
||||
DATABASE_PASSWORD: str = Field(
|
||||
default="postgres",
|
||||
description="数据库密码"
|
||||
)
|
||||
|
||||
TEST_USERNAME: str = Field(
|
||||
default="admin",
|
||||
description="测试用户名"
|
||||
)
|
||||
|
||||
TEST_PASSWORD: str = Field(
|
||||
default="admin123",
|
||||
description="测试用户密码"
|
||||
)
|
||||
|
||||
REQUEST_TIMEOUT: int = Field(
|
||||
default=30000,
|
||||
description="请求超时时间(毫秒)"
|
||||
)
|
||||
|
||||
HEADLESS_BROWSER: bool = Field(
|
||||
default=True,
|
||||
description="无头浏览器模式"
|
||||
)
|
||||
|
||||
BROWSER_TYPE: str = Field(
|
||||
default="chromium",
|
||||
description="浏览器类型"
|
||||
)
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Pytest配置和fixtures
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from typing import AsyncGenerator, Generator
|
||||
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
|
||||
from httpx import AsyncClient
|
||||
|
||||
from config.settings import settings
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""创建事件循环"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def browser() -> AsyncGenerator[Browser, None]:
|
||||
"""浏览器fixture"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.launch(
|
||||
headless=settings.HEADLESS_BROWSER,
|
||||
browser_type=settings.BROWSER_TYPE
|
||||
)
|
||||
yield browser
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser: Browser) -> AsyncGenerator[BrowserContext, None]:
|
||||
"""浏览器上下文fixture"""
|
||||
context = await browser.new_context()
|
||||
yield context
|
||||
await context.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def page(context: BrowserContext) -> AsyncGenerator[Page, None]:
|
||||
"""页面fixture"""
|
||||
page = await context.new_page()
|
||||
page.set_default_timeout(settings.REQUEST_TIMEOUT)
|
||||
yield page
|
||||
await page.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def http_client() -> AsyncGenerator[AsyncClient, None]:
|
||||
"""HTTP客户端fixture"""
|
||||
async with AsyncClient(
|
||||
base_url=settings.API_BASE_URL,
|
||||
timeout=settings.REQUEST_TIMEOUT / 1000
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token(http_client: AsyncClient) -> str:
|
||||
"""获取认证token"""
|
||||
response = await http_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"username": settings.TEST_USERNAME,
|
||||
"password": settings.TEST_PASSWORD
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
return data.get("accessToken")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_client(http_client: AsyncClient, auth_token: str) -> AsyncClient:
|
||||
"""已认证的HTTP客户端"""
|
||||
http_client.headers.update({"Authorization": f"Bearer {auth_token}"})
|
||||
return http_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_data():
|
||||
"""测试用户数据"""
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
return {
|
||||
"username": f"testuser_{timestamp}",
|
||||
"password": "password123",
|
||||
"email": f"test_{timestamp}@example.com",
|
||||
"roleId": 2,
|
||||
"status": 1
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_role_data():
|
||||
"""测试角色数据"""
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
return {
|
||||
"name": f"TEST_ROLE_{timestamp}",
|
||||
"description": "测试角色",
|
||||
"permissions": "READ,WRITE"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_dictionary_data():
|
||||
"""测试字典数据"""
|
||||
return {
|
||||
"type": "USER_STATUS",
|
||||
"code": "ACTIVE",
|
||||
"name": "激活",
|
||||
"value": "1",
|
||||
"remark": "用户激活状态",
|
||||
"sort": 1
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_user(authenticated_client: AsyncClient):
|
||||
"""清理测试用户"""
|
||||
user_ids = []
|
||||
|
||||
yield user_ids
|
||||
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/users/{user_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_role(authenticated_client: AsyncClient):
|
||||
"""清理测试角色"""
|
||||
role_ids = []
|
||||
|
||||
yield role_ids
|
||||
|
||||
for role_id in role_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/roles/{role_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_dictionary(authenticated_client: AsyncClient):
|
||||
"""清理测试字典"""
|
||||
dict_ids = []
|
||||
|
||||
yield dict_ids
|
||||
|
||||
for dict_id in dict_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/dictionaries/{dict_id}")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,23 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--cov=.
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--alluredir=allure-results
|
||||
markers =
|
||||
auth: 认证相关测试
|
||||
user: 用户管理测试
|
||||
role: 角色管理测试
|
||||
dictionary: 字典管理测试
|
||||
oauth2: OAuth2相关测试
|
||||
smoke: 冒烟测试
|
||||
regression: 回归测试
|
||||
slow: 慢速测试
|
||||
asyncio_mode = auto
|
||||
@@ -0,0 +1,29 @@
|
||||
# Python依赖包
|
||||
|
||||
# 测试框架
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-cov==4.1.0
|
||||
pytest-xdist==3.5.0
|
||||
|
||||
# Playwright
|
||||
playwright==1.40.0
|
||||
|
||||
# HTTP客户端
|
||||
httpx==0.25.2
|
||||
requests==2.31.0
|
||||
|
||||
# 数据处理
|
||||
pydantic==2.5.2
|
||||
pydantic-settings==2.1.0
|
||||
faker==20.1.0
|
||||
|
||||
# 配置管理
|
||||
python-dotenv==1.0.0
|
||||
pyyaml==6.0.1
|
||||
|
||||
# 测试报告
|
||||
allure-pytest==2.13.2
|
||||
|
||||
# 工具库
|
||||
loguru==0.7.2
|
||||
@@ -0,0 +1 @@
|
||||
"""测试模块"""
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
认证测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from api.auth_api import AuthAPI
|
||||
from config.settings import settings
|
||||
|
||||
|
||||
@pytest.mark.auth
|
||||
@pytest.mark.smoke
|
||||
class TestAuth:
|
||||
"""认证测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(self, http_client):
|
||||
"""测试成功登录"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await auth_api.login(settings.TEST_USERNAME, settings.TEST_PASSWORD)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "accessToken" in data
|
||||
assert "refreshToken" in data
|
||||
assert isinstance(data["accessToken"], str)
|
||||
assert isinstance(data["refreshToken"], str)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_credentials(self, http_client):
|
||||
"""测试无效凭证登录"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await auth_api.login("invalid_user", "invalid_password")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_missing_fields(self, http_client):
|
||||
"""测试缺少必填字段"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await http_client.post("/api/auth/login", json={
|
||||
"username": "test"
|
||||
})
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_success(self, http_client, auth_token):
|
||||
"""测试刷新token成功"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
|
||||
login_response = await auth_api.login(settings.TEST_USERNAME, settings.TEST_PASSWORD)
|
||||
refresh_token = login_response.json().get("refreshToken")
|
||||
|
||||
response = await auth_api.refresh_token(refresh_token)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "accessToken" in data
|
||||
assert "refreshToken" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_invalid(self, http_client):
|
||||
"""测试无效刷新token"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await auth_api.refresh_token("invalid_refresh_token")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_success(self, http_client, auth_token):
|
||||
"""测试登出成功"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await auth_api.logout(auth_token)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_without_token(self, http_client):
|
||||
"""测试无token登出"""
|
||||
auth_api = AuthAPI(http_client)
|
||||
response = await http_client.post("/api/auth/logout")
|
||||
|
||||
assert response.status_code == 400
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
字典管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from api.dictionary_api import DictionaryAPI
|
||||
|
||||
|
||||
@pytest.mark.dictionary
|
||||
@pytest.mark.regression
|
||||
class TestDictionary:
|
||||
"""字典管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dictionary_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试创建字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["type"] == test_dictionary_data["type"]
|
||||
assert data["code"] == test_dictionary_data["code"]
|
||||
assert data["name"] == test_dictionary_data["name"]
|
||||
|
||||
cleanup_dictionary.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dictionary_duplicate_type_code(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试创建重复类型和编码"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
|
||||
assert response.status_code in [400, 409]
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dictionary_by_id_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试根据ID获取字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.get_dictionary_by_id(dict_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == dict_id
|
||||
assert data["type"] == test_dictionary_data["type"]
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dictionary_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的字典"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.get_dictionary_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dictionaries_by_type_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试根据类型获取字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.get_dictionaries_by_type(test_dictionary_data["type"])
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert any(d["id"] == dict_id for d in data)
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_dictionaries_success(self, authenticated_client):
|
||||
"""测试获取所有字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.get_all_dictionaries()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_dictionary_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试更新字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"name": "Updated name"}
|
||||
response = await dict_api.update_dictionary(dict_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Updated name"
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_dictionary_success(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试删除字典成功"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.delete_dictionary(dict_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_type_and_code_exists_true(self, authenticated_client, test_dictionary_data, cleanup_dictionary):
|
||||
"""测试检查类型和编码存在-返回true"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
|
||||
create_response = await dict_api.create_dictionary(test_dictionary_data)
|
||||
dict_id = create_response.json()["id"]
|
||||
|
||||
response = await dict_api.check_type_and_code_exists(
|
||||
test_dictionary_data["type"],
|
||||
test_dictionary_data["code"]
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_dictionary.append(dict_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_type_and_code_exists_false(self, authenticated_client):
|
||||
"""测试检查类型和编码存在-返回false"""
|
||||
dict_api = DictionaryAPI(authenticated_client)
|
||||
response = await dict_api.check_type_and_code_exists("NONEXISTENT_TYPE", "NONEXISTENT_CODE")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
OAuth2客户端管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
@pytest.mark.oauth2
|
||||
@pytest.mark.regression
|
||||
class TestOAuth2:
|
||||
"""OAuth2客户端管理测试类"""
|
||||
|
||||
@pytest.fixture
|
||||
def test_oauth2_client_data(self):
|
||||
"""测试OAuth2客户端数据"""
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
return {
|
||||
"clientId": f"test-client-{timestamp}",
|
||||
"clientSecret": "secret123",
|
||||
"clientName": "Test Client",
|
||||
"webServerRedirectUri": "http://localhost:8080/callback",
|
||||
"scope": "read,write",
|
||||
"authorizedGrantTypes": "authorization_code,refresh_token",
|
||||
"accessTokenValiditySeconds": 7200,
|
||||
"refreshTokenValiditySeconds": 2592000,
|
||||
"autoApprove": False,
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
async def cleanup_oauth2_client(self, authenticated_client: AsyncClient):
|
||||
"""清理测试OAuth2客户端"""
|
||||
client_ids = []
|
||||
|
||||
yield client_ids
|
||||
|
||||
for client_id in client_ids:
|
||||
try:
|
||||
await authenticated_client.delete(f"/api/oauth2/clients/{client_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_oauth2_client_success(self, authenticated_client, test_oauth2_client_data, cleanup_oauth2_client):
|
||||
"""测试创建OAuth2客户端成功"""
|
||||
response = await authenticated_client.post("/api/oauth2/clients", json=test_oauth2_client_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["clientId"] == test_oauth2_client_data["clientId"]
|
||||
assert data["clientName"] == test_oauth2_client_data["clientName"]
|
||||
assert "clientSecret" not in data or data["clientSecret"] != test_oauth2_client_data["clientSecret"]
|
||||
|
||||
cleanup_oauth2_client.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_oauth2_client_by_id_success(self, authenticated_client, test_oauth2_client_data, cleanup_oauth2_client):
|
||||
"""测试根据ID获取OAuth2客户端成功"""
|
||||
create_response = await authenticated_client.post("/api/oauth2/clients", json=test_oauth2_client_data)
|
||||
client_id = create_response.json()["id"]
|
||||
|
||||
response = await authenticated_client.get(f"/api/oauth2/clients/{client_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == client_id
|
||||
assert data["clientId"] == test_oauth2_client_data["clientId"]
|
||||
|
||||
cleanup_oauth2_client.append(client_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_oauth2_client_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的OAuth2客户端"""
|
||||
response = await authenticated_client.get("/api/oauth2/clients/999999")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_oauth2_client_by_client_id_success(self, authenticated_client, test_oauth2_client_data, cleanup_oauth2_client):
|
||||
"""测试根据clientId获取OAuth2客户端成功"""
|
||||
create_response = await authenticated_client.post("/api/oauth2/clients", json=test_oauth2_client_data)
|
||||
client_id = create_response.json()["id"]
|
||||
|
||||
response = await authenticated_client.get(f"/api/oauth2/clients/client-id/{test_oauth2_client_data['clientId']}")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["clientId"] == test_oauth2_client_data["clientId"]
|
||||
|
||||
cleanup_oauth2_client.append(client_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_oauth2_clients_success(self, authenticated_client):
|
||||
"""测试获取所有OAuth2客户端成功"""
|
||||
response = await authenticated_client.get("/api/oauth2/clients")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_oauth2_client_success(self, authenticated_client, test_oauth2_client_data, cleanup_oauth2_client):
|
||||
"""测试更新OAuth2客户端成功"""
|
||||
create_response = await authenticated_client.post("/api/oauth2/clients", json=test_oauth2_client_data)
|
||||
client_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"clientName": "Updated Client Name"}
|
||||
response = await authenticated_client.put(f"/api/oauth2/clients/{client_id}", json=update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["clientName"] == "Updated Client Name"
|
||||
|
||||
cleanup_oauth2_client.append(client_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_oauth2_client_success(self, authenticated_client, test_oauth2_client_data, cleanup_oauth2_client):
|
||||
"""测试删除OAuth2客户端成功"""
|
||||
create_response = await authenticated_client.post("/api/oauth2/clients", json=test_oauth2_client_data)
|
||||
client_id = create_response.json()["id"]
|
||||
|
||||
response = await authenticated_client.delete(f"/api/oauth2/clients/{client_id}")
|
||||
|
||||
assert response.status_code == 204
|
||||
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
角色管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from api.role_api import RoleAPI
|
||||
|
||||
|
||||
@pytest.mark.role
|
||||
@pytest.mark.regression
|
||||
class TestRole:
|
||||
"""角色管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试创建角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.create_role(test_role_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["name"] == test_role_data["name"]
|
||||
assert data["description"] == test_role_data["description"]
|
||||
assert data["permissions"] == test_role_data["permissions"]
|
||||
|
||||
cleanup_role.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_role_duplicate_name(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试创建重复角色名"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.create_role(test_role_data)
|
||||
|
||||
assert response.status_code in [400, 409]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_by_id_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试根据ID获取角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.get_role_by_id(role_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == role_id
|
||||
assert data["name"] == test_role_data["name"]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的角色"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.get_role_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_role_by_name_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试根据名称获取角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.get_role_by_name(test_role_data["name"])
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == test_role_data["name"]
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_roles_success(self, authenticated_client):
|
||||
"""测试获取所有角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.get_all_roles()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试更新角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"description": "Updated description"}
|
||||
response = await role_api.update_role(role_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["description"] == "Updated description"
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试删除角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.delete_role(role_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logical_delete_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试逻辑删除角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.logical_delete_role(role_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
get_response = await role_api.get_role_by_id(role_id)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
get_deleted_response = await role_api.get_all_roles(include_deleted=True)
|
||||
deleted_roles = get_deleted_response.json()
|
||||
assert any(r["id"] == role_id for r in deleted_roles)
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_role_success(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试恢复角色成功"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
await role_api.logical_delete_role(role_id)
|
||||
|
||||
response = await role_api.restore_role(role_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
get_response = await role_api.get_role_by_id(role_id)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_name_exists_true(self, authenticated_client, test_role_data, cleanup_role):
|
||||
"""测试检查角色名存在-返回true"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
|
||||
create_response = await role_api.create_role(test_role_data)
|
||||
role_id = create_response.json()["id"]
|
||||
|
||||
response = await role_api.check_name_exists(test_role_data["name"])
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_role.append(role_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_name_exists_false(self, authenticated_client):
|
||||
"""测试检查角色名存在-返回false"""
|
||||
role_api = RoleAPI(authenticated_client)
|
||||
response = await role_api.check_name_exists("NONEXISTENT_ROLE")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
用户管理测试用例
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from api.user_api import UserAPI
|
||||
from config.settings import settings
|
||||
|
||||
|
||||
@pytest.mark.user
|
||||
@pytest.mark.regression
|
||||
class TestUser:
|
||||
"""用户管理测试类"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试创建用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.create_user(test_user_data)
|
||||
|
||||
print(f"Response status: {response.status_code}")
|
||||
print(f"Response text: {response.text}")
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["username"] == test_user_data["username"]
|
||||
assert data["email"] == test_user_data["email"]
|
||||
assert "password" not in data or data["password"] != test_user_data["password"]
|
||||
|
||||
cleanup_user.append(data["id"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_duplicate_username(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试创建重复用户名"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
await user_api.create_user(test_user_data)
|
||||
response = await user_api.create_user(test_user_data)
|
||||
|
||||
assert response.status_code in [400, 409]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_id_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试根据ID获取用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.get_user_by_id(user_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == user_id
|
||||
assert data["username"] == test_user_data["username"]
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_id_not_found(self, authenticated_client):
|
||||
"""测试获取不存在的用户"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.get_user_by_id(999999)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_users_success(self, authenticated_client):
|
||||
"""测试获取所有用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.get_all_users()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试更新用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"email": "updated@example.com"}
|
||||
response = await user_api.update_user(user_id, update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["email"] == "updated@example.com"
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试删除用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.delete_user(user_id)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logical_delete_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试逻辑删除用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.logical_delete_user(user_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
get_response = await user_api.get_user_by_id(user_id)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
get_deleted_response = await user_api.get_all_users(include_deleted=True)
|
||||
deleted_users = get_deleted_response.json()
|
||||
assert any(u["id"] == user_id for u in deleted_users)
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_user_success(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试恢复用户成功"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
await user_api.logical_delete_user(user_id)
|
||||
|
||||
response = await user_api.restore_user(user_id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
get_response = await user_api.get_user_by_id(user_id)
|
||||
assert get_response.status_code == 200
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_username_exists_true(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试检查用户名存在-返回true"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.check_username_exists(test_user_data["username"])
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_username_exists_false(self, authenticated_client):
|
||||
"""测试检查用户名存在-返回false"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.check_username_exists("nonexistent_user")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_email_exists_true(self, authenticated_client, test_user_data, cleanup_user):
|
||||
"""测试检查邮箱存在-返回true"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
|
||||
create_response = await user_api.create_user(test_user_data)
|
||||
user_id = create_response.json()["id"]
|
||||
|
||||
response = await user_api.check_email_exists(test_user_data["email"])
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is True
|
||||
|
||||
cleanup_user.append(user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_email_exists_false(self, authenticated_client):
|
||||
"""测试检查邮箱存在-返回false"""
|
||||
user_api = UserAPI(authenticated_client)
|
||||
response = await user_api.check_email_exists("nonexistent@example.com")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() is False
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
断言工具
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from httpx import Response
|
||||
|
||||
|
||||
class Assertions:
|
||||
"""断言工具类"""
|
||||
|
||||
@staticmethod
|
||||
def assert_status_code(response: Response, expected_status: int):
|
||||
"""断言状态码"""
|
||||
assert response.status_code == expected_status, \
|
||||
f"Expected status code {expected_status}, got {response.status_code}. Response: {response.text}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_contains(response: Response, key: str, value: Any = None):
|
||||
"""断言响应包含指定字段"""
|
||||
data = response.json()
|
||||
assert key in data, f"Response does not contain key '{key}'. Response: {data}"
|
||||
if value is not None:
|
||||
assert data[key] == value, \
|
||||
f"Expected {value} for key '{key}', got {data[key]}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_is_list(response: Response):
|
||||
"""断言响应是列表"""
|
||||
data = response.json()
|
||||
assert isinstance(data, list), f"Expected list, got {type(data)}. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_not_empty(response: Response):
|
||||
"""断言响应不为空"""
|
||||
data = response.json()
|
||||
assert data, f"Response is empty. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_field_type(response: Response, field: str, expected_type: type):
|
||||
"""断言响应字段类型"""
|
||||
data = response.json()
|
||||
assert field in data, f"Response does not contain field '{field}'"
|
||||
assert isinstance(data[field], expected_type), \
|
||||
f"Expected field '{field}' to be {expected_type}, got {type(data[field])}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_fields_present(response: Response, fields: List[str]):
|
||||
"""断言响应包含所有指定字段"""
|
||||
data = response.json()
|
||||
missing_fields = [field for field in fields if field not in data]
|
||||
assert not missing_fields, \
|
||||
f"Response is missing fields: {missing_fields}. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_field_length(response: Response, field: str, min_length: int = None, max_length: int = None):
|
||||
"""断言响应字段长度"""
|
||||
data = response.json()
|
||||
assert field in data, f"Response does not contain field '{field}'"
|
||||
field_value = data[field]
|
||||
|
||||
if isinstance(field_value, (str, list, dict)):
|
||||
length = len(field_value)
|
||||
if min_length is not None:
|
||||
assert length >= min_length, \
|
||||
f"Field '{field}' length {length} is less than minimum {min_length}"
|
||||
if max_length is not None:
|
||||
assert length <= max_length, \
|
||||
f"Field '{field}' length {length} is greater than maximum {max_length}"
|
||||
else:
|
||||
raise AssertionError(f"Field '{field}' is not a string, list, or dict")
|
||||
|
||||
@staticmethod
|
||||
def assert_error_response(response: Response, expected_message: str = None):
|
||||
"""断言错误响应"""
|
||||
Assertions.assert_status_code(response, 400)
|
||||
if expected_message:
|
||||
data = response.json()
|
||||
assert expected_message in str(data), \
|
||||
f"Expected error message '{expected_message}' not found in response: {data}"
|
||||
|
||||
|
||||
assertions = Assertions()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
测试数据生成器
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
from faker import Faker
|
||||
|
||||
|
||||
class DataGenerator:
|
||||
"""测试数据生成器"""
|
||||
|
||||
def __init__(self, locale: str = "zh_CN"):
|
||||
self.faker = Faker(locale)
|
||||
|
||||
def generate_username(self) -> str:
|
||||
"""生成用户名"""
|
||||
return f"testuser_{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}"
|
||||
|
||||
def generate_password(self, length: int = 12) -> str:
|
||||
"""生成密码"""
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return ''.join(random.choices(chars, k=length))
|
||||
|
||||
def generate_email(self) -> str:
|
||||
"""生成邮箱"""
|
||||
return self.faker.email()
|
||||
|
||||
def generate_phone(self) -> str:
|
||||
"""生成手机号"""
|
||||
return self.faker.phone_number()
|
||||
|
||||
def generate_name(self) -> str:
|
||||
"""生成姓名"""
|
||||
return self.faker.name()
|
||||
|
||||
def generate_role_name(self) -> str:
|
||||
"""生成角色名"""
|
||||
return f"ROLE_{''.join(random.choices(string.ascii_uppercase, k=6))}"
|
||||
|
||||
def generate_dict_type(self) -> str:
|
||||
"""生成字典类型"""
|
||||
return f"DICT_TYPE_{''.join(random.choices(string.ascii_uppercase, k=4))}"
|
||||
|
||||
def generate_dict_code(self) -> str:
|
||||
"""生成字典编码"""
|
||||
return f"CODE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=6))}"
|
||||
|
||||
def generate_url(self) -> str:
|
||||
"""生成URL"""
|
||||
return self.faker.url()
|
||||
|
||||
def generate_company_name(self) -> str:
|
||||
"""生成公司名"""
|
||||
return self.faker.company()
|
||||
|
||||
def generate_address(self) -> str:
|
||||
"""生成地址"""
|
||||
return self.faker.address()
|
||||
|
||||
def generate_description(self) -> str:
|
||||
"""生成描述"""
|
||||
return self.faker.text(max_nb_chars=200)
|
||||
|
||||
def generate_permissions(self) -> str:
|
||||
"""生成权限字符串"""
|
||||
permissions = ["READ", "WRITE", "DELETE", "ADMIN", "MANAGE"]
|
||||
selected = random.sample(permissions, random.randint(1, len(permissions)))
|
||||
return ",".join(selected)
|
||||
|
||||
|
||||
data_generator = DataGenerator()
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
日志工具
|
||||
"""
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_logger(log_file: str = "e2e_tests.log", log_level: str = "INFO"):
|
||||
"""配置日志"""
|
||||
logger.remove()
|
||||
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
level=log_level,
|
||||
colorize=True
|
||||
)
|
||||
|
||||
logger.add(
|
||||
log_file,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
level=log_level,
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
setup_logger()
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cn.novalon.manage</groupId>
|
||||
<artifactId>novalon-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>manage-sys</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Manage Sys</name>
|
||||
<description>System Management Module</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>r2dbc-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package cn.novalon.manage.sys;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ManageSysApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ManageSysApplication.class, args);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package cn.novalon.manage.sys.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
return http
|
||||
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
||||
.authorizeExchange(exchanges -> exchanges
|
||||
.pathMatchers("/api/auth/**").permitAll()
|
||||
.pathMatchers("/api/public/**").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package cn.novalon.manage.sys.config;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class SystemWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
sessions.put(session.getId(), session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
sessions.remove(session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
// Handle incoming messages if needed
|
||||
}
|
||||
|
||||
public void sendMessageToUser(String userId, String message) {
|
||||
sessions.values().forEach(session -> {
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(new TextMessage(message));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void broadcast(String message) {
|
||||
sessions.values().forEach(session -> {
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(new TextMessage(message));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package cn.novalon.manage.sys.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final SystemWebSocketHandler webSocketHandler;
|
||||
|
||||
public WebSocketConfig(SystemWebSocketHandler webSocketHandler) {
|
||||
this.webSocketHandler = webSocketHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(webSocketHandler, "/ws")
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public abstract class BaseDomain {
|
||||
|
||||
private Long id;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
public class OperationLog extends BaseDomain {
|
||||
|
||||
private String username;
|
||||
private String operation;
|
||||
private String method;
|
||||
private String params;
|
||||
private String result;
|
||||
private String ip;
|
||||
private Long duration;
|
||||
private String status;
|
||||
private String errorMsg;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getOperation() {
|
||||
return operation;
|
||||
}
|
||||
|
||||
public void setOperation(String operation) {
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
public void setParams(String params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public void setIp(String ip) {
|
||||
this.ip = ip;
|
||||
}
|
||||
|
||||
public Long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(Long duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getErrorMsg() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
public void setErrorMsg(String errorMsg) {
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysConfig {
|
||||
|
||||
private Long id;
|
||||
private String configName;
|
||||
private String configKey;
|
||||
private String configValue;
|
||||
private String configType;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getConfigName() { return configName; }
|
||||
public void setConfigName(String configName) { this.configName = configName; }
|
||||
public String getConfigKey() { return configKey; }
|
||||
public void setConfigKey(String configKey) { this.configKey = configKey; }
|
||||
public String getConfigValue() { return configValue; }
|
||||
public void setConfigValue(String configValue) { this.configValue = configValue; }
|
||||
public String getConfigType() { return configType; }
|
||||
public void setConfigType(String configType) { this.configType = configType; }
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysDictData {
|
||||
|
||||
private Long id;
|
||||
private Long dictTypeId;
|
||||
private String dictLabel;
|
||||
private String dictValue;
|
||||
private Integer dictSort;
|
||||
private String dictType;
|
||||
private String cssClass;
|
||||
private String listClass;
|
||||
private String isDefault;
|
||||
private String status;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public Long getDictTypeId() { return dictTypeId; }
|
||||
public void setDictTypeId(Long dictTypeId) { this.dictTypeId = dictTypeId; }
|
||||
public String getDictLabel() { return dictLabel; }
|
||||
public void setDictLabel(String dictLabel) { this.dictLabel = dictLabel; }
|
||||
public String getDictValue() { return dictValue; }
|
||||
public void setDictValue(String dictValue) { this.dictValue = dictValue; }
|
||||
public Integer getDictSort() { return dictSort; }
|
||||
public void setDictSort(Integer dictSort) { this.dictSort = dictSort; }
|
||||
public String getDictType() { return dictType; }
|
||||
public void setDictType(String dictType) { this.dictType = dictType; }
|
||||
public String getCssClass() { return cssClass; }
|
||||
public void setCssClass(String cssClass) { this.cssClass = cssClass; }
|
||||
public String getListClass() { return listClass; }
|
||||
public void setListClass(String listClass) { this.listClass = listClass; }
|
||||
public String getIsDefault() { return isDefault; }
|
||||
public void setIsDefault(String isDefault) { this.isDefault = isDefault; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysDictType {
|
||||
|
||||
private Long id;
|
||||
private String dictName;
|
||||
private String dictType;
|
||||
private String status;
|
||||
private String remark;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getDictName() { return dictName; }
|
||||
public void setDictName(String dictName) { this.dictName = dictName; }
|
||||
public String getDictType() { return dictType; }
|
||||
public void setDictType(String dictType) { this.dictType = dictType; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getRemark() { return remark; }
|
||||
public void setRemark(String remark) { this.remark = remark; }
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysExceptionLog {
|
||||
|
||||
private Long id;
|
||||
private String username;
|
||||
private String title;
|
||||
private String exceptionName;
|
||||
private String methodName;
|
||||
private String methodParams;
|
||||
private String exceptionMsg;
|
||||
private String exceptionStack;
|
||||
private String ip;
|
||||
private LocalDateTime createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getExceptionName() { return exceptionName; }
|
||||
public void setExceptionName(String exceptionName) { this.exceptionName = exceptionName; }
|
||||
public String getMethodName() { return methodName; }
|
||||
public void setMethodName(String methodName) { this.methodName = methodName; }
|
||||
public String getMethodParams() { return methodParams; }
|
||||
public void setMethodParams(String methodParams) { this.methodParams = methodParams; }
|
||||
public String getExceptionMsg() { return exceptionMsg; }
|
||||
public void setExceptionMsg(String exceptionMsg) { this.exceptionMsg = exceptionMsg; }
|
||||
public String getExceptionStack() { return exceptionStack; }
|
||||
public void setExceptionStack(String exceptionStack) { this.exceptionStack = exceptionStack; }
|
||||
public String getIp() { return ip; }
|
||||
public void setIp(String ip) { this.ip = ip; }
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysFile {
|
||||
|
||||
private Long id;
|
||||
private String fileName;
|
||||
private String filePath;
|
||||
private String fileSize;
|
||||
private String fileType;
|
||||
private String storageType;
|
||||
private String createBy;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getFileName() { return fileName; }
|
||||
public void setFileName(String fileName) { this.fileName = fileName; }
|
||||
public String getFilePath() { return filePath; }
|
||||
public void setFilePath(String filePath) { this.filePath = filePath; }
|
||||
public String getFileSize() { return fileSize; }
|
||||
public void setFileSize(String fileSize) { this.fileSize = fileSize; }
|
||||
public String getFileType() { return fileType; }
|
||||
public void setFileType(String fileType) { this.fileType = fileType; }
|
||||
public String getStorageType() { return storageType; }
|
||||
public void setStorageType(String storageType) { this.storageType = storageType; }
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysLoginLog {
|
||||
|
||||
private Long id;
|
||||
private String username;
|
||||
private String ip;
|
||||
private String location;
|
||||
private String browser;
|
||||
private String os;
|
||||
private String status;
|
||||
private String message;
|
||||
private LocalDateTime loginTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getIp() { return ip; }
|
||||
public void setIp(String ip) { this.ip = ip; }
|
||||
public String getLocation() { return location; }
|
||||
public void setLocation(String location) { this.location = location; }
|
||||
public String getBrowser() { return browser; }
|
||||
public void setBrowser(String browser) { this.browser = browser; }
|
||||
public String getOs() { return os; }
|
||||
public void setOs(String os) { this.os = os; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
public LocalDateTime getLoginTime() { return loginTime; }
|
||||
public void setLoginTime(LocalDateTime loginTime) { this.loginTime = loginTime; }
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SysMenu extends BaseDomain {
|
||||
|
||||
private String menuName;
|
||||
private Long parentId;
|
||||
private Integer orderNum;
|
||||
private String menuType;
|
||||
private String perms;
|
||||
private String component;
|
||||
private String status;
|
||||
private List<SysMenu> children;
|
||||
|
||||
public String getMenuName() {
|
||||
return menuName;
|
||||
}
|
||||
|
||||
public void setMenuName(String menuName) {
|
||||
this.menuName = menuName;
|
||||
}
|
||||
|
||||
public Long getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(Long parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public Integer getOrderNum() {
|
||||
return orderNum;
|
||||
}
|
||||
|
||||
public void setOrderNum(Integer orderNum) {
|
||||
this.orderNum = orderNum;
|
||||
}
|
||||
|
||||
public String getMenuType() {
|
||||
return menuType;
|
||||
}
|
||||
|
||||
public void setMenuType(String menuType) {
|
||||
this.menuType = menuType;
|
||||
}
|
||||
|
||||
public String getPerms() {
|
||||
return perms;
|
||||
}
|
||||
|
||||
public void setPerms(String perms) {
|
||||
this.perms = perms;
|
||||
}
|
||||
|
||||
public String getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
public void setComponent(String component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public List<SysMenu> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<SysMenu> children) {
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysNotice {
|
||||
|
||||
private Long id;
|
||||
private String noticeTitle;
|
||||
private String noticeType;
|
||||
private String noticeContent;
|
||||
private String status;
|
||||
private String createBy;
|
||||
private String updateBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getNoticeTitle() { return noticeTitle; }
|
||||
public void setNoticeTitle(String noticeTitle) { this.noticeTitle = noticeTitle; }
|
||||
public String getNoticeType() { return noticeType; }
|
||||
public void setNoticeType(String noticeType) { this.noticeType = noticeType; }
|
||||
public String getNoticeContent() { return noticeContent; }
|
||||
public void setNoticeContent(String noticeContent) { this.noticeContent = noticeContent; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getCreateBy() { return createBy; }
|
||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||
public String getUpdateBy() { return updateBy; }
|
||||
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
public class SysRole extends BaseDomain {
|
||||
|
||||
private String roleName;
|
||||
private String roleKey;
|
||||
private Integer roleSort;
|
||||
private Integer status;
|
||||
|
||||
public String getRoleName() {
|
||||
return roleName;
|
||||
}
|
||||
|
||||
public void setRoleName(String roleName) {
|
||||
this.roleName = roleName;
|
||||
}
|
||||
|
||||
public String getRoleKey() {
|
||||
return roleKey;
|
||||
}
|
||||
|
||||
public void setRoleKey(String roleKey) {
|
||||
this.roleKey = roleKey;
|
||||
}
|
||||
|
||||
public Integer getRoleSort() {
|
||||
return roleSort;
|
||||
}
|
||||
|
||||
public void setRoleSort(Integer roleSort) {
|
||||
this.roleSort = roleSort;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
public class SysUser extends BaseDomain {
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
private String email;
|
||||
private Long roleId;
|
||||
private Integer status;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public Long getRoleId() {
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public void setRoleId(Long roleId) {
|
||||
this.roleId = roleId;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SysUserMessage {
|
||||
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String title;
|
||||
private String content;
|
||||
private String messageType;
|
||||
private String isRead;
|
||||
private LocalDateTime createTime;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getMessageType() { return messageType; }
|
||||
public void setMessageType(String messageType) { this.messageType = messageType; }
|
||||
public String getIsRead() { return isRead; }
|
||||
public void setIsRead(String isRead) { this.isRead = isRead; }
|
||||
public LocalDateTime getCreateTime() { return createTime; }
|
||||
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package cn.novalon.manage.sys.core.repository;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.OperationLog;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IOperationLogRepository {
|
||||
|
||||
Mono<OperationLog> findById(Long id);
|
||||
|
||||
Mono<OperationLog> save(OperationLog operationLog);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
|
||||
Flux<OperationLog> findAll();
|
||||
|
||||
Flux<OperationLog> findByUsername(String username);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package cn.novalon.manage.sys.core.repository;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysMenu;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysMenuRepository {
|
||||
|
||||
Mono<SysMenu> findById(Long id);
|
||||
|
||||
Flux<SysMenu> findAll();
|
||||
|
||||
Flux<SysMenu> findByParentId(Long parentId);
|
||||
|
||||
Mono<SysMenu> save(SysMenu sysMenu);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package cn.novalon.manage.sys.core.repository;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysRoleRepository {
|
||||
|
||||
Mono<SysRole> findById(Long id);
|
||||
|
||||
Mono<SysRole> save(SysRole sysRole);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
|
||||
Flux<SysRole> findAll();
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package cn.novalon.manage.sys.core.repository;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysUserRepository {
|
||||
|
||||
Mono<SysUser> findByUsername(String username);
|
||||
|
||||
Mono<SysUser> findById(Long id);
|
||||
|
||||
Mono<SysUser> save(SysUser sysUser);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
|
||||
Flux<SysUser> findAll();
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.OperationLog;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IOperationLogService {
|
||||
Mono<OperationLog> save(OperationLog log);
|
||||
Flux<OperationLog> findAll();
|
||||
Flux<OperationLog> findByUsername(String username);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysConfig;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysConfigService {
|
||||
Flux<SysConfig> findAll();
|
||||
Mono<SysConfig> findById(Long id);
|
||||
Mono<SysConfig> findByConfigKey(String configKey);
|
||||
Mono<SysConfig> save(SysConfig config);
|
||||
Mono<Void> deleteById(Long id);
|
||||
Mono<String> getConfigValue(String configKey);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictData;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysDictDataService {
|
||||
Flux<SysDictData> findAll();
|
||||
Flux<SysDictData> findByDictType(String dictType);
|
||||
Flux<SysDictData> findByDictTypeAndStatus(String dictType, String status);
|
||||
Mono<SysDictData> findById(Long id);
|
||||
Mono<SysDictData> save(SysDictData dictData);
|
||||
Mono<Void> deleteById(Long id);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictType;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysDictTypeService {
|
||||
Flux<SysDictType> findAll();
|
||||
Mono<SysDictType> findById(Long id);
|
||||
Mono<SysDictType> findByDictType(String dictType);
|
||||
Mono<SysDictType> save(SysDictType dictType);
|
||||
Mono<Void> deleteById(Long id);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface ISysExceptionLogService {
|
||||
Flux<SysExceptionLog> findAll();
|
||||
Flux<SysExceptionLog> findByUsername(String username);
|
||||
Flux<SysExceptionLog> findByCreateTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
Mono<SysExceptionLog> save(SysExceptionLog exceptionLog);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysFile;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface ISysFileService {
|
||||
Flux<SysFile> findAll();
|
||||
Flux<SysFile> findByCreateBy(String createBy);
|
||||
Mono<SysFile> findById(Long id);
|
||||
Mono<SysFile> upload(MultipartFile file, String createBy);
|
||||
Mono<Void> deleteById(Long id);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysLoginLog;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public interface ISysLoginLogService {
|
||||
Flux<SysLoginLog> findAll();
|
||||
Flux<SysLoginLog> findByUsername(String username);
|
||||
Flux<SysLoginLog> findByLoginTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
Mono<SysLoginLog> save(SysLoginLog loginLog);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysMenu;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysMenuService {
|
||||
Mono<SysMenu> findById(Long id);
|
||||
Flux<SysMenu> findAll();
|
||||
Flux<SysMenu> findByParentId(Long parentId);
|
||||
Mono<SysMenu> createMenu(SysMenu menu);
|
||||
Mono<SysMenu> updateMenu(SysMenu menu);
|
||||
Mono<Void> deleteMenu(Long id);
|
||||
Flux<SysMenu> buildMenuTree(Flux<SysMenu> menus);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysNotice;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysNoticeService {
|
||||
Flux<SysNotice> findAll();
|
||||
Flux<SysNotice> findByStatus(String status);
|
||||
Mono<SysNotice> findById(Long id);
|
||||
Mono<SysNotice> save(SysNotice notice);
|
||||
Mono<Void> deleteById(Long id);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysRoleService {
|
||||
Mono<SysRole> findById(Long id);
|
||||
Flux<SysRole> findAll();
|
||||
Mono<SysRole> createRole(SysRole role);
|
||||
Mono<SysRole> updateRole(SysRole role);
|
||||
Mono<Void> deleteRole(Long id);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUserMessage;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysUserMessageService {
|
||||
Flux<SysUserMessage> findByUserId(Long userId);
|
||||
Flux<SysUserMessage> findByUserIdAndIsRead(Long userId, String isRead);
|
||||
Mono<Long> countUnread(Long userId);
|
||||
Mono<SysUserMessage> save(SysUserMessage message);
|
||||
Mono<Void> markAsRead(Long id);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package cn.novalon.manage.sys.core.service;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ISysUserService {
|
||||
Mono<SysUser> findById(Long id);
|
||||
Mono<SysUser> findByUsername(String username);
|
||||
Mono<SysUser> createUser(SysUser user);
|
||||
Mono<SysUser> updateUser(SysUser user);
|
||||
Mono<Void> deleteUser(Long id);
|
||||
Mono<SysUser> changePassword(Long userId, String oldPassword, String newPassword);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.OperationLog;
|
||||
import cn.novalon.manage.sys.core.repository.IOperationLogRepository;
|
||||
import cn.novalon.manage.sys.core.service.IOperationLogService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class OperationLogService implements IOperationLogService {
|
||||
|
||||
private final IOperationLogRepository logRepository;
|
||||
|
||||
public OperationLogService(IOperationLogRepository logRepository) {
|
||||
this.logRepository = logRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OperationLog> save(OperationLog log) {
|
||||
log.setCreatedAt(LocalDateTime.now());
|
||||
return logRepository.save(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<OperationLog> findAll() {
|
||||
return logRepository.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<OperationLog> findByUsername(String username) {
|
||||
return logRepository.findByUsername(username);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysConfig;
|
||||
import cn.novalon.manage.sys.core.service.ISysConfigService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysConfigConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysConfigDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class SysConfigServiceImpl implements ISysConfigService {
|
||||
|
||||
private final SysConfigDao dao;
|
||||
private final SysConfigConverter converter;
|
||||
|
||||
public SysConfigServiceImpl(SysConfigDao dao, SysConfigConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysConfig> findAll() {
|
||||
return dao.findByDeletedAtIsNull()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysConfig> findById(Long id) {
|
||||
return dao.findById(id)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysConfig> findByConfigKey(String configKey) {
|
||||
return dao.findByConfigKeyAndDeletedAtIsNull(configKey)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysConfig> save(SysConfig config) {
|
||||
return dao.save(converter.toEntity(config))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return dao.deleteByIdAndDeletedAtIsNull(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> getConfigValue(String configKey) {
|
||||
return findByConfigKey(configKey)
|
||||
.map(SysConfig::getConfigValue);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictData;
|
||||
import cn.novalon.manage.sys.core.service.ISysDictDataService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysDictDataConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysDictDataDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class SysDictDataServiceImpl implements ISysDictDataService {
|
||||
|
||||
private final SysDictDataDao dao;
|
||||
private final SysDictDataConverter converter;
|
||||
|
||||
public SysDictDataServiceImpl(SysDictDataDao dao, SysDictDataConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysDictData> findAll() {
|
||||
return dao.findByDeletedAtIsNull()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysDictData> findByDictType(String dictType) {
|
||||
return dao.findByDictTypeAndDeletedAtIsNull(dictType)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysDictData> findByDictTypeAndStatus(String dictType, String status) {
|
||||
return dao.findByDictTypeAndStatusAndDeletedAtIsNull(dictType, status)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysDictData> findById(Long id) {
|
||||
return dao.findById(id)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysDictData> save(SysDictData dictData) {
|
||||
return dao.save(converter.toEntity(dictData))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return dao.deleteByIdAndDeletedAtIsNull(id);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictType;
|
||||
import cn.novalon.manage.sys.core.service.ISysDictTypeService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysDictTypeConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysDictTypeDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class SysDictTypeServiceImpl implements ISysDictTypeService {
|
||||
|
||||
private final SysDictTypeDao dao;
|
||||
private final SysDictTypeConverter converter;
|
||||
|
||||
public SysDictTypeServiceImpl(SysDictTypeDao dao, SysDictTypeConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysDictType> findAll() {
|
||||
return dao.findByDeletedAtIsNull()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysDictType> findById(Long id) {
|
||||
return dao.findById(id)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysDictType> findByDictType(String dictType) {
|
||||
return dao.findByDictTypeAndDeletedAtIsNull(dictType)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysDictType> save(SysDictType dictType) {
|
||||
return dao.save(converter.toEntity(dictType))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return dao.deleteByIdAndDeletedAtIsNull(id);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
|
||||
import cn.novalon.manage.sys.core.service.ISysExceptionLogService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysExceptionLogConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysExceptionLogDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class SysExceptionLogServiceImpl implements ISysExceptionLogService {
|
||||
|
||||
private final SysExceptionLogDao dao;
|
||||
private final SysExceptionLogConverter converter;
|
||||
|
||||
public SysExceptionLogServiceImpl(SysExceptionLogDao dao, SysExceptionLogConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysExceptionLog> findAll() {
|
||||
return dao.findAllByOrderByCreateTimeDesc()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysExceptionLog> findByUsername(String username) {
|
||||
return dao.findByUsernameOrderByCreateTimeDesc(username)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysExceptionLog> findByCreateTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return dao.findByCreateTimeBetweenOrderByCreateTimeDesc(startTime, endTime)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysExceptionLog> save(SysExceptionLog exceptionLog) {
|
||||
return dao.save(converter.toEntity(exceptionLog))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysFile;
|
||||
import cn.novalon.manage.sys.core.service.ISysFileService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysFileConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysFileDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class SysFileServiceImpl implements ISysFileService {
|
||||
|
||||
private final SysFileDao dao;
|
||||
private final SysFileConverter converter;
|
||||
private final Path uploadPath = Paths.get("./uploads");
|
||||
|
||||
public SysFileServiceImpl(SysFileDao dao, SysFileConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysFile> findAll() {
|
||||
return dao.findByDeletedAtIsNullOrderByCreatedAtDesc()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysFile> findByCreateBy(String createBy) {
|
||||
return dao.findByCreateByOrderByCreatedAtDesc(createBy)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysFile> findById(Long id) {
|
||||
return dao.findById(id)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysFile> upload(MultipartFile file, String createBy) {
|
||||
try {
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||
Path filePath = uploadPath.resolve(fileName);
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
|
||||
SysFile sysFile = new SysFile();
|
||||
sysFile.setFileName(file.getOriginalFilename());
|
||||
sysFile.setFilePath("/api/files/download/" + fileName);
|
||||
sysFile.setFileSize(String.valueOf(file.getSize()));
|
||||
sysFile.setFileType(file.getContentType());
|
||||
sysFile.setStorageType("local");
|
||||
sysFile.setCreateBy(createBy);
|
||||
sysFile.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
return dao.save(converter.toEntity(sysFile))
|
||||
.map(converter::toDomain);
|
||||
} catch (Exception e) {
|
||||
return Mono.error(new RuntimeException("文件上传失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return dao.deleteByIdAndDeletedAtIsNull(id);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysLoginLog;
|
||||
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysLoginLogConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysLoginLogDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class SysLoginLogServiceImpl implements ISysLoginLogService {
|
||||
|
||||
private final SysLoginLogDao dao;
|
||||
private final SysLoginLogConverter converter;
|
||||
|
||||
public SysLoginLogServiceImpl(SysLoginLogDao dao, SysLoginLogConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysLoginLog> findAll() {
|
||||
return dao.findAllByOrderByLoginTimeDesc()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysLoginLog> findByUsername(String username) {
|
||||
return dao.findByUsernameOrderByLoginTimeDesc(username)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysLoginLog> findByLoginTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return dao.findByLoginTimeBetweenOrderByLoginTimeDesc(startTime, endTime)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysLoginLog> save(SysLoginLog loginLog) {
|
||||
return dao.save(converter.toEntity(loginLog))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysMenu;
|
||||
import cn.novalon.manage.sys.core.repository.ISysMenuRepository;
|
||||
import cn.novalon.manage.sys.core.service.ISysMenuService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class SysMenuService implements ISysMenuService {
|
||||
|
||||
private final ISysMenuRepository menuRepository;
|
||||
|
||||
public SysMenuService(ISysMenuRepository menuRepository) {
|
||||
this.menuRepository = menuRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysMenu> findById(Long id) {
|
||||
return menuRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysMenu> findAll() {
|
||||
return menuRepository.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysMenu> findByParentId(Long parentId) {
|
||||
return menuRepository.findByParentId(parentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysMenu> createMenu(SysMenu menu) {
|
||||
menu.setCreatedAt(LocalDateTime.now());
|
||||
menu.setStatus("0");
|
||||
return menuRepository.save(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysMenu> updateMenu(SysMenu menu) {
|
||||
menu.setUpdatedAt(LocalDateTime.now());
|
||||
return menuRepository.save(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteMenu(Long id) {
|
||||
return menuRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysMenu> buildMenuTree(Flux<SysMenu> menus) {
|
||||
return menus.collectList()
|
||||
.map(list -> buildTree(list, 0L))
|
||||
.flatMapMany(Flux::fromIterable);
|
||||
}
|
||||
|
||||
private List<SysMenu> buildTree(List<SysMenu> menus, Long parentId) {
|
||||
return menus.stream()
|
||||
.filter(m -> m.getParentId().equals(parentId))
|
||||
.peek(m -> m.setChildren(buildTree(menus, m.getId())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysNotice;
|
||||
import cn.novalon.manage.sys.core.service.ISysNoticeService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysNoticeConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysNoticeDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class SysNoticeServiceImpl implements ISysNoticeService {
|
||||
|
||||
private final SysNoticeDao dao;
|
||||
private final SysNoticeConverter converter;
|
||||
|
||||
public SysNoticeServiceImpl(SysNoticeDao dao, SysNoticeConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysNotice> findAll() {
|
||||
return dao.findByDeletedAtIsNull()
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysNotice> findByStatus(String status) {
|
||||
return dao.findByStatusAndDeletedAtIsNull(status)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysNotice> findById(Long id) {
|
||||
return dao.findById(id)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysNotice> save(SysNotice notice) {
|
||||
return dao.save(converter.toEntity(notice))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return dao.deleteByIdAndDeletedAtIsNull(id);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
import cn.novalon.manage.sys.core.repository.ISysRoleRepository;
|
||||
import cn.novalon.manage.sys.core.service.ISysRoleService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class SysRoleService implements ISysRoleService {
|
||||
|
||||
private final ISysRoleRepository roleRepository;
|
||||
|
||||
public SysRoleService(ISysRoleRepository roleRepository) {
|
||||
this.roleRepository = roleRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysRole> findById(Long id) {
|
||||
return roleRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysRole> findAll() {
|
||||
return roleRepository.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysRole> createRole(SysRole role) {
|
||||
role.setCreatedAt(LocalDateTime.now());
|
||||
role.setStatus(1);
|
||||
return roleRepository.save(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysRole> updateRole(SysRole role) {
|
||||
role.setUpdatedAt(LocalDateTime.now());
|
||||
return roleRepository.save(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteRole(Long id) {
|
||||
return roleRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUserMessage;
|
||||
import cn.novalon.manage.sys.core.service.ISysUserMessageService;
|
||||
import cn.novalon.manage.sys.infrastructure.db.converter.SysUserMessageConverter;
|
||||
import cn.novalon.manage.sys.infrastructure.db.dao.SysUserMessageDao;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class SysUserMessageServiceImpl implements ISysUserMessageService {
|
||||
|
||||
private final SysUserMessageDao dao;
|
||||
private final SysUserMessageConverter converter;
|
||||
|
||||
public SysUserMessageServiceImpl(SysUserMessageDao dao, SysUserMessageConverter converter) {
|
||||
this.dao = dao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysUserMessage> findByUserId(Long userId) {
|
||||
return dao.findByUserIdOrderByCreateTimeDesc(userId)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<SysUserMessage> findByUserIdAndIsRead(Long userId, String isRead) {
|
||||
return dao.findByUserIdAndIsReadOrderByCreateTimeDesc(userId, isRead)
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> countUnread(Long userId) {
|
||||
return dao.countByUserIdAndIsRead(userId, "0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUserMessage> save(SysUserMessage message) {
|
||||
return dao.save(converter.toEntity(message))
|
||||
.map(converter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> markAsRead(Long id) {
|
||||
return dao.findById(id)
|
||||
.flatMap(entity -> {
|
||||
entity.setIsRead("1");
|
||||
return dao.save(entity);
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package cn.novalon.manage.sys.core.service.impl;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
|
||||
import cn.novalon.manage.sys.core.service.ISysUserService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class SysUserService implements ISysUserService {
|
||||
|
||||
private final ISysUserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public SysUserService(ISysUserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUser> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUser> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUser> createUser(SysUser user) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setStatus(1);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUser> updateUser(SysUser user) {
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteUser(Long id) {
|
||||
return userRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SysUser> changePassword(Long userId, String oldPassword, String newPassword) {
|
||||
return userRepository.findById(userId)
|
||||
.flatMap(user -> {
|
||||
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
|
||||
return Mono.error(new RuntimeException("旧密码不正确"));
|
||||
}
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
return userRepository.save(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package cn.novalon.manage.sys.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class LoginRequest {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package cn.novalon.manage.sys.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class PasswordChangeRequest {
|
||||
|
||||
@NotBlank(message = "旧密码不能为空")
|
||||
private String oldPassword;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String newPassword;
|
||||
|
||||
public String getOldPassword() {
|
||||
return oldPassword;
|
||||
}
|
||||
|
||||
public void setOldPassword(String oldPassword) {
|
||||
this.oldPassword = oldPassword;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
}
|
||||
|
||||
public void setNewPassword(String newPassword) {
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package cn.novalon.manage.sys.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class UserRegisterRequest {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 100, message = "密码长度必须在6-100之间")
|
||||
private String password;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package cn.novalon.manage.sys.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
|
||||
public class UserUpdateRequest {
|
||||
|
||||
private String email;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Long roleId;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getRoleId() {
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public void setRoleId(Long roleId) {
|
||||
this.roleId = roleId;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package cn.novalon.manage.sys.dto.response;
|
||||
|
||||
public class AuthResponse {
|
||||
|
||||
private String token;
|
||||
private Long userId;
|
||||
private String username;
|
||||
|
||||
public AuthResponse() {
|
||||
}
|
||||
|
||||
public AuthResponse(String token, Long userId, String username) {
|
||||
this.token = token;
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package cn.novalon.manage.sys.dto.response;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class UserResponse {
|
||||
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private Long roleId;
|
||||
private Integer status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public Long getRoleId() {
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public void setRoleId(Long roleId) {
|
||||
this.roleId = roleId;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public static UserResponse fromDomain(cn.novalon.manage.sys.core.domain.SysUser user) {
|
||||
UserResponse response = new UserResponse();
|
||||
response.setId(user.getId());
|
||||
response.setUsername(user.getUsername());
|
||||
response.setEmail(user.getEmail());
|
||||
response.setRoleId(user.getRoleId());
|
||||
response.setStatus(user.getStatus());
|
||||
response.setCreatedAt(user.getCreatedAt());
|
||||
response.setUpdatedAt(user.getUpdatedAt());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package cn.novalon.manage.sys.handler;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
|
||||
import cn.novalon.manage.sys.core.service.ISysExceptionLogService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
private final ISysExceptionLogService exceptionLogService;
|
||||
|
||||
public GlobalExceptionHandler(ISysExceptionLogService exceptionLogService) {
|
||||
this.exceptionLogService = exceptionLogService;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(Exception ex, WebRequest request) {
|
||||
logger.error("Exception occurred: ", ex);
|
||||
|
||||
SysExceptionLog exceptionLog = new SysExceptionLog();
|
||||
exceptionLog.setTitle("System Exception");
|
||||
exceptionLog.setExceptionName(ex.getClass().getSimpleName());
|
||||
exceptionLog.setExceptionMsg(ex.getMessage());
|
||||
exceptionLog.setMethodName(request.getDescription(false));
|
||||
exceptionLog.setIp(getClientIp(request));
|
||||
exceptionLog.setCreateTime(LocalDateTime.now());
|
||||
|
||||
StringBuilder stackTrace = new StringBuilder();
|
||||
for (StackTraceElement element : ex.getStackTrace()) {
|
||||
stackTrace.append(element.toString()).append("\n");
|
||||
}
|
||||
exceptionLog.setExceptionStack(stackTrace.toString());
|
||||
|
||||
exceptionLogService.save(exceptionLog).subscribe();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
response.put("message", ex.getMessage());
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) {
|
||||
logger.warn("Illegal argument: ", ex);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", HttpStatus.BAD_REQUEST.value());
|
||||
response.put("message", ex.getMessage());
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
|
||||
}
|
||||
|
||||
private String getClientIp(WebRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package cn.novalon.manage.sys.handler.auth;
|
||||
|
||||
import cn.novalon.manage.sys.dto.request.LoginRequest;
|
||||
import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
|
||||
import cn.novalon.manage.sys.dto.response.AuthResponse;
|
||||
import cn.novalon.manage.sys.security.JwtTokenProvider;
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.service.ISysUserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class SysAuthHandler {
|
||||
|
||||
private final ISysUserService userService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
public SysAuthHandler(ISysUserService userService, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider) {
|
||||
this.userService = userService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public Mono<ResponseEntity<AuthResponse>> login(@Valid @RequestBody LoginRequest request) {
|
||||
return userService.findByUsername(request.getUsername())
|
||||
.filter(user -> passwordEncoder.matches(request.getPassword(), user.getPassword()))
|
||||
.filter(user -> 1 == user.getStatus())
|
||||
.map(user -> {
|
||||
String token = jwtTokenProvider.generateToken(user.getUsername(), user.getId());
|
||||
AuthResponse response = new AuthResponse(token, user.getId(), user.getUsername());
|
||||
return ResponseEntity.ok(response);
|
||||
})
|
||||
.defaultIfEmpty(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public Mono<ResponseEntity<SysUser>> register(@Valid @RequestBody UserRegisterRequest request) {
|
||||
SysUser user = new SysUser();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(request.getPassword());
|
||||
user.setEmail(request.getEmail());
|
||||
return userService.findByUsername(request.getUsername())
|
||||
.flatMap(existing -> Mono.<ResponseEntity<SysUser>>error(new RuntimeException("用户名已存在")))
|
||||
.switchIfEmpty(userService.createUser(user)
|
||||
.map(u -> ResponseEntity.status(HttpStatus.CREATED).body(u)));
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public Mono<ResponseEntity<Void>> logout() {
|
||||
return Mono.just(ResponseEntity.ok().build());
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysConfig;
|
||||
import cn.novalon.manage.sys.core.service.ISysConfigService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/config")
|
||||
public class SysConfigHandler {
|
||||
|
||||
private final ISysConfigService configService;
|
||||
|
||||
public SysConfigHandler(ISysConfigService configService) {
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<SysConfig> getAllConfigs() {
|
||||
return configService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysConfig>> getConfigById(@PathVariable Long id) {
|
||||
return configService.findById(id)
|
||||
.map(config -> ResponseEntity.ok(config))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/key/{configKey}")
|
||||
public Mono<ResponseEntity<SysConfig>> getConfigByKey(@PathVariable String configKey) {
|
||||
return configService.findByConfigKey(configKey)
|
||||
.map(config -> ResponseEntity.ok(config))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<SysConfig>> createConfig(@RequestBody SysConfig config) {
|
||||
config.setConfigType("N");
|
||||
config.setCreatedAt(LocalDateTime.now());
|
||||
return configService.save(config)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysConfig>> updateConfig(@PathVariable Long id, @RequestBody SysConfig config) {
|
||||
config.setId(id);
|
||||
config.setUpdatedAt(LocalDateTime.now());
|
||||
return configService.save(config)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteConfig(@PathVariable Long id) {
|
||||
return configService.deleteById(id)
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictType;
|
||||
import cn.novalon.manage.sys.core.service.ISysDictTypeService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dict")
|
||||
public class SysDictHandler {
|
||||
|
||||
private final ISysDictTypeService dictTypeService;
|
||||
|
||||
public SysDictHandler(ISysDictTypeService dictTypeService) {
|
||||
this.dictTypeService = dictTypeService;
|
||||
}
|
||||
|
||||
@GetMapping("/types")
|
||||
public Flux<SysDictType> getAllDictTypes() {
|
||||
return dictTypeService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/types/{id}")
|
||||
public Mono<ResponseEntity<SysDictType>> getDictTypeById(@PathVariable Long id) {
|
||||
return dictTypeService.findById(id)
|
||||
.map(dictType -> ResponseEntity.ok(dictType))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/types/type/{dictType}")
|
||||
public Mono<ResponseEntity<SysDictType>> getDictTypeByDictType(@PathVariable String dictType) {
|
||||
return dictTypeService.findByDictType(dictType)
|
||||
.map(dictTypeResult -> ResponseEntity.ok(dictTypeResult))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/types")
|
||||
public Mono<ResponseEntity<SysDictType>> createDictType(@RequestBody SysDictType dictType) {
|
||||
dictType.setStatus("0");
|
||||
dictType.setCreatedAt(LocalDateTime.now());
|
||||
return dictTypeService.save(dictType)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@PutMapping("/types/{id}")
|
||||
public Mono<ResponseEntity<SysDictType>> updateDictType(@PathVariable Long id, @RequestBody SysDictType dictType) {
|
||||
dictType.setId(id);
|
||||
dictType.setUpdatedAt(LocalDateTime.now());
|
||||
return dictTypeService.save(dictType)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@DeleteMapping("/types/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteDictType(@PathVariable Long id) {
|
||||
return dictTypeService.deleteById(id)
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysFile;
|
||||
import cn.novalon.manage.sys.core.service.ISysFileService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
public class SysFileHandler {
|
||||
|
||||
private final ISysFileService fileService;
|
||||
private final Path uploadPath = Paths.get("./uploads");
|
||||
|
||||
public SysFileHandler(ISysFileService fileService) {
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<SysFile> getAllFiles() {
|
||||
return fileService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysFile>> getFileById(@PathVariable Long id) {
|
||||
return fileService.findById(id)
|
||||
.map(file -> ResponseEntity.ok(file))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public Mono<ResponseEntity<SysFile>> uploadFile(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "createBy", required = false, defaultValue = "anonymous") String createBy) {
|
||||
return fileService.upload(file, createBy)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@GetMapping("/download/{fileName}")
|
||||
public Mono<Resource> downloadFile(@PathVariable String fileName) throws MalformedURLException {
|
||||
Path filePath = uploadPath.resolve(fileName);
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
return Mono.just(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/preview/{fileName}")
|
||||
public Mono<ResponseEntity<byte[]>> previewFile(@PathVariable String fileName) throws MalformedURLException {
|
||||
return Mono.fromCallable(() -> {
|
||||
Path filePath = uploadPath.resolve(fileName);
|
||||
byte[] data = Files.readAllBytes(filePath);
|
||||
return data;
|
||||
}).map(data -> {
|
||||
String contentType = "application/octet-stream";
|
||||
try {
|
||||
contentType = Files.probeContentType(uploadPath.resolve(fileName));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.body(data);
|
||||
});
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteFile(@PathVariable Long id) {
|
||||
return fileService.findById(id)
|
||||
.flatMap(file -> {
|
||||
try {
|
||||
String fileName = file.getFilePath().substring(file.getFilePath().lastIndexOf("/") + 1);
|
||||
Files.deleteIfExists(uploadPath.resolve(fileName));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return fileService.deleteById(id);
|
||||
})
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysLoginLog;
|
||||
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
|
||||
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
|
||||
import cn.novalon.manage.sys.core.service.ISysExceptionLogService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/logs")
|
||||
public class SysLogHandler {
|
||||
|
||||
private final ISysLoginLogService loginLogService;
|
||||
private final ISysExceptionLogService exceptionLogService;
|
||||
|
||||
public SysLogHandler(ISysLoginLogService loginLogService, ISysExceptionLogService exceptionLogService) {
|
||||
this.loginLogService = loginLogService;
|
||||
this.exceptionLogService = exceptionLogService;
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public Flux<SysLoginLog> getLoginLogs() {
|
||||
return loginLogService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/login/{id}")
|
||||
public Mono<ResponseEntity<SysLoginLog>> getLoginLogById(@PathVariable Long id) {
|
||||
return loginLogService.findAll()
|
||||
.filter(log -> log.getId().equals(id))
|
||||
.next()
|
||||
.map(log -> ResponseEntity.ok(log))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public Mono<ResponseEntity<SysLoginLog>> createLoginLog(@RequestBody SysLoginLog log) {
|
||||
log.setLoginTime(LocalDateTime.now());
|
||||
return loginLogService.save(log)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@GetMapping("/login/user/{username}")
|
||||
public Flux<SysLoginLog> getLoginLogsByUsername(@PathVariable String username) {
|
||||
return loginLogService.findByUsername(username);
|
||||
}
|
||||
|
||||
@GetMapping("/exception")
|
||||
public Flux<SysExceptionLog> getExceptionLogs() {
|
||||
return exceptionLogService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/exception/{id}")
|
||||
public Mono<ResponseEntity<SysExceptionLog>> getExceptionLogById(@PathVariable Long id) {
|
||||
return exceptionLogService.findAll()
|
||||
.filter(log -> log.getId().equals(id))
|
||||
.next()
|
||||
.map(log -> ResponseEntity.ok(log))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/exception/user/{username}")
|
||||
public Flux<SysExceptionLog> getExceptionLogsByUsername(@PathVariable String username) {
|
||||
return exceptionLogService.findByUsername(username);
|
||||
}
|
||||
|
||||
@PostMapping("/exception")
|
||||
public Mono<ResponseEntity<SysExceptionLog>> createExceptionLog(@RequestBody SysExceptionLog log) {
|
||||
log.setCreateTime(LocalDateTime.now());
|
||||
return exceptionLogService.save(log)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysMenu;
|
||||
import cn.novalon.manage.sys.core.service.ISysMenuService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/menus")
|
||||
public class SysMenuHandler {
|
||||
|
||||
private final ISysMenuService menuService;
|
||||
|
||||
public SysMenuHandler(ISysMenuService menuService) {
|
||||
this.menuService = menuService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<SysMenu> getAllMenus() {
|
||||
return menuService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
public Flux<SysMenu> getMenuTree() {
|
||||
return menuService.buildMenuTree(menuService.findAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysMenu>> getMenuById(@PathVariable Long id) {
|
||||
return menuService.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<SysMenu>> createMenu(@RequestBody SysMenu menu) {
|
||||
return menuService.createMenu(menu)
|
||||
.map(m -> ResponseEntity.status(HttpStatus.CREATED).body(m));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysMenu>> updateMenu(@PathVariable Long id, @RequestBody SysMenu menu) {
|
||||
menu.setId(id);
|
||||
return menuService.updateMenu(menu)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteMenu(@PathVariable Long id) {
|
||||
return menuService.deleteMenu(id)
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUserMessage;
|
||||
import cn.novalon.manage.sys.core.service.ISysUserMessageService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/messages")
|
||||
public class SysMessageHandler {
|
||||
|
||||
private final ISysUserMessageService messageService;
|
||||
|
||||
public SysMessageHandler(ISysUserMessageService messageService) {
|
||||
this.messageService = messageService;
|
||||
}
|
||||
|
||||
@GetMapping("/user/{userId}")
|
||||
public Flux<SysUserMessage> getMessagesByUserId(@PathVariable Long userId) {
|
||||
return messageService.findByUserId(userId);
|
||||
}
|
||||
|
||||
@GetMapping("/user/{userId}/unread")
|
||||
public Mono<Long> getUnreadCount(@PathVariable Long userId) {
|
||||
return messageService.countUnread(userId);
|
||||
}
|
||||
|
||||
@GetMapping("/user/{userId}/unread/list")
|
||||
public Flux<SysUserMessage> getUnreadMessages(@PathVariable Long userId) {
|
||||
return messageService.findByUserIdAndIsRead(userId, "0");
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<SysUserMessage>> createMessage(@RequestBody SysUserMessage message) {
|
||||
message.setIsRead("0");
|
||||
return messageService.save(message)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/read")
|
||||
public Mono<ResponseEntity<Void>> markAsRead(@PathVariable Long id) {
|
||||
return messageService.markAsRead(id)
|
||||
.then(Mono.just(ResponseEntity.ok().<Void>build()));
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysNotice;
|
||||
import cn.novalon.manage.sys.core.service.ISysNoticeService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/notices")
|
||||
public class SysNoticeHandler {
|
||||
|
||||
private final ISysNoticeService noticeService;
|
||||
|
||||
public SysNoticeHandler(ISysNoticeService noticeService) {
|
||||
this.noticeService = noticeService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<SysNotice> getAllNotices() {
|
||||
return noticeService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysNotice>> getNoticeById(@PathVariable Long id) {
|
||||
return noticeService.findById(id)
|
||||
.map(notice -> ResponseEntity.ok(notice))
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/status/{status}")
|
||||
public Flux<SysNotice> getNoticesByStatus(@PathVariable String status) {
|
||||
return noticeService.findByStatus(status);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<SysNotice>> createNotice(@RequestBody SysNotice notice) {
|
||||
notice.setStatus("0");
|
||||
notice.setCreatedAt(LocalDateTime.now());
|
||||
return noticeService.save(notice)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysNotice>> updateNotice(@PathVariable Long id, @RequestBody SysNotice notice) {
|
||||
notice.setId(id);
|
||||
notice.setUpdatedAt(LocalDateTime.now());
|
||||
return noticeService.save(notice)
|
||||
.map(saved -> ResponseEntity.ok(saved));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteNotice(@PathVariable Long id) {
|
||||
return noticeService.deleteById(id)
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package cn.novalon.manage.sys.handler.sys;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
import cn.novalon.manage.sys.core.service.ISysRoleService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/roles")
|
||||
public class SysRoleHandler {
|
||||
|
||||
private final ISysRoleService roleService;
|
||||
|
||||
public SysRoleHandler(ISysRoleService roleService) {
|
||||
this.roleService = roleService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<SysRole> getAllRoles() {
|
||||
return roleService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysRole>> getRoleById(@PathVariable Long id) {
|
||||
return roleService.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<SysRole>> createRole(@RequestBody SysRole role) {
|
||||
return roleService.createRole(role)
|
||||
.map(r -> ResponseEntity.status(HttpStatus.CREATED).body(r));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysRole>> updateRole(@PathVariable Long id, @RequestBody SysRole role) {
|
||||
role.setId(id);
|
||||
return roleService.updateRole(role)
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteRole(@PathVariable Long id) {
|
||||
return roleService.deleteRole(id)
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package cn.novalon.manage.sys.handler.user;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.service.ISysUserService;
|
||||
import cn.novalon.manage.sys.dto.request.PasswordChangeRequest;
|
||||
import cn.novalon.manage.sys.dto.request.UserUpdateRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class SysUserHandler {
|
||||
|
||||
private final ISysUserService userService;
|
||||
|
||||
public SysUserHandler(ISysUserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysUser>> getUserById(@PathVariable Long id) {
|
||||
return userService.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/username/{username}")
|
||||
public Mono<ResponseEntity<SysUser>> getUserByUsername(@PathVariable String username) {
|
||||
return userService.findByUsername(username)
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<ResponseEntity<SysUser>> createUser(@RequestBody SysUser user) {
|
||||
return userService.createUser(user)
|
||||
.map(u -> ResponseEntity.status(HttpStatus.CREATED).body(u));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<ResponseEntity<SysUser>> updateUser(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) {
|
||||
return userService.findById(id)
|
||||
.flatMap(existing -> {
|
||||
if (request.getEmail() != null) {
|
||||
existing.setEmail(request.getEmail());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
existing.setStatus(request.getStatus());
|
||||
}
|
||||
if (request.getRoleId() != null) {
|
||||
existing.setRoleId(request.getRoleId());
|
||||
}
|
||||
return userService.updateUser(existing);
|
||||
})
|
||||
.map(ResponseEntity::ok)
|
||||
.defaultIfEmpty(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<ResponseEntity<Void>> deleteUser(@PathVariable Long id) {
|
||||
return userService.deleteUser(id)
|
||||
.then(Mono.just(ResponseEntity.noContent().build()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/password")
|
||||
public Mono<ResponseEntity<SysUser>> changePassword(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody PasswordChangeRequest request) {
|
||||
return userService.changePassword(id, request.getOldPassword(), request.getNewPassword())
|
||||
.map(ResponseEntity::ok);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package cn.novalon.manage.sys.infrastructure.db.converter;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.OperationLog;
|
||||
import cn.novalon.manage.sys.infrastructure.db.entity.OperationLogEntity;
|
||||
|
||||
public class OperationLogConverter {
|
||||
|
||||
public OperationLog toDomain(OperationLogEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
OperationLog domain = new OperationLog();
|
||||
domain.setId(entity.getId());
|
||||
domain.setUsername(entity.getUsername());
|
||||
domain.setOperation(entity.getOperation());
|
||||
domain.setMethod(entity.getMethod());
|
||||
domain.setParams(entity.getParams());
|
||||
domain.setResult(entity.getResult());
|
||||
domain.setIp(entity.getIp());
|
||||
domain.setDuration(entity.getDuration());
|
||||
domain.setStatus(entity.getStatus());
|
||||
domain.setErrorMsg(entity.getErrorMsg());
|
||||
domain.setCreatedAt(entity.getCreatedAt());
|
||||
domain.setUpdatedAt(entity.getUpdatedAt());
|
||||
domain.setDeletedAt(entity.getDeletedAt());
|
||||
return domain;
|
||||
}
|
||||
|
||||
public OperationLogEntity toEntity(OperationLog domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
OperationLogEntity entity = new OperationLogEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setUsername(domain.getUsername());
|
||||
entity.setOperation(domain.getOperation());
|
||||
entity.setMethod(domain.getMethod());
|
||||
entity.setParams(domain.getParams());
|
||||
entity.setResult(domain.getResult());
|
||||
entity.setIp(domain.getIp());
|
||||
entity.setDuration(domain.getDuration());
|
||||
entity.setStatus(domain.getStatus());
|
||||
entity.setErrorMsg(domain.getErrorMsg());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
entity.setDeletedAt(domain.getDeletedAt());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package cn.novalon.manage.sys.infrastructure.db.converter;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysConfig;
|
||||
import cn.novalon.manage.sys.infrastructure.db.entity.SysConfigEntity;
|
||||
|
||||
public class SysConfigConverter {
|
||||
|
||||
public SysConfig toDomain(SysConfigEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysConfig domain = new SysConfig();
|
||||
domain.setId(entity.getId());
|
||||
domain.setConfigName(entity.getConfigName());
|
||||
domain.setConfigKey(entity.getConfigKey());
|
||||
domain.setConfigValue(entity.getConfigValue());
|
||||
domain.setConfigType(entity.getConfigType());
|
||||
domain.setCreateBy(entity.getCreateBy());
|
||||
domain.setUpdateBy(entity.getUpdateBy());
|
||||
domain.setCreatedAt(entity.getCreatedAt());
|
||||
domain.setUpdatedAt(entity.getUpdatedAt());
|
||||
return domain;
|
||||
}
|
||||
|
||||
public SysConfigEntity toEntity(SysConfig domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
SysConfigEntity entity = new SysConfigEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setConfigName(domain.getConfigName());
|
||||
entity.setConfigKey(domain.getConfigKey());
|
||||
entity.setConfigValue(domain.getConfigValue());
|
||||
entity.setConfigType(domain.getConfigType());
|
||||
entity.setCreateBy(domain.getCreateBy());
|
||||
entity.setUpdateBy(domain.getUpdateBy());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package cn.novalon.manage.sys.infrastructure.db.converter;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictData;
|
||||
import cn.novalon.manage.sys.infrastructure.db.entity.SysDictDataEntity;
|
||||
|
||||
public class SysDictDataConverter {
|
||||
|
||||
public SysDictData toDomain(SysDictDataEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysDictData domain = new SysDictData();
|
||||
domain.setId(entity.getId());
|
||||
domain.setDictSort(entity.getDictSort());
|
||||
domain.setDictLabel(entity.getDictLabel());
|
||||
domain.setDictValue(entity.getDictValue());
|
||||
domain.setDictType(entity.getDictType());
|
||||
domain.setCssClass(entity.getCssClass());
|
||||
domain.setListClass(entity.getListClass());
|
||||
domain.setIsDefault(entity.getIsDefault());
|
||||
domain.setStatus(entity.getStatus());
|
||||
domain.setCreateBy(entity.getCreateBy());
|
||||
domain.setUpdateBy(entity.getUpdateBy());
|
||||
domain.setCreatedAt(entity.getCreatedAt());
|
||||
domain.setUpdatedAt(entity.getUpdatedAt());
|
||||
return domain;
|
||||
}
|
||||
|
||||
public SysDictDataEntity toEntity(SysDictData domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
SysDictDataEntity entity = new SysDictDataEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setDictSort(domain.getDictSort());
|
||||
entity.setDictLabel(domain.getDictLabel());
|
||||
entity.setDictValue(domain.getDictValue());
|
||||
entity.setDictType(domain.getDictType());
|
||||
entity.setCssClass(domain.getCssClass());
|
||||
entity.setListClass(domain.getListClass());
|
||||
entity.setIsDefault(domain.getIsDefault());
|
||||
entity.setStatus(domain.getStatus());
|
||||
entity.setCreateBy(domain.getCreateBy());
|
||||
entity.setUpdateBy(domain.getUpdateBy());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package cn.novalon.manage.sys.infrastructure.db.converter;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysDictType;
|
||||
import cn.novalon.manage.sys.infrastructure.db.entity.SysDictTypeEntity;
|
||||
|
||||
public class SysDictTypeConverter {
|
||||
|
||||
public SysDictType toDomain(SysDictTypeEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysDictType domain = new SysDictType();
|
||||
domain.setId(entity.getId());
|
||||
domain.setDictName(entity.getDictName());
|
||||
domain.setDictType(entity.getDictType());
|
||||
domain.setStatus(entity.getStatus());
|
||||
domain.setRemark(entity.getRemark());
|
||||
domain.setCreateBy(entity.getCreateBy());
|
||||
domain.setUpdateBy(entity.getUpdateBy());
|
||||
domain.setCreatedAt(entity.getCreatedAt());
|
||||
domain.setUpdatedAt(entity.getUpdatedAt());
|
||||
return domain;
|
||||
}
|
||||
|
||||
public SysDictTypeEntity toEntity(SysDictType domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
SysDictTypeEntity entity = new SysDictTypeEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setDictName(domain.getDictName());
|
||||
entity.setDictType(domain.getDictType());
|
||||
entity.setStatus(domain.getStatus());
|
||||
entity.setRemark(domain.getRemark());
|
||||
entity.setCreateBy(domain.getCreateBy());
|
||||
entity.setUpdateBy(domain.getUpdateBy());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package cn.novalon.manage.sys.infrastructure.db.converter;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
|
||||
import cn.novalon.manage.sys.infrastructure.db.entity.SysExceptionLogEntity;
|
||||
|
||||
public class SysExceptionLogConverter {
|
||||
|
||||
public SysExceptionLog toDomain(SysExceptionLogEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysExceptionLog domain = new SysExceptionLog();
|
||||
domain.setId(entity.getId());
|
||||
domain.setUsername(entity.getUsername());
|
||||
domain.setTitle(entity.getTitle());
|
||||
domain.setExceptionName(entity.getExceptionName());
|
||||
domain.setMethodName(entity.getMethodName());
|
||||
domain.setMethodParams(entity.getMethodParams());
|
||||
domain.setExceptionMsg(entity.getExceptionMsg());
|
||||
domain.setExceptionStack(entity.getExceptionStack());
|
||||
domain.setIp(entity.getIp());
|
||||
domain.setCreateTime(entity.getCreateTime());
|
||||
return domain;
|
||||
}
|
||||
|
||||
public SysExceptionLogEntity toEntity(SysExceptionLog domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
SysExceptionLogEntity entity = new SysExceptionLogEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setUsername(domain.getUsername());
|
||||
entity.setTitle(domain.getTitle());
|
||||
entity.setExceptionName(domain.getExceptionName());
|
||||
entity.setMethodName(domain.getMethodName());
|
||||
entity.setMethodParams(domain.getMethodParams());
|
||||
entity.setExceptionMsg(domain.getExceptionMsg());
|
||||
entity.setExceptionStack(domain.getExceptionStack());
|
||||
entity.setIp(domain.getIp());
|
||||
entity.setCreateTime(domain.getCreateTime());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user