feat: 添加系统配置、审计中心、通知中心、文件管理模块

This commit is contained in:
张翔
2026-03-11 12:11:59 +08:00
commit 52c66444a5
264 changed files with 10109 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+707
View File
@@ -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. 测试覆盖率
## 缺陷分类
- 严重: 系统崩溃、数据丢失
- 高: 核心功能不可用
- 中: 功能部分不可用
- 低: 界面、文案问题
+339
View File
@@ -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 | 待办事宜清单 |
+14
View File
@@ -0,0 +1,14 @@
{
"sqltools.connections": [
{
"ssh": "Disabled",
"previewLimit": 50,
"server": "localhost",
"port": 55432,
"driver": "PostgreSQL",
"name": "local",
"database": "postgres",
"username": "postgres"
}
]
}
+61
View File
@@ -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: 测试
+147
View File
@@ -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);
+22
View File
@@ -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
+55
View File
@@ -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/
+104
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
"""
E2E测试项目 - Novalon管理系统
使用Playwright进行端到端测试
"""
__version__ = "1.0.0"
+1
View File
@@ -0,0 +1 @@
"""API模块"""
+33
View File
@@ -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}"
})
+58
View File
@@ -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]}"
+42
View File
@@ -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})
+58
View File
@@ -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})
+58
View File
@@ -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})
+1
View File
@@ -0,0 +1 @@
"""配置模块"""
+74
View File
@@ -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()
+161
View File
@@ -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
+23
View File
@@ -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
+29
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
"""测试模块"""
+83
View File
@@ -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
+149
View File
@@ -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
+127
View File
@@ -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
+185
View File
@@ -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
+193
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
"""
工具模块
"""
+83
View File
@@ -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()
+72
View File
@@ -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()
+33
View File
@@ -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()
+96
View File
@@ -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>
@@ -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);
}
}
@@ -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();
}
}
@@ -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) {
}
});
}
}
@@ -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("*");
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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; }
}
@@ -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; }
}
@@ -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; }
}
@@ -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; }
}
@@ -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; }
}
@@ -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; }
}
@@ -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;
}
}
@@ -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; }
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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; }
}
@@ -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);
}
@@ -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);
}
@@ -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();
}
@@ -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();
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
});
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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));
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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