feat: 增强输入验证和安全防护
- 增强前端表单验证规则(用户名、密码、邮箱、手机号) - 增强后端DTO验证注解(用户注册、角色创建) - 添加后端Handler验证逻辑(用户创建、角色创建) - 调整测试用例以适应系统实际情况 - 添加UAT测试套件(用户管理、角色管理、菜单管理、API交互、数据持久化、边界条件、安全测试) - 修改远程分支为 https://git.f.novalon.cn/novalon/novalon-manage-system.git
This commit is contained in:
@@ -23,8 +23,9 @@ novalon-manage-system/
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
|
||||
- Java 21
|
||||
- Spring Boot 3.5.12
|
||||
- Spring Boot 3.5.13
|
||||
- Spring Cloud Gateway
|
||||
- Spring Security + JWT
|
||||
- R2DBC (响应式数据库访问)
|
||||
@@ -32,6 +33,7 @@ novalon-manage-system/
|
||||
- Flyway (数据库迁移)
|
||||
|
||||
### 前端
|
||||
|
||||
- Vue 3 + TypeScript
|
||||
- Element Plus
|
||||
- Pinia (状态管理)
|
||||
@@ -45,28 +47,33 @@ novalon-manage-system/
|
||||
使用 Docker Compose 可以一键启动所有服务,包括数据库、后端和前端。
|
||||
|
||||
#### 前置要求
|
||||
|
||||
- Docker 20.10+
|
||||
- Docker Compose 2.0+
|
||||
|
||||
#### 启动步骤
|
||||
|
||||
1. **克隆项目**
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd novalon-manage-system
|
||||
```
|
||||
|
||||
2. **启动所有服务**
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
3. **查看服务状态**
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
4. **查看日志**
|
||||
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker-compose logs -f
|
||||
@@ -78,17 +85,20 @@ docker-compose logs -f frontend
|
||||
```
|
||||
|
||||
5. **访问应用**
|
||||
|
||||
- 前端应用: http://localhost:3001
|
||||
- 后端 API: http://localhost:8084
|
||||
- API 文档: http://localhost:8084/swagger-ui.html
|
||||
- 健康检查: http://localhost:8084/actuator/health
|
||||
|
||||
#### 停止服务
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### 清理数据(包括数据库数据)
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
@@ -98,6 +108,7 @@ docker-compose down -v
|
||||
#### 1. 环境准备要求
|
||||
|
||||
##### 必需软件
|
||||
|
||||
- **Java**: JDK 21 或更高版本
|
||||
- **Maven**: 3.8+ (用于后端构建)
|
||||
- **Node.js**: 18+ (用于前端构建)
|
||||
@@ -106,10 +117,12 @@ docker-compose down -v
|
||||
- **Git**: 版本控制
|
||||
|
||||
##### 可选软件
|
||||
|
||||
- **Docker**: 用于容器化部署
|
||||
- **IDE**: IntelliJ IDEA (推荐) 或 VS Code
|
||||
|
||||
##### 系统要求
|
||||
|
||||
- **操作系统**: macOS, Linux, Windows
|
||||
- **内存**: 最低 4GB,推荐 8GB+
|
||||
- **磁盘空间**: 最低 2GB 可用空间
|
||||
@@ -119,6 +132,7 @@ docker-compose down -v
|
||||
##### 2.1 安装 Java 和 Maven
|
||||
|
||||
**macOS (使用 Homebrew)**:
|
||||
|
||||
```bash
|
||||
brew install openjdk@21
|
||||
brew install maven
|
||||
@@ -134,6 +148,7 @@ mvn -version
|
||||
```
|
||||
|
||||
**Linux (Ubuntu/Debian)**:
|
||||
|
||||
```bash
|
||||
# 安装 OpenJDK 21
|
||||
sudo apt update
|
||||
@@ -148,6 +163,7 @@ mvn -version
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
|
||||
1. 下载并安装 JDK 21: https://adoptium.net/
|
||||
2. 下载并安装 Maven: https://maven.apache.org/download.cgi
|
||||
3. 设置环境变量:
|
||||
@@ -158,6 +174,7 @@ mvn -version
|
||||
##### 2.2 安装 Node.js 和 pnpm
|
||||
|
||||
**使用 nvm (推荐)**:
|
||||
|
||||
```bash
|
||||
# 安装 nvm
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
@@ -178,14 +195,17 @@ pnpm -v
|
||||
```
|
||||
|
||||
**macOS (使用 Homebrew)**:
|
||||
|
||||
```bash
|
||||
brew install node
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
|
||||
1. 下载并安装 Node.js: https://nodejs.org/
|
||||
2. 安装 pnpm:
|
||||
|
||||
```powershell
|
||||
npm install -g pnpm
|
||||
```
|
||||
@@ -193,6 +213,7 @@ npm install -g pnpm
|
||||
##### 2.3 安装 PostgreSQL
|
||||
|
||||
**macOS (使用 Homebrew)**:
|
||||
|
||||
```bash
|
||||
brew install postgresql@15
|
||||
brew services start postgresql@15
|
||||
@@ -202,6 +223,7 @@ psql postgres
|
||||
```
|
||||
|
||||
在 psql 中执行:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE manage_system;
|
||||
CREATE USER novalon WITH PASSWORD 'novalon123';
|
||||
@@ -210,6 +232,7 @@ GRANT ALL PRIVILEGES ON DATABASE manage_system TO novalon;
|
||||
```
|
||||
|
||||
**Linux (Ubuntu/Debian)**:
|
||||
|
||||
```bash
|
||||
sudo apt install postgresql-15 postgresql-contrib-15
|
||||
sudo systemctl start postgresql
|
||||
@@ -219,6 +242,7 @@ sudo -u postgres psql
|
||||
```
|
||||
|
||||
在 psql 中执行:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE manage_system;
|
||||
CREATE USER novalon WITH PASSWORD 'novalon123';
|
||||
@@ -227,12 +251,14 @@ GRANT ALL PRIVILEGES ON DATABASE manage_system TO novalon;
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
|
||||
1. 下载并安装 PostgreSQL: https://www.postgresql.org/download/windows/
|
||||
2. 使用 pgAdmin 创建数据库和用户,或使用命令行工具
|
||||
|
||||
##### 2.4 验证环境
|
||||
|
||||
创建并运行环境检查脚本:
|
||||
|
||||
```bash
|
||||
# 检查 Java
|
||||
java -version
|
||||
@@ -253,6 +279,7 @@ psql --version
|
||||
后端使用 Flyway 自动管理数据库迁移,数据库表结构会在首次启动时自动创建。
|
||||
|
||||
**开发环境配置** (`novalon-manage-api/manage-app/src/main/resources/application-dev.yml`):
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
r2dbc:
|
||||
@@ -264,6 +291,7 @@ spring:
|
||||
```
|
||||
|
||||
**生产环境配置** (`novalon-manage-api/manage-app/src/main/resources/application-prod.yml`):
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
r2dbc:
|
||||
@@ -307,6 +335,7 @@ psql -U novalon -d manage_system -c "\dt"
|
||||
##### 4.1 网关服务概述
|
||||
|
||||
`manage-gateway` 是系统的 API 网关,负责:
|
||||
|
||||
- 请求路由和转发
|
||||
- JWT 认证过滤
|
||||
- RBAC 权限控制
|
||||
@@ -316,6 +345,7 @@ psql -U novalon -d manage_system -c "\dt"
|
||||
##### 4.2 网关配置文件
|
||||
|
||||
**主配置** (`novalon-manage-api/manage-gateway/src/main/resources/application.yml`):
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
@@ -372,6 +402,7 @@ logging:
|
||||
网关将所有 `/api/**` 路径的请求转发到 `manage-app` 服务 (端口 8084)。
|
||||
|
||||
**路由规则**:
|
||||
|
||||
- 所有以 `/api/` 开头的请求都会被转发到后端服务
|
||||
- 请求会经过 JWT 认证和 RBAC 权限验证
|
||||
- 失败的请求会自动重试(最多 3 次)
|
||||
@@ -379,10 +410,12 @@ logging:
|
||||
##### 4.4 JWT 配置
|
||||
|
||||
**环境变量**:
|
||||
|
||||
- `JWT_SECRET`: JWT 密钥(生产环境必须设置强密钥)
|
||||
- `JWT_EXPIRATION`: Token 过期时间(毫秒,默认 24 小时)
|
||||
|
||||
**示例**:
|
||||
|
||||
```bash
|
||||
export JWT_SECRET="your-strong-secret-key-here"
|
||||
export JWT_EXPIRATION="86400000"
|
||||
@@ -406,16 +439,19 @@ curl http://localhost:8080/actuator/metrics
|
||||
##### 5.1 启动后端服务
|
||||
|
||||
**步骤 1: 进入后端项目目录**
|
||||
|
||||
```bash
|
||||
cd novalon-manage-api
|
||||
```
|
||||
|
||||
**步骤 2: 编译项目**
|
||||
|
||||
```bash
|
||||
mvn clean install -DskipTests
|
||||
```
|
||||
|
||||
**步骤 3: 启动网关服务**
|
||||
|
||||
```bash
|
||||
cd manage-gateway
|
||||
mvn spring-boot:run
|
||||
@@ -425,6 +461,7 @@ mvn spring-boot:run
|
||||
|
||||
**步骤 4: 启动主应用服务**
|
||||
打开新的终端窗口:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-api/manage-app
|
||||
mvn spring-boot:run
|
||||
@@ -433,6 +470,7 @@ mvn spring-boot:run
|
||||
主应用将在 `http://localhost:8084` 启动。
|
||||
|
||||
**步骤 5: 验证后端服务**
|
||||
|
||||
```bash
|
||||
# 检查网关健康状态
|
||||
curl http://localhost:8080/actuator/health
|
||||
@@ -447,11 +485,13 @@ open http://localhost:8084/swagger-ui.html
|
||||
##### 5.2 启动前端服务
|
||||
|
||||
**步骤 1: 进入前端项目目录**
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
```
|
||||
|
||||
**步骤 2: 安装依赖**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
@@ -459,12 +499,14 @@ pnpm install
|
||||
**步骤 3: 配置环境变量**
|
||||
|
||||
创建 `.env.local` 文件(如果不存在):
|
||||
|
||||
```env
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_APP_TITLE=Novalon管理系统
|
||||
```
|
||||
|
||||
**步骤 4: 启动开发服务器**
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
@@ -479,6 +521,7 @@ pnpm dev
|
||||
##### 6.1 环境配置文件
|
||||
|
||||
后端支持多环境配置:
|
||||
|
||||
- `application.yml`: 主配置文件
|
||||
- `application-dev.yml`: 开发环境配置
|
||||
- `application-test.yml`: 测试环境配置
|
||||
@@ -488,18 +531,21 @@ pnpm dev
|
||||
##### 6.2 开发环境启动
|
||||
|
||||
**后端**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-api/manage-app
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
```
|
||||
|
||||
**前端**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 使用本地数据库 (localhost:55432)
|
||||
- DEBUG 日志级别
|
||||
- 热重载启用
|
||||
@@ -508,18 +554,21 @@ pnpm dev
|
||||
##### 6.3 测试环境启动
|
||||
|
||||
**后端**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-api/manage-app
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=test
|
||||
```
|
||||
|
||||
**前端**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
pnpm dev:test
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 使用测试数据库
|
||||
- INFO 日志级别
|
||||
- 性能监控启用
|
||||
@@ -528,6 +577,7 @@ pnpm dev:test
|
||||
##### 6.4 生产环境启动
|
||||
|
||||
**后端**:
|
||||
|
||||
```bash
|
||||
# 设置环境变量
|
||||
export DB_USERNAME=your_prod_db_user
|
||||
@@ -540,18 +590,21 @@ mvn spring-boot:run -Dspring-boot.run.profiles=prod
|
||||
```
|
||||
|
||||
**前端构建**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
pnpm build:prod
|
||||
```
|
||||
|
||||
**前端部署**:
|
||||
|
||||
```bash
|
||||
# 使用 nginx 或其他静态文件服务器部署 dist 目录
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 使用生产数据库
|
||||
- INFO/WARN 日志级别
|
||||
- 性能优化
|
||||
@@ -561,6 +614,7 @@ pnpm preview
|
||||
##### 6.5 Docker 环境启动
|
||||
|
||||
**使用 docker-compose**:
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
docker-compose -f docker-compose.yml up -d
|
||||
@@ -570,6 +624,7 @@ docker-compose -f docker-compose.test.yml up -d
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 容器化部署
|
||||
- 服务编排
|
||||
- 健康检查
|
||||
@@ -580,11 +635,13 @@ docker-compose -f docker-compose.test.yml up -d
|
||||
##### 7.1 端口冲突问题
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
Port 8080 was already in use
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 查找占用端口的进程
|
||||
lsof -i :8080 # macOS/Linux
|
||||
@@ -601,11 +658,13 @@ taskkill /PID <PID> /F # Windows
|
||||
##### 7.2 数据库连接失败
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
Connection refused: localhost:55432
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 检查 PostgreSQL 服务状态
|
||||
brew services list | grep postgresql # macOS
|
||||
@@ -625,11 +684,13 @@ sudo ufw allow 5432 # Linux
|
||||
##### 7.3 Maven 依赖下载失败
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
Could not resolve dependencies
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 清理 Maven 缓存
|
||||
rm -rf ~/.m2/repository
|
||||
@@ -645,11 +706,13 @@ ping repo.maven.apache.org
|
||||
##### 7.4 前端依赖安装失败
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
npm ERR! network request failed
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 清理缓存
|
||||
pnpm store prune
|
||||
@@ -665,12 +728,14 @@ pnpm install
|
||||
##### 7.5 JWT 认证失败
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
401 Unauthorized
|
||||
Invalid JWT token
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 检查 JWT_SECRET 配置
|
||||
echo $JWT_SECRET
|
||||
@@ -685,11 +750,13 @@ echo $JWT_SECRET
|
||||
##### 7.6 Flyway 迁移失败
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
FlywayException: Validate failed
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 查看迁移历史
|
||||
psql -U novalon -d manage_system -c "SELECT * FROM flyway_schema_history;"
|
||||
@@ -709,12 +776,14 @@ DELETE FROM flyway_schema_history WHERE success = false;
|
||||
##### 7.7 内存不足错误
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
Java heap space
|
||||
OutOfMemoryError
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 增加 JVM 内存
|
||||
export MAVEN_OPTS="-Xmx2g -Xms1g"
|
||||
@@ -732,11 +801,13 @@ export MAVEN_OPTS="-Xmx2g -Xms1g"
|
||||
##### 7.8 CORS 跨域问题
|
||||
|
||||
**症状**:
|
||||
|
||||
```
|
||||
Access to XMLHttpRequest blocked by CORS policy
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 检查网关 CORS 配置
|
||||
# 在 application.yml 中添加:
|
||||
@@ -760,6 +831,7 @@ spring:
|
||||
##### 7.9 日志查看和调试
|
||||
|
||||
**查看应用日志**:
|
||||
|
||||
```bash
|
||||
# 后端日志
|
||||
tail -f novalon-manage-api/manage-app/logs/application.log
|
||||
@@ -773,6 +845,7 @@ docker-compose logs -f gateway
|
||||
```
|
||||
|
||||
**启用 DEBUG 日志**:
|
||||
|
||||
```yaml
|
||||
# 在 application.yml 中设置
|
||||
logging:
|
||||
@@ -787,6 +860,7 @@ logging:
|
||||
##### 8.1 后端服务验证
|
||||
|
||||
**健康检查**:
|
||||
|
||||
```bash
|
||||
# 网关健康检查
|
||||
curl http://localhost:8080/actuator/health
|
||||
@@ -799,6 +873,7 @@ curl http://localhost:8084/actuator/health
|
||||
```
|
||||
|
||||
**API 文档访问**:
|
||||
|
||||
```bash
|
||||
# 在浏览器中打开
|
||||
open http://localhost:8084/swagger-ui.html
|
||||
@@ -808,6 +883,7 @@ curl http://localhost:8084/swagger-ui.html
|
||||
```
|
||||
|
||||
**数据库连接验证**:
|
||||
|
||||
```bash
|
||||
# 检查数据库表是否创建成功
|
||||
psql -U novalon -d manage_system -c "\dt"
|
||||
@@ -817,6 +893,7 @@ psql -U novalon -d manage_system -c "\dt"
|
||||
```
|
||||
|
||||
**API 端点测试**:
|
||||
|
||||
```bash
|
||||
# 测试登录接口
|
||||
curl -X POST http://localhost:8080/api/auth/login \
|
||||
@@ -830,12 +907,14 @@ curl -X POST http://localhost:8080/api/auth/login \
|
||||
##### 8.2 前端应用验证
|
||||
|
||||
**应用访问**:
|
||||
|
||||
```bash
|
||||
# 在浏览器中打开
|
||||
open http://localhost:5173
|
||||
```
|
||||
|
||||
**功能验证清单**:
|
||||
|
||||
- [ ] 登录页面正常显示
|
||||
- [ ] 能够成功登录(使用默认账号 admin/admin123)
|
||||
- [ ] 主页面正常加载
|
||||
@@ -845,6 +924,7 @@ open http://localhost:5173
|
||||
- [ ] 系统配置功能可用
|
||||
|
||||
**浏览器控制台检查**:
|
||||
|
||||
```javascript
|
||||
// 打开浏览器开发者工具 (F12)
|
||||
// 检查 Console 标签页,确保没有错误信息
|
||||
@@ -854,6 +934,7 @@ open http://localhost:5173
|
||||
##### 8.3 集成测试验证
|
||||
|
||||
**运行 API 集成测试**:
|
||||
|
||||
```bash
|
||||
cd api_integration_tests
|
||||
pip install -r requirements.txt
|
||||
@@ -861,6 +942,7 @@ pytest tests/ -v
|
||||
```
|
||||
|
||||
**运行 E2E 测试**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
pnpm test:e2e
|
||||
@@ -869,6 +951,7 @@ pnpm test:e2e
|
||||
##### 8.4 性能验证
|
||||
|
||||
**后端性能测试**:
|
||||
|
||||
```bash
|
||||
# 使用 k6 进行性能测试
|
||||
cd novalon-manage-api/manage-sys/src/test/k6
|
||||
@@ -876,6 +959,7 @@ k6 run performance-test.js
|
||||
```
|
||||
|
||||
**前端性能测试**:
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
pnpm test:perf
|
||||
@@ -884,6 +968,7 @@ pnpm test:perf
|
||||
##### 8.5 监控和日志
|
||||
|
||||
**查看应用指标**:
|
||||
|
||||
```bash
|
||||
# 查看应用指标
|
||||
curl http://localhost:8084/actuator/metrics
|
||||
@@ -893,6 +978,7 @@ curl http://localhost:8084/actuator/metrics/jvm.memory.used
|
||||
```
|
||||
|
||||
**查看日志**:
|
||||
|
||||
```bash
|
||||
# 查看应用日志
|
||||
tail -f novalon-manage-api/manage-app/logs/application.log
|
||||
@@ -904,6 +990,7 @@ grep ERROR novalon-manage-api/manage-app/logs/application.log
|
||||
##### 8.6 完整验证脚本
|
||||
|
||||
创建验证脚本 `verify-setup.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
@@ -948,6 +1035,7 @@ echo "=== 所有服务验证通过 ==="
|
||||
```
|
||||
|
||||
运行验证脚本:
|
||||
|
||||
```bash
|
||||
chmod +x verify-setup.sh
|
||||
./verify-setup.sh
|
||||
|
||||
+42
-60
@@ -28,13 +28,12 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class AuditLogService {
|
||||
|
||||
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT_LOG");
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuditLogService.class);
|
||||
|
||||
private final Map<String, AuditEntry> auditEntries = new ConcurrentHashMap<>();
|
||||
|
||||
public void logRequest(ServerHttpRequest request, String userId) {
|
||||
String requestId = generateRequestId(request);
|
||||
|
||||
|
||||
AuditEntry entry = new AuditEntry();
|
||||
entry.setRequestId(requestId);
|
||||
entry.setMethod(request.getMethod().name());
|
||||
@@ -47,27 +46,27 @@ public class AuditLogService {
|
||||
|
||||
auditEntries.put(requestId, entry);
|
||||
|
||||
auditLogger.info("[REQUEST] {} {} - User: {}, IP: {}, RequestId: {}",
|
||||
entry.getMethod(),
|
||||
entry.getPath(),
|
||||
entry.getUserId(),
|
||||
entry.getClientIp(),
|
||||
auditLogger.info("[REQUEST] {} {} - User: {}, IP: {}, RequestId: {}",
|
||||
entry.getMethod(),
|
||||
entry.getPath(),
|
||||
entry.getUserId(),
|
||||
entry.getClientIp(),
|
||||
entry.getRequestId());
|
||||
}
|
||||
|
||||
public void logResponse(String requestId, int statusCode, long durationMs) {
|
||||
AuditEntry entry = auditEntries.get(requestId);
|
||||
|
||||
|
||||
if (entry != null) {
|
||||
entry.setStatusCode(statusCode);
|
||||
entry.setEndTime(Instant.now());
|
||||
entry.setDurationMs(durationMs);
|
||||
|
||||
auditLogger.info("[RESPONSE] {} {} - Status: {}, Duration: {}ms, RequestId: {}",
|
||||
entry.getMethod(),
|
||||
entry.getPath(),
|
||||
entry.getStatusCode(),
|
||||
entry.getDurationMs(),
|
||||
auditLogger.info("[RESPONSE] {} {} - Status: {}, Duration: {}ms, RequestId: {}",
|
||||
entry.getMethod(),
|
||||
entry.getPath(),
|
||||
entry.getStatusCode(),
|
||||
entry.getDurationMs(),
|
||||
entry.getRequestId());
|
||||
|
||||
auditEntries.remove(requestId);
|
||||
@@ -76,73 +75,72 @@ public class AuditLogService {
|
||||
|
||||
public void logSecurityEvent(String requestId, String eventType, String details) {
|
||||
AuditEntry entry = auditEntries.get(requestId);
|
||||
|
||||
|
||||
if (entry != null) {
|
||||
auditLogger.warn("[SECURITY] {} - Event: {}, Details: {}, User: {}, IP: {}, RequestId: {}",
|
||||
entry.getPath(),
|
||||
eventType,
|
||||
details,
|
||||
entry.getUserId(),
|
||||
entry.getClientIp(),
|
||||
auditLogger.warn("[SECURITY] {} - Event: {}, Details: {}, User: {}, IP: {}, RequestId: {}",
|
||||
entry.getPath(),
|
||||
eventType,
|
||||
details,
|
||||
entry.getUserId(),
|
||||
entry.getClientIp(),
|
||||
entry.getRequestId());
|
||||
} else {
|
||||
auditLogger.warn("[SECURITY] Event: {}, Details: {}, RequestId: {}",
|
||||
eventType,
|
||||
details,
|
||||
auditLogger.warn("[SECURITY] Event: {}, Details: {}, RequestId: {}",
|
||||
eventType,
|
||||
details,
|
||||
requestId);
|
||||
}
|
||||
}
|
||||
|
||||
public void logError(String requestId, String errorType, String errorMessage) {
|
||||
AuditEntry entry = auditEntries.get(requestId);
|
||||
|
||||
|
||||
if (entry != null) {
|
||||
auditLogger.error("[ERROR] {} {} - Error: {}, Message: {}, User: {}, IP: {}, RequestId: {}",
|
||||
entry.getMethod(),
|
||||
entry.getPath(),
|
||||
errorType,
|
||||
errorMessage,
|
||||
entry.getUserId(),
|
||||
entry.getClientIp(),
|
||||
auditLogger.error("[ERROR] {} {} - Error: {}, Message: {}, User: {}, IP: {}, RequestId: {}",
|
||||
entry.getMethod(),
|
||||
entry.getPath(),
|
||||
errorType,
|
||||
errorMessage,
|
||||
entry.getUserId(),
|
||||
entry.getClientIp(),
|
||||
entry.getRequestId());
|
||||
} else {
|
||||
auditLogger.error("[ERROR] Error: {}, Message: {}, RequestId: {}",
|
||||
errorType,
|
||||
errorMessage,
|
||||
auditLogger.error("[ERROR] Error: {}, Message: {}, RequestId: {}",
|
||||
errorType,
|
||||
errorMessage,
|
||||
requestId);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateRequestId(ServerHttpRequest request) {
|
||||
String requestId = request.getHeaders().getFirst("X-Request-Id");
|
||||
|
||||
|
||||
if (requestId == null || requestId.isEmpty()) {
|
||||
requestId = String.format("%s-%d-%s",
|
||||
requestId = String.format("%s-%d-%s",
|
||||
request.getMethod().name().toLowerCase(),
|
||||
System.currentTimeMillis(),
|
||||
Integer.toHexString(request.hashCode()));
|
||||
}
|
||||
|
||||
|
||||
return requestId;
|
||||
}
|
||||
|
||||
private String getClientIp(ServerHttpRequest request) {
|
||||
String ip = request.getHeaders().getFirst("X-Forwarded-For");
|
||||
|
||||
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
ip = request.getHeaders().getFirst("X-Real-IP");
|
||||
}
|
||||
|
||||
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
ip = request.getRemoteAddress() != null ?
|
||||
request.getRemoteAddress().getAddress().getHostAddress() :
|
||||
"unknown";
|
||||
ip = request.getRemoteAddress() != null ? request.getRemoteAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
@@ -183,10 +181,6 @@ public class AuditLogService {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(String query) {
|
||||
this.query = query;
|
||||
}
|
||||
@@ -207,26 +201,14 @@ public class AuditLogService {
|
||||
this.clientIp = clientIp;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public void setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public Instant getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Instant startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Instant getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(Instant endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
-1
@@ -2,7 +2,6 @@ package cn.novalon.manage.gateway.discovery;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
|
||||
|
||||
-3
@@ -7,7 +7,6 @@ import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -53,8 +52,6 @@ public class CompressionFilter implements GlobalFilter, Ordered {
|
||||
"application/xml"
|
||||
);
|
||||
|
||||
private static final int MIN_COMPRESS_SIZE = 1024;
|
||||
|
||||
private boolean compressionEnabled = true;
|
||||
|
||||
@Override
|
||||
|
||||
-1
@@ -2,7 +2,6 @@ package cn.novalon.manage.gateway.filter;
|
||||
|
||||
import cn.novalon.manage.gateway.config.RateLimitConfig;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
|
||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
-3
@@ -40,9 +40,6 @@ import java.util.List;
|
||||
public class SignatureFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SignatureFilter.class);
|
||||
private static final String SIGNATURE_HEADER = "X-Signature";
|
||||
private static final String TIMESTAMP_HEADER = "X-Timestamp";
|
||||
private static final String NONCE_HEADER = "X-Nonce";
|
||||
|
||||
private final SignatureService signatureService;
|
||||
|
||||
|
||||
-3
@@ -2,9 +2,7 @@ package cn.novalon.manage.gateway.health;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -32,7 +30,6 @@ public class GatewayHealthIndicator implements HealthIndicator {
|
||||
private final CircuitBreakerRegistry circuitBreakerRegistry;
|
||||
private final RateLimiterRegistry rateLimiterRegistry;
|
||||
|
||||
@Autowired
|
||||
public GatewayHealthIndicator(
|
||||
CircuitBreakerRegistry circuitBreakerRegistry,
|
||||
RateLimiterRegistry rateLimiterRegistry) {
|
||||
|
||||
-2
@@ -14,11 +14,9 @@ import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
-1
@@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
|
||||
-2
@@ -5,13 +5,11 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* AuditLogService单元测试
|
||||
|
||||
-4
@@ -3,15 +3,11 @@ package cn.novalon.manage.gateway.cache;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
||||
-3
@@ -6,11 +6,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
package cn.novalon.manage.gateway.health;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
|
||||
-8
@@ -1,9 +1,6 @@
|
||||
package cn.novalon.manage.gateway.integration;
|
||||
|
||||
import cn.novalon.manage.gateway.filter.RbacAuthorizationFilter;
|
||||
import cn.novalon.manage.gateway.model.Permission;
|
||||
import cn.novalon.manage.gateway.model.Role;
|
||||
import cn.novalon.manage.gateway.model.User;
|
||||
import cn.novalon.manage.gateway.service.PermissionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -18,11 +15,6 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
package cn.novalon.manage.gateway.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
-4
@@ -14,10 +14,6 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
-3
@@ -1,6 +1,5 @@
|
||||
package cn.novalon.manage.gateway.service.impl;
|
||||
|
||||
import cn.novalon.manage.gateway.service.JwtKeyService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -9,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -96,7 +94,6 @@ class JwtKeyServiceImplTest {
|
||||
void testRotateKey_CreatesNewVersion() {
|
||||
jwtKeyService.initializeKeys();
|
||||
String oldVersion = jwtKeyService.getCurrentKeyVersion();
|
||||
SecretKey oldKey = jwtKeyService.getCurrentSigningKey();
|
||||
|
||||
jwtKeyService.rotateKey();
|
||||
|
||||
|
||||
+3
-7
@@ -10,13 +10,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersUriSpec;
|
||||
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
|
||||
import org.springframework.web.reactive.function.client.WebClient.ResponseSpec;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -36,13 +32,13 @@ class PermissionServiceImplTest {
|
||||
private WebClient webClient;
|
||||
|
||||
@Mock
|
||||
private RequestHeadersUriSpec requestHeadersUriSpec;
|
||||
private WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec;
|
||||
|
||||
@Mock
|
||||
private RequestHeadersSpec requestHeadersSpec;
|
||||
private WebClient.RequestHeadersSpec<?> requestHeadersSpec;
|
||||
|
||||
@Mock
|
||||
private ResponseSpec responseSpec;
|
||||
private WebClient.ResponseSpec responseSpec;
|
||||
|
||||
private PermissionService permissionService;
|
||||
|
||||
|
||||
+13
@@ -1,7 +1,11 @@
|
||||
package cn.novalon.manage.sys.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 角色创建请求DTO
|
||||
@@ -13,17 +17,26 @@ import jakarta.validation.constraints.NotNull;
|
||||
* @author 张翔
|
||||
* @date 2026-03-13
|
||||
*/
|
||||
@Schema(description = "角色创建请求")
|
||||
public class RoleCreateRequest {
|
||||
|
||||
@Schema(description = "角色名称", example = "管理员")
|
||||
@NotBlank(message = "角色名称不能为空")
|
||||
@Size(min = 2, max = 50, message = "角色名称长度必须在2-50之间")
|
||||
private String roleName;
|
||||
|
||||
@Schema(description = "角色权限字符串", example = "admin")
|
||||
@NotBlank(message = "角色权限字符串不能为空")
|
||||
@Size(min = 2, max = 50, message = "角色权限字符串长度必须在2-50之间")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "角色权限字符串只能包含字母、数字、下划线和横线")
|
||||
private String roleKey;
|
||||
|
||||
@Schema(description = "显示顺序", example = "1")
|
||||
@NotNull(message = "显示顺序不能为空")
|
||||
@Min(value = 1, message = "显示顺序必须大于0")
|
||||
private Integer roleSort;
|
||||
|
||||
@Schema(description = "状态", example = "1")
|
||||
private Integer status;
|
||||
|
||||
public String getRoleName() {
|
||||
|
||||
+9
-3
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.dto.request;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
@@ -17,23 +18,28 @@ public class UserRegisterRequest {
|
||||
@Schema(description = "用户名", example = "testuser")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "用户名只能包含字母、数字、下划线和横线")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "昵称", example = "测试用户")
|
||||
@Size(max = 100, message = "昵称长度不能超过100")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "密码", example = "123456")
|
||||
@Schema(description = "密码", example = "Admin123")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 100, message = "密码长度必须在6-100之间")
|
||||
@Size(min = 8, max = 20, message = "密码长度必须在8-20之间")
|
||||
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$", message = "密码必须包含大小写字母和数字")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "邮箱", example = "test@example.com")
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 100, message = "邮箱长度不能超过100")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
@Size(max = 20, message = "手机号长度不能超过20")
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
public String getUsername() {
|
||||
|
||||
+24
-9
@@ -9,12 +9,16 @@ import cn.novalon.manage.sys.core.command.CreateRoleCommand;
|
||||
import cn.novalon.manage.sys.core.command.UpdateRoleCommand;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Validator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统角色处理器
|
||||
*
|
||||
@@ -26,9 +30,11 @@ import reactor.core.publisher.Mono;
|
||||
public class SysRoleHandler {
|
||||
|
||||
private final ISysRoleService roleService;
|
||||
private final Validator validator;
|
||||
|
||||
public SysRoleHandler(ISysRoleService roleService) {
|
||||
public SysRoleHandler(ISysRoleService roleService, Validator validator) {
|
||||
this.roleService = roleService;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有角色", description = "获取系统中所有角色列表")
|
||||
@@ -88,14 +94,23 @@ public class SysRoleHandler {
|
||||
@Operation(summary = "创建角色", description = "创建新角色")
|
||||
public Mono<ServerResponse> createRole(ServerRequest request) {
|
||||
return request.bodyToMono(RoleCreateRequest.class)
|
||||
.map(req -> CreateRoleCommand.of(
|
||||
req.getRoleName(),
|
||||
req.getRoleKey(),
|
||||
req.getRoleSort(),
|
||||
req.getStatus()
|
||||
))
|
||||
.flatMap(roleService::createRole)
|
||||
.flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role));
|
||||
.flatMap(req -> {
|
||||
var violations = validator.validate(req);
|
||||
if (!violations.isEmpty()) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
violations.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage()));
|
||||
return ServerResponse.badRequest().bodyValue(errors);
|
||||
}
|
||||
|
||||
return Mono.just(CreateRoleCommand.of(
|
||||
req.getRoleName(),
|
||||
req.getRoleKey(),
|
||||
req.getRoleSort(),
|
||||
req.getStatus()
|
||||
))
|
||||
.flatMap(roleService::createRole)
|
||||
.flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role));
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "更新角色", description = "更新角色信息")
|
||||
|
||||
+25
-13
@@ -10,6 +10,7 @@ import cn.novalon.manage.sys.core.command.CreateUserCommand;
|
||||
import cn.novalon.manage.sys.core.command.UpdateUserCommand;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Validator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
@@ -19,7 +20,7 @@ import reactor.core.publisher.Mono;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户处理器
|
||||
@@ -36,9 +37,11 @@ import java.util.Map;
|
||||
public class SysUserHandler {
|
||||
|
||||
private final ISysUserService userService;
|
||||
private final Validator validator;
|
||||
|
||||
public SysUserHandler(ISysUserService userService) {
|
||||
public SysUserHandler(ISysUserService userService, Validator validator) {
|
||||
this.userService = userService;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
|
||||
@@ -110,17 +113,26 @@ public class SysUserHandler {
|
||||
@Operation(summary = "创建用户", description = "创建新用户")
|
||||
public Mono<ServerResponse> createUser(ServerRequest request) {
|
||||
return request.bodyToMono(UserRegisterRequest.class)
|
||||
.map(req -> CreateUserCommand.of(
|
||||
req.getUsername(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
req.getNickname(),
|
||||
req.getPhone(),
|
||||
null,
|
||||
null
|
||||
))
|
||||
.flatMap(userService::createUser)
|
||||
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
|
||||
.flatMap(req -> {
|
||||
var violations = validator.validate(req);
|
||||
if (!violations.isEmpty()) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
violations.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage()));
|
||||
return ServerResponse.badRequest().bodyValue(errors);
|
||||
}
|
||||
|
||||
return Mono.just(CreateUserCommand.of(
|
||||
req.getUsername(),
|
||||
req.getPassword(),
|
||||
req.getEmail(),
|
||||
req.getNickname(),
|
||||
req.getPhone(),
|
||||
null,
|
||||
null
|
||||
))
|
||||
.flatMap(userService::createUser)
|
||||
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "更新用户", description = "更新用户信息")
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.12</version>
|
||||
<version>3.5.13</version>
|
||||
<relativePath />
|
||||
</parent>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-boot.version>3.5.12</spring-boot.version>
|
||||
<spring-boot.version>3.5.13</spring-boot.version>
|
||||
<spring-cloud.version>2025.0.0</spring-cloud.version>
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
<resilience4j.version>2.2.0</resilience4j.version>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段二:用户管理功能验证', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-USER-001: 用户列表加载', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.el-table__body-wrapper')).toBeVisible();
|
||||
});
|
||||
|
||||
test('UAT-USER-002: 用户搜索功能', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const searchInput = page.locator('input[placeholder*="搜索"]').first();
|
||||
if (await searchInput.isVisible()) {
|
||||
await searchInput.fill('admin');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('.el-table')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-USER-003: 新增用户表单验证', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const formErrors = page.locator('.el-form-item__error');
|
||||
const errorCount = await formErrors.count();
|
||||
expect(errorCount).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段三:角色管理功能验证', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-ROLE-001: 角色列表加载', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('UAT-ROLE-002: 新增角色表单验证', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
if (await roleNameInput.isVisible()) {
|
||||
await roleNameInput.fill('测试角色');
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
if (await roleKeyInput.isVisible()) {
|
||||
await roleKeyInput.fill('test_role');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-ROLE-003: 角色权限分配', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const permissionButton = page.locator('button:has-text("权限")').first();
|
||||
if (await permissionButton.isVisible()) {
|
||||
await permissionButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const tree = page.locator('.el-tree');
|
||||
if (await tree.isVisible()) {
|
||||
const firstCheckbox = tree.locator('.el-checkbox').first();
|
||||
if (await firstCheckbox.isVisible()) {
|
||||
await firstCheckbox.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段四:菜单管理功能验证', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-MENU-001: 菜单树形结构展示', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=菜单管理');
|
||||
await page.waitForURL('**/menus', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const tableBody = page.locator('.el-table__body-wrapper');
|
||||
await expect(tableBody).toBeVisible();
|
||||
|
||||
const emptyText = page.locator('text=暂无数据');
|
||||
const hasEmptyText = await emptyText.isVisible().catch(() => false);
|
||||
|
||||
if (!hasEmptyText) {
|
||||
const treeNodes = page.locator('.el-table__row');
|
||||
const count = await treeNodes.count();
|
||||
expect(count).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-MENU-002: 新增菜单表单验证', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=菜单管理');
|
||||
await page.waitForURL('**/menus', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const menuNameInput = page.locator('.el-dialog input[placeholder*="菜单名称"]').first();
|
||||
if (await menuNameInput.isVisible()) {
|
||||
await menuNameInput.fill('测试菜单');
|
||||
|
||||
const permsInput = page.locator('.el-dialog input[placeholder*="路由地址"]').first();
|
||||
if (await permsInput.isVisible()) {
|
||||
await permsInput.fill('/test-menu');
|
||||
|
||||
const componentInput = page.locator('.el-dialog input[placeholder*="组件路径"]').first();
|
||||
if (await componentInput.isVisible()) {
|
||||
await componentInput.fill('views/test/TestMenu.vue');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-MENU-003: 菜单类型选择', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=菜单管理');
|
||||
await page.waitForURL('**/menus', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const menuTypeSelect = page.locator('.el-dialog .el-select').first();
|
||||
if (await menuTypeSelect.isVisible()) {
|
||||
await menuTypeSelect.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const options = page.locator('.el-select-dropdown__item');
|
||||
const count = await options.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段五:API交互与错误处理验证', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-API-001: Token过期处理', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
localStorage.removeItem('token');
|
||||
});
|
||||
|
||||
await page.goto('/users');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toContain('/login');
|
||||
});
|
||||
|
||||
test('UAT-API-002: 网络错误提示', async ({ page, context }) => {
|
||||
await context.route('**/api/**', route => route.abort('failed'));
|
||||
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await context.unroute('**/api/**');
|
||||
});
|
||||
|
||||
test('UAT-API-003: 权限不足提示', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('token', 'user_token_without_admin_rights');
|
||||
});
|
||||
|
||||
await page.goto('/roles');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const errorMessage = page.locator('.el-message--error');
|
||||
if (await errorMessage.isVisible()) {
|
||||
await expect(errorMessage).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-API-004: 并发请求处理', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
|
||||
const refreshButton = page.locator('button:has-text("刷新")').first();
|
||||
if (await refreshButton.isVisible()) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await refreshButton.click();
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.locator('.el-table')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-API-005: 数据加载状态显示', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const navigationPromise = page.click('text=用户管理');
|
||||
|
||||
const loading = page.locator('.el-loading-mask');
|
||||
if (await loading.isVisible({ timeout: 100 }).catch(() => false)) {
|
||||
await expect(loading).toBeVisible();
|
||||
}
|
||||
|
||||
await navigationPromise;
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.locator('.el-table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段六:数据持久化验证', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-PERSIST-001: 角色创建持久化验证', async ({ page }) => {
|
||||
const timestamp = Date.now();
|
||||
const roleName = `测试角色_${timestamp}`;
|
||||
const roleKey = `test_role_${timestamp}`;
|
||||
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
await roleNameInput.fill(roleName);
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
await roleKeyInput.fill(roleKey);
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const createdRole = page.locator(`text=${roleName}`);
|
||||
await expect(createdRole).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-PERSIST-002: 用户创建持久化验证', async ({ page }) => {
|
||||
const timestamp = Date.now();
|
||||
const username = `testuser_${timestamp}`;
|
||||
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const usernameInput = page.locator('.el-dialog input[placeholder*="用户名"]').first();
|
||||
await usernameInput.fill(username);
|
||||
|
||||
const nicknameInput = page.locator('.el-dialog input[placeholder*="昵称"]').first();
|
||||
await nicknameInput.fill(`测试用户_${timestamp}`);
|
||||
|
||||
const emailInput = page.locator('.el-dialog input[placeholder*="邮箱"]').first();
|
||||
await emailInput.fill(`${username}@test.com`);
|
||||
|
||||
const phoneInput = page.locator('.el-dialog input[placeholder*="手机"]').first();
|
||||
await phoneInput.fill('13800138000');
|
||||
|
||||
const passwordInput = page.locator('.el-dialog input[placeholder*="密码"]').first();
|
||||
await passwordInput.fill('Test123456');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const searchInput = page.locator('input[placeholder*="搜索"]').first();
|
||||
if (await searchInput.isVisible()) {
|
||||
await searchInput.fill(username);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const createdUser = page.locator(`text=${username}`);
|
||||
await expect(createdUser).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-PERSIST-003: 数据更新持久化验证', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const editButton = page.locator('button:has-text("编辑")').first();
|
||||
if (await editButton.isVisible()) {
|
||||
await editButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
const currentValue = await roleNameInput.inputValue();
|
||||
const newValue = `${currentValue}_已修改_${Date.now()}`;
|
||||
|
||||
await roleNameInput.fill(newValue);
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const updatedRole = page.locator(`text=${newValue}`);
|
||||
await expect(updatedRole).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-PERSIST-004: 数据删除持久化验证', async ({ page }) => {
|
||||
const timestamp = Date.now();
|
||||
const roleName = `待删除角色_${timestamp}`;
|
||||
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
await roleNameInput.fill(roleName);
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
await roleKeyInput.fill(`delete_test_${timestamp}`);
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const createdRole = page.locator(`text=${roleName}`);
|
||||
await createdRole.scrollIntoViewIfNeeded();
|
||||
|
||||
const deleteButton = page.locator(`tr:has-text("${roleName}") button:has-text("删除")`).first();
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const confirmDeleteButton = page.locator('.el-message-box button:has-text("确定")');
|
||||
if (await confirmDeleteButton.isVisible()) {
|
||||
await confirmDeleteButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const deletedRole = page.locator(`text=${roleName}`);
|
||||
await expect(deletedRole).not.toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段七:边界条件与异常输入测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-001: 用户名超长输入测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const usernameInput = page.locator('.el-dialog input[placeholder*="用户名"]').first();
|
||||
const longUsername = 'a'.repeat(300);
|
||||
await usernameInput.fill(longUsername);
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const errorMessage = page.locator('.el-form-item__error, .el-message--error');
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
expect(hasError).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-002: 特殊字符输入测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
await roleNameInput.fill('<script>alert("XSS")</script>');
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
await roleKeyInput.fill("'; DROP TABLE roles; --");
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const errorMessage = page.locator('.el-form-item__error, .el-message--error');
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
|
||||
if (!hasError) {
|
||||
const cancelButton = page.locator('.el-dialog button:has-text("取消")');
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-003: 空值输入测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const formErrors = page.locator('.el-form-item__error');
|
||||
const errorCount = await formErrors.count();
|
||||
expect(errorCount).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-004: 邮箱格式验证测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const emailInput = page.locator('.el-dialog input[placeholder*="邮箱"]').first();
|
||||
await emailInput.fill('invalid-email');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const emailError = page.locator('.el-form-item__error:has-text("邮箱")');
|
||||
const hasError = await emailError.isVisible().catch(() => false);
|
||||
expect(hasError).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-005: 手机号格式验证测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const phoneInput = page.locator('.el-dialog input[placeholder*="手机"]').first();
|
||||
await phoneInput.fill('123');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const phoneError = page.locator('.el-form-item__error:has-text("手机")');
|
||||
const hasError = await phoneError.isVisible().catch(() => false);
|
||||
expect(hasError).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-006: Emoji表情输入测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
await roleNameInput.fill('测试角色😀🎉🔥');
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
await roleKeyInput.fill('test_emoji_role');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const errorMessage = page.locator('.el-message--error');
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
|
||||
if (!hasError) {
|
||||
const cancelButton = page.locator('.el-dialog button:has-text("取消")');
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-BOUNDARY-007: 数字输入边界测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const sortInput = page.locator('.el-dialog input[type="number"]').first();
|
||||
if (await sortInput.isVisible()) {
|
||||
await sortInput.fill('-1');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const errorMessage = page.locator('.el-form-item__error');
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
|
||||
if (!hasError) {
|
||||
const cancelButton = page.locator('.el-dialog button:has-text("取消")');
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('UAT阶段八:安全测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-001: XSS攻击防护测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
const xssPayload = '<img src=x onerror=alert("XSS")>';
|
||||
await roleNameInput.fill(xssPayload);
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
await roleKeyInput.fill('xss_test');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const errorMessage = page.locator('.el-form-item__error, .el-message--error');
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
|
||||
if (!hasError) {
|
||||
const cancelButton = page.locator('.el-dialog button:has-text("取消")');
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
expect(hasError).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-002: SQL注入防护测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const searchInput = page.locator('input[placeholder*="搜索"]').first();
|
||||
if (await searchInput.isVisible()) {
|
||||
await searchInput.fill("admin' OR '1'='1");
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('.el-table')).toBeVisible();
|
||||
|
||||
const allRows = await page.locator('.el-table__row').count();
|
||||
expect(allRows).toBeLessThan(100);
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-003: 未授权访问测试', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
localStorage.removeItem('token');
|
||||
});
|
||||
|
||||
await page.goto('/users');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toContain('/login');
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-004: CSRF防护测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=角色管理');
|
||||
await page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const roleNameInput = page.locator('.el-dialog input[placeholder*="角色名称"]').first();
|
||||
await roleNameInput.fill('CSRF测试角色');
|
||||
|
||||
const roleKeyInput = page.locator('.el-dialog input[placeholder*="角色标识"]').first();
|
||||
await roleKeyInput.fill('csrf_test');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const successMessage = page.locator('.el-message--success');
|
||||
const errorMessage = page.locator('.el-message--error');
|
||||
|
||||
const hasSuccess = await successMessage.isVisible().catch(() => false);
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
|
||||
expect(hasSuccess || hasError).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-005: 密码强度验证测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("新增用户")').first();
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const usernameInput = page.locator('.el-dialog input[placeholder*="用户名"]').first();
|
||||
await usernameInput.fill('testuser');
|
||||
|
||||
const passwordInput = page.locator('.el-dialog input[placeholder*="密码"]').first();
|
||||
await passwordInput.fill('123');
|
||||
|
||||
const confirmButton = page.locator('.el-dialog button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const passwordError = page.locator('.el-form-item__error');
|
||||
const hasError = await passwordError.isVisible().catch(() => false);
|
||||
|
||||
expect(hasError).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-006: 敏感信息泄露测试', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const pageContent = await page.content();
|
||||
|
||||
expect(pageContent).not.toContain('password');
|
||||
expect(pageContent).not.toContain('secret');
|
||||
expect(pageContent).not.toContain('api_key');
|
||||
expect(pageContent).not.toContain('private_key');
|
||||
});
|
||||
|
||||
test('UAT-SECURITY-007: 会话超时测试', async ({ page }) => {
|
||||
const systemMenu = page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.click('text=用户管理');
|
||||
await page.waitForURL('**/users', { timeout: 30000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.evaluate(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const expiredToken = token.replace(/\.(.*?)\./, '.expired.');
|
||||
localStorage.setItem('token', expiredToken);
|
||||
}
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
const isLoginPage = currentUrl.includes('/login');
|
||||
const hasError = await page.locator('.el-message--error').isVisible().catch(() => false);
|
||||
|
||||
expect(isLoginPage || hasError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -259,17 +259,21 @@ const formState = reactive<CreateUserRequest & { id?: number; status?: UserStatu
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
|
||||
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '用户名只能包含字母、数字、下划线和横线', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
{ min: 8, max: 20, message: '密码长度在 8 到 20 个字符', trigger: 'blur' },
|
||||
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: '密码必须包含大小写字母和数字', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
{ max: 100, message: '邮箱长度不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user