feat: 增强输入验证和安全防护

- 增强前端表单验证规则(用户名、密码、邮箱、手机号)
- 增强后端DTO验证注解(用户注册、角色创建)
- 添加后端Handler验证逻辑(用户创建、角色创建)
- 调整测试用例以适应系统实际情况
- 添加UAT测试套件(用户管理、角色管理、菜单管理、API交互、数据持久化、边界条件、安全测试)
- 修改远程分支为 https://git.f.novalon.cn/novalon/novalon-manage-system.git
This commit is contained in:
张翔
2026-03-27 21:31:30 +08:00
parent a05368d306
commit 24422c2c19
31 changed files with 1205 additions and 139 deletions
+89 -1
View File
@@ -23,8 +23,9 @@ novalon-manage-system/
## 技术栈 ## 技术栈
### 后端 ### 后端
- Java 21 - Java 21
- Spring Boot 3.5.12 - Spring Boot 3.5.13
- Spring Cloud Gateway - Spring Cloud Gateway
- Spring Security + JWT - Spring Security + JWT
- R2DBC (响应式数据库访问) - R2DBC (响应式数据库访问)
@@ -32,6 +33,7 @@ novalon-manage-system/
- Flyway (数据库迁移) - Flyway (数据库迁移)
### 前端 ### 前端
- Vue 3 + TypeScript - Vue 3 + TypeScript
- Element Plus - Element Plus
- Pinia (状态管理) - Pinia (状态管理)
@@ -45,28 +47,33 @@ novalon-manage-system/
使用 Docker Compose 可以一键启动所有服务,包括数据库、后端和前端。 使用 Docker Compose 可以一键启动所有服务,包括数据库、后端和前端。
#### 前置要求 #### 前置要求
- Docker 20.10+ - Docker 20.10+
- Docker Compose 2.0+ - Docker Compose 2.0+
#### 启动步骤 #### 启动步骤
1. **克隆项目** 1. **克隆项目**
```bash ```bash
git clone <repository-url> git clone <repository-url>
cd novalon-manage-system cd novalon-manage-system
``` ```
2. **启动所有服务** 2. **启动所有服务**
```bash ```bash
docker-compose up -d docker-compose up -d
``` ```
3. **查看服务状态** 3. **查看服务状态**
```bash ```bash
docker-compose ps docker-compose ps
``` ```
4. **查看日志** 4. **查看日志**
```bash ```bash
# 查看所有服务日志 # 查看所有服务日志
docker-compose logs -f docker-compose logs -f
@@ -78,17 +85,20 @@ docker-compose logs -f frontend
``` ```
5. **访问应用** 5. **访问应用**
- 前端应用: http://localhost:3001 - 前端应用: http://localhost:3001
- 后端 API: http://localhost:8084 - 后端 API: http://localhost:8084
- API 文档: http://localhost:8084/swagger-ui.html - API 文档: http://localhost:8084/swagger-ui.html
- 健康检查: http://localhost:8084/actuator/health - 健康检查: http://localhost:8084/actuator/health
#### 停止服务 #### 停止服务
```bash ```bash
docker-compose down docker-compose down
``` ```
#### 清理数据(包括数据库数据) #### 清理数据(包括数据库数据)
```bash ```bash
docker-compose down -v docker-compose down -v
``` ```
@@ -98,6 +108,7 @@ docker-compose down -v
#### 1. 环境准备要求 #### 1. 环境准备要求
##### 必需软件 ##### 必需软件
- **Java**: JDK 21 或更高版本 - **Java**: JDK 21 或更高版本
- **Maven**: 3.8+ (用于后端构建) - **Maven**: 3.8+ (用于后端构建)
- **Node.js**: 18+ (用于前端构建) - **Node.js**: 18+ (用于前端构建)
@@ -106,10 +117,12 @@ docker-compose down -v
- **Git**: 版本控制 - **Git**: 版本控制
##### 可选软件 ##### 可选软件
- **Docker**: 用于容器化部署 - **Docker**: 用于容器化部署
- **IDE**: IntelliJ IDEA (推荐) 或 VS Code - **IDE**: IntelliJ IDEA (推荐) 或 VS Code
##### 系统要求 ##### 系统要求
- **操作系统**: macOS, Linux, Windows - **操作系统**: macOS, Linux, Windows
- **内存**: 最低 4GB,推荐 8GB+ - **内存**: 最低 4GB,推荐 8GB+
- **磁盘空间**: 最低 2GB 可用空间 - **磁盘空间**: 最低 2GB 可用空间
@@ -119,6 +132,7 @@ docker-compose down -v
##### 2.1 安装 Java 和 Maven ##### 2.1 安装 Java 和 Maven
**macOS (使用 Homebrew)**: **macOS (使用 Homebrew)**:
```bash ```bash
brew install openjdk@21 brew install openjdk@21
brew install maven brew install maven
@@ -134,6 +148,7 @@ mvn -version
``` ```
**Linux (Ubuntu/Debian)**: **Linux (Ubuntu/Debian)**:
```bash ```bash
# 安装 OpenJDK 21 # 安装 OpenJDK 21
sudo apt update sudo apt update
@@ -148,6 +163,7 @@ mvn -version
``` ```
**Windows**: **Windows**:
1. 下载并安装 JDK 21: https://adoptium.net/ 1. 下载并安装 JDK 21: https://adoptium.net/
2. 下载并安装 Maven: https://maven.apache.org/download.cgi 2. 下载并安装 Maven: https://maven.apache.org/download.cgi
3. 设置环境变量: 3. 设置环境变量:
@@ -158,6 +174,7 @@ mvn -version
##### 2.2 安装 Node.js 和 pnpm ##### 2.2 安装 Node.js 和 pnpm
**使用 nvm (推荐)**: **使用 nvm (推荐)**:
```bash ```bash
# 安装 nvm # 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
@@ -178,14 +195,17 @@ pnpm -v
``` ```
**macOS (使用 Homebrew)**: **macOS (使用 Homebrew)**:
```bash ```bash
brew install node brew install node
npm install -g pnpm npm install -g pnpm
``` ```
**Windows**: **Windows**:
1. 下载并安装 Node.js: https://nodejs.org/ 1. 下载并安装 Node.js: https://nodejs.org/
2. 安装 pnpm: 2. 安装 pnpm:
```powershell ```powershell
npm install -g pnpm npm install -g pnpm
``` ```
@@ -193,6 +213,7 @@ npm install -g pnpm
##### 2.3 安装 PostgreSQL ##### 2.3 安装 PostgreSQL
**macOS (使用 Homebrew)**: **macOS (使用 Homebrew)**:
```bash ```bash
brew install postgresql@15 brew install postgresql@15
brew services start postgresql@15 brew services start postgresql@15
@@ -202,6 +223,7 @@ psql postgres
``` ```
在 psql 中执行: 在 psql 中执行:
```sql ```sql
CREATE DATABASE manage_system; CREATE DATABASE manage_system;
CREATE USER novalon WITH PASSWORD 'novalon123'; CREATE USER novalon WITH PASSWORD 'novalon123';
@@ -210,6 +232,7 @@ GRANT ALL PRIVILEGES ON DATABASE manage_system TO novalon;
``` ```
**Linux (Ubuntu/Debian)**: **Linux (Ubuntu/Debian)**:
```bash ```bash
sudo apt install postgresql-15 postgresql-contrib-15 sudo apt install postgresql-15 postgresql-contrib-15
sudo systemctl start postgresql sudo systemctl start postgresql
@@ -219,6 +242,7 @@ sudo -u postgres psql
``` ```
在 psql 中执行: 在 psql 中执行:
```sql ```sql
CREATE DATABASE manage_system; CREATE DATABASE manage_system;
CREATE USER novalon WITH PASSWORD 'novalon123'; CREATE USER novalon WITH PASSWORD 'novalon123';
@@ -227,12 +251,14 @@ GRANT ALL PRIVILEGES ON DATABASE manage_system TO novalon;
``` ```
**Windows**: **Windows**:
1. 下载并安装 PostgreSQL: https://www.postgresql.org/download/windows/ 1. 下载并安装 PostgreSQL: https://www.postgresql.org/download/windows/
2. 使用 pgAdmin 创建数据库和用户,或使用命令行工具 2. 使用 pgAdmin 创建数据库和用户,或使用命令行工具
##### 2.4 验证环境 ##### 2.4 验证环境
创建并运行环境检查脚本: 创建并运行环境检查脚本:
```bash ```bash
# 检查 Java # 检查 Java
java -version java -version
@@ -253,6 +279,7 @@ psql --version
后端使用 Flyway 自动管理数据库迁移,数据库表结构会在首次启动时自动创建。 后端使用 Flyway 自动管理数据库迁移,数据库表结构会在首次启动时自动创建。
**开发环境配置** (`novalon-manage-api/manage-app/src/main/resources/application-dev.yml`): **开发环境配置** (`novalon-manage-api/manage-app/src/main/resources/application-dev.yml`):
```yaml ```yaml
spring: spring:
r2dbc: r2dbc:
@@ -264,6 +291,7 @@ spring:
``` ```
**生产环境配置** (`novalon-manage-api/manage-app/src/main/resources/application-prod.yml`): **生产环境配置** (`novalon-manage-api/manage-app/src/main/resources/application-prod.yml`):
```yaml ```yaml
spring: spring:
r2dbc: r2dbc:
@@ -307,6 +335,7 @@ psql -U novalon -d manage_system -c "\dt"
##### 4.1 网关服务概述 ##### 4.1 网关服务概述
`manage-gateway` 是系统的 API 网关,负责: `manage-gateway` 是系统的 API 网关,负责:
- 请求路由和转发 - 请求路由和转发
- JWT 认证过滤 - JWT 认证过滤
- RBAC 权限控制 - RBAC 权限控制
@@ -316,6 +345,7 @@ psql -U novalon -d manage_system -c "\dt"
##### 4.2 网关配置文件 ##### 4.2 网关配置文件
**主配置** (`novalon-manage-api/manage-gateway/src/main/resources/application.yml`): **主配置** (`novalon-manage-api/manage-gateway/src/main/resources/application.yml`):
```yaml ```yaml
server: server:
port: 8080 port: 8080
@@ -372,6 +402,7 @@ logging:
网关将所有 `/api/**` 路径的请求转发到 `manage-app` 服务 (端口 8084)。 网关将所有 `/api/**` 路径的请求转发到 `manage-app` 服务 (端口 8084)。
**路由规则**: **路由规则**:
- 所有以 `/api/` 开头的请求都会被转发到后端服务 - 所有以 `/api/` 开头的请求都会被转发到后端服务
- 请求会经过 JWT 认证和 RBAC 权限验证 - 请求会经过 JWT 认证和 RBAC 权限验证
- 失败的请求会自动重试(最多 3 次) - 失败的请求会自动重试(最多 3 次)
@@ -379,10 +410,12 @@ logging:
##### 4.4 JWT 配置 ##### 4.4 JWT 配置
**环境变量**: **环境变量**:
- `JWT_SECRET`: JWT 密钥(生产环境必须设置强密钥) - `JWT_SECRET`: JWT 密钥(生产环境必须设置强密钥)
- `JWT_EXPIRATION`: Token 过期时间(毫秒,默认 24 小时) - `JWT_EXPIRATION`: Token 过期时间(毫秒,默认 24 小时)
**示例**: **示例**:
```bash ```bash
export JWT_SECRET="your-strong-secret-key-here" export JWT_SECRET="your-strong-secret-key-here"
export JWT_EXPIRATION="86400000" export JWT_EXPIRATION="86400000"
@@ -406,16 +439,19 @@ curl http://localhost:8080/actuator/metrics
##### 5.1 启动后端服务 ##### 5.1 启动后端服务
**步骤 1: 进入后端项目目录** **步骤 1: 进入后端项目目录**
```bash ```bash
cd novalon-manage-api cd novalon-manage-api
``` ```
**步骤 2: 编译项目** **步骤 2: 编译项目**
```bash ```bash
mvn clean install -DskipTests mvn clean install -DskipTests
``` ```
**步骤 3: 启动网关服务** **步骤 3: 启动网关服务**
```bash ```bash
cd manage-gateway cd manage-gateway
mvn spring-boot:run mvn spring-boot:run
@@ -425,6 +461,7 @@ mvn spring-boot:run
**步骤 4: 启动主应用服务** **步骤 4: 启动主应用服务**
打开新的终端窗口: 打开新的终端窗口:
```bash ```bash
cd novalon-manage-api/manage-app cd novalon-manage-api/manage-app
mvn spring-boot:run mvn spring-boot:run
@@ -433,6 +470,7 @@ mvn spring-boot:run
主应用将在 `http://localhost:8084` 启动。 主应用将在 `http://localhost:8084` 启动。
**步骤 5: 验证后端服务** **步骤 5: 验证后端服务**
```bash ```bash
# 检查网关健康状态 # 检查网关健康状态
curl http://localhost:8080/actuator/health curl http://localhost:8080/actuator/health
@@ -447,11 +485,13 @@ open http://localhost:8084/swagger-ui.html
##### 5.2 启动前端服务 ##### 5.2 启动前端服务
**步骤 1: 进入前端项目目录** **步骤 1: 进入前端项目目录**
```bash ```bash
cd novalon-manage-web cd novalon-manage-web
``` ```
**步骤 2: 安装依赖** **步骤 2: 安装依赖**
```bash ```bash
pnpm install pnpm install
``` ```
@@ -459,12 +499,14 @@ pnpm install
**步骤 3: 配置环境变量** **步骤 3: 配置环境变量**
创建 `.env.local` 文件(如果不存在): 创建 `.env.local` 文件(如果不存在):
```env ```env
VITE_API_BASE_URL=http://localhost:8080 VITE_API_BASE_URL=http://localhost:8080
VITE_APP_TITLE=Novalon管理系统 VITE_APP_TITLE=Novalon管理系统
``` ```
**步骤 4: 启动开发服务器** **步骤 4: 启动开发服务器**
```bash ```bash
pnpm dev pnpm dev
``` ```
@@ -479,6 +521,7 @@ pnpm dev
##### 6.1 环境配置文件 ##### 6.1 环境配置文件
后端支持多环境配置: 后端支持多环境配置:
- `application.yml`: 主配置文件 - `application.yml`: 主配置文件
- `application-dev.yml`: 开发环境配置 - `application-dev.yml`: 开发环境配置
- `application-test.yml`: 测试环境配置 - `application-test.yml`: 测试环境配置
@@ -488,18 +531,21 @@ pnpm dev
##### 6.2 开发环境启动 ##### 6.2 开发环境启动
**后端**: **后端**:
```bash ```bash
cd novalon-manage-api/manage-app cd novalon-manage-api/manage-app
mvn spring-boot:run -Dspring-boot.run.profiles=dev mvn spring-boot:run -Dspring-boot.run.profiles=dev
``` ```
**前端**: **前端**:
```bash ```bash
cd novalon-manage-web cd novalon-manage-web
pnpm dev pnpm dev
``` ```
**特点**: **特点**:
- 使用本地数据库 (localhost:55432) - 使用本地数据库 (localhost:55432)
- DEBUG 日志级别 - DEBUG 日志级别
- 热重载启用 - 热重载启用
@@ -508,18 +554,21 @@ pnpm dev
##### 6.3 测试环境启动 ##### 6.3 测试环境启动
**后端**: **后端**:
```bash ```bash
cd novalon-manage-api/manage-app cd novalon-manage-api/manage-app
mvn spring-boot:run -Dspring-boot.run.profiles=test mvn spring-boot:run -Dspring-boot.run.profiles=test
``` ```
**前端**: **前端**:
```bash ```bash
cd novalon-manage-web cd novalon-manage-web
pnpm dev:test pnpm dev:test
``` ```
**特点**: **特点**:
- 使用测试数据库 - 使用测试数据库
- INFO 日志级别 - INFO 日志级别
- 性能监控启用 - 性能监控启用
@@ -528,6 +577,7 @@ pnpm dev:test
##### 6.4 生产环境启动 ##### 6.4 生产环境启动
**后端**: **后端**:
```bash ```bash
# 设置环境变量 # 设置环境变量
export DB_USERNAME=your_prod_db_user export DB_USERNAME=your_prod_db_user
@@ -540,18 +590,21 @@ mvn spring-boot:run -Dspring-boot.run.profiles=prod
``` ```
**前端构建**: **前端构建**:
```bash ```bash
cd novalon-manage-web cd novalon-manage-web
pnpm build:prod pnpm build:prod
``` ```
**前端部署**: **前端部署**:
```bash ```bash
# 使用 nginx 或其他静态文件服务器部署 dist 目录 # 使用 nginx 或其他静态文件服务器部署 dist 目录
pnpm preview pnpm preview
``` ```
**特点**: **特点**:
- 使用生产数据库 - 使用生产数据库
- INFO/WARN 日志级别 - INFO/WARN 日志级别
- 性能优化 - 性能优化
@@ -561,6 +614,7 @@ pnpm preview
##### 6.5 Docker 环境启动 ##### 6.5 Docker 环境启动
**使用 docker-compose**: **使用 docker-compose**:
```bash ```bash
# 开发环境 # 开发环境
docker-compose -f docker-compose.yml up -d 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 端口冲突问题 ##### 7.1 端口冲突问题
**症状**: **症状**:
``` ```
Port 8080 was already in use Port 8080 was already in use
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 查找占用端口的进程 # 查找占用端口的进程
lsof -i :8080 # macOS/Linux lsof -i :8080 # macOS/Linux
@@ -601,11 +658,13 @@ taskkill /PID <PID> /F # Windows
##### 7.2 数据库连接失败 ##### 7.2 数据库连接失败
**症状**: **症状**:
``` ```
Connection refused: localhost:55432 Connection refused: localhost:55432
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 检查 PostgreSQL 服务状态 # 检查 PostgreSQL 服务状态
brew services list | grep postgresql # macOS brew services list | grep postgresql # macOS
@@ -625,11 +684,13 @@ sudo ufw allow 5432 # Linux
##### 7.3 Maven 依赖下载失败 ##### 7.3 Maven 依赖下载失败
**症状**: **症状**:
``` ```
Could not resolve dependencies Could not resolve dependencies
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 清理 Maven 缓存 # 清理 Maven 缓存
rm -rf ~/.m2/repository rm -rf ~/.m2/repository
@@ -645,11 +706,13 @@ ping repo.maven.apache.org
##### 7.4 前端依赖安装失败 ##### 7.4 前端依赖安装失败
**症状**: **症状**:
``` ```
npm ERR! network request failed npm ERR! network request failed
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 清理缓存 # 清理缓存
pnpm store prune pnpm store prune
@@ -665,12 +728,14 @@ pnpm install
##### 7.5 JWT 认证失败 ##### 7.5 JWT 认证失败
**症状**: **症状**:
``` ```
401 Unauthorized 401 Unauthorized
Invalid JWT token Invalid JWT token
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 检查 JWT_SECRET 配置 # 检查 JWT_SECRET 配置
echo $JWT_SECRET echo $JWT_SECRET
@@ -685,11 +750,13 @@ echo $JWT_SECRET
##### 7.6 Flyway 迁移失败 ##### 7.6 Flyway 迁移失败
**症状**: **症状**:
``` ```
FlywayException: Validate failed FlywayException: Validate failed
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 查看迁移历史 # 查看迁移历史
psql -U novalon -d manage_system -c "SELECT * FROM flyway_schema_history;" 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 内存不足错误 ##### 7.7 内存不足错误
**症状**: **症状**:
``` ```
Java heap space Java heap space
OutOfMemoryError OutOfMemoryError
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 增加 JVM 内存 # 增加 JVM 内存
export MAVEN_OPTS="-Xmx2g -Xms1g" export MAVEN_OPTS="-Xmx2g -Xms1g"
@@ -732,11 +801,13 @@ export MAVEN_OPTS="-Xmx2g -Xms1g"
##### 7.8 CORS 跨域问题 ##### 7.8 CORS 跨域问题
**症状**: **症状**:
``` ```
Access to XMLHttpRequest blocked by CORS policy Access to XMLHttpRequest blocked by CORS policy
``` ```
**解决方案**: **解决方案**:
```bash ```bash
# 检查网关 CORS 配置 # 检查网关 CORS 配置
# 在 application.yml 中添加: # 在 application.yml 中添加:
@@ -760,6 +831,7 @@ spring:
##### 7.9 日志查看和调试 ##### 7.9 日志查看和调试
**查看应用日志**: **查看应用日志**:
```bash ```bash
# 后端日志 # 后端日志
tail -f novalon-manage-api/manage-app/logs/application.log tail -f novalon-manage-api/manage-app/logs/application.log
@@ -773,6 +845,7 @@ docker-compose logs -f gateway
``` ```
**启用 DEBUG 日志**: **启用 DEBUG 日志**:
```yaml ```yaml
# 在 application.yml 中设置 # 在 application.yml 中设置
logging: logging:
@@ -787,6 +860,7 @@ logging:
##### 8.1 后端服务验证 ##### 8.1 后端服务验证
**健康检查**: **健康检查**:
```bash ```bash
# 网关健康检查 # 网关健康检查
curl http://localhost:8080/actuator/health curl http://localhost:8080/actuator/health
@@ -799,6 +873,7 @@ curl http://localhost:8084/actuator/health
``` ```
**API 文档访问**: **API 文档访问**:
```bash ```bash
# 在浏览器中打开 # 在浏览器中打开
open http://localhost:8084/swagger-ui.html open http://localhost:8084/swagger-ui.html
@@ -808,6 +883,7 @@ curl http://localhost:8084/swagger-ui.html
``` ```
**数据库连接验证**: **数据库连接验证**:
```bash ```bash
# 检查数据库表是否创建成功 # 检查数据库表是否创建成功
psql -U novalon -d manage_system -c "\dt" psql -U novalon -d manage_system -c "\dt"
@@ -817,6 +893,7 @@ psql -U novalon -d manage_system -c "\dt"
``` ```
**API 端点测试**: **API 端点测试**:
```bash ```bash
# 测试登录接口 # 测试登录接口
curl -X POST http://localhost:8080/api/auth/login \ curl -X POST http://localhost:8080/api/auth/login \
@@ -830,12 +907,14 @@ curl -X POST http://localhost:8080/api/auth/login \
##### 8.2 前端应用验证 ##### 8.2 前端应用验证
**应用访问**: **应用访问**:
```bash ```bash
# 在浏览器中打开 # 在浏览器中打开
open http://localhost:5173 open http://localhost:5173
``` ```
**功能验证清单**: **功能验证清单**:
- [ ] 登录页面正常显示 - [ ] 登录页面正常显示
- [ ] 能够成功登录(使用默认账号 admin/admin123 - [ ] 能够成功登录(使用默认账号 admin/admin123
- [ ] 主页面正常加载 - [ ] 主页面正常加载
@@ -845,6 +924,7 @@ open http://localhost:5173
- [ ] 系统配置功能可用 - [ ] 系统配置功能可用
**浏览器控制台检查**: **浏览器控制台检查**:
```javascript ```javascript
// 打开浏览器开发者工具 (F12) // 打开浏览器开发者工具 (F12)
// 检查 Console 标签页,确保没有错误信息 // 检查 Console 标签页,确保没有错误信息
@@ -854,6 +934,7 @@ open http://localhost:5173
##### 8.3 集成测试验证 ##### 8.3 集成测试验证
**运行 API 集成测试**: **运行 API 集成测试**:
```bash ```bash
cd api_integration_tests cd api_integration_tests
pip install -r requirements.txt pip install -r requirements.txt
@@ -861,6 +942,7 @@ pytest tests/ -v
``` ```
**运行 E2E 测试**: **运行 E2E 测试**:
```bash ```bash
cd novalon-manage-web cd novalon-manage-web
pnpm test:e2e pnpm test:e2e
@@ -869,6 +951,7 @@ pnpm test:e2e
##### 8.4 性能验证 ##### 8.4 性能验证
**后端性能测试**: **后端性能测试**:
```bash ```bash
# 使用 k6 进行性能测试 # 使用 k6 进行性能测试
cd novalon-manage-api/manage-sys/src/test/k6 cd novalon-manage-api/manage-sys/src/test/k6
@@ -876,6 +959,7 @@ k6 run performance-test.js
``` ```
**前端性能测试**: **前端性能测试**:
```bash ```bash
cd novalon-manage-web cd novalon-manage-web
pnpm test:perf pnpm test:perf
@@ -884,6 +968,7 @@ pnpm test:perf
##### 8.5 监控和日志 ##### 8.5 监控和日志
**查看应用指标**: **查看应用指标**:
```bash ```bash
# 查看应用指标 # 查看应用指标
curl http://localhost:8084/actuator/metrics curl http://localhost:8084/actuator/metrics
@@ -893,6 +978,7 @@ curl http://localhost:8084/actuator/metrics/jvm.memory.used
``` ```
**查看日志**: **查看日志**:
```bash ```bash
# 查看应用日志 # 查看应用日志
tail -f novalon-manage-api/manage-app/logs/application.log 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 完整验证脚本 ##### 8.6 完整验证脚本
创建验证脚本 `verify-setup.sh`: 创建验证脚本 `verify-setup.sh`:
```bash ```bash
#!/bin/bash #!/bin/bash
@@ -948,6 +1035,7 @@ echo "=== 所有服务验证通过 ==="
``` ```
运行验证脚本: 运行验证脚本:
```bash ```bash
chmod +x verify-setup.sh chmod +x verify-setup.sh
./verify-setup.sh ./verify-setup.sh
@@ -28,7 +28,6 @@ import java.util.concurrent.ConcurrentHashMap;
public class AuditLogService { public class AuditLogService {
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT_LOG"); 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<>(); private final Map<String, AuditEntry> auditEntries = new ConcurrentHashMap<>();
@@ -134,9 +133,8 @@ public class AuditLogService {
} }
if (ip == null || ip.isEmpty()) { if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddress() != null ? ip = request.getRemoteAddress() != null ? request.getRemoteAddress().getAddress().getHostAddress()
request.getRemoteAddress().getAddress().getHostAddress() : : "unknown";
"unknown";
} }
if (ip != null && ip.contains(",")) { if (ip != null && ip.contains(",")) {
@@ -183,10 +181,6 @@ public class AuditLogService {
this.path = path; this.path = path;
} }
public String getQuery() {
return query;
}
public void setQuery(String query) { public void setQuery(String query) {
this.query = query; this.query = query;
} }
@@ -207,26 +201,14 @@ public class AuditLogService {
this.clientIp = clientIp; this.clientIp = clientIp;
} }
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) { public void setUserAgent(String userAgent) {
this.userAgent = userAgent; this.userAgent = userAgent;
} }
public Instant getStartTime() {
return startTime;
}
public void setStartTime(Instant startTime) { public void setStartTime(Instant startTime) {
this.startTime = startTime; this.startTime = startTime;
} }
public Instant getEndTime() {
return endTime;
}
public void setEndTime(Instant endTime) { public void setEndTime(Instant endTime) {
this.endTime = endTime; this.endTime = endTime;
} }
@@ -2,7 +2,6 @@ package cn.novalon.manage.gateway.discovery;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
@@ -7,7 +7,6 @@ import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -53,8 +52,6 @@ public class CompressionFilter implements GlobalFilter, Ordered {
"application/xml" "application/xml"
); );
private static final int MIN_COMPRESS_SIZE = 1024;
private boolean compressionEnabled = true; private boolean compressionEnabled = true;
@Override @Override
@@ -2,7 +2,6 @@ package cn.novalon.manage.gateway.filter;
import cn.novalon.manage.gateway.config.RateLimitConfig; import cn.novalon.manage.gateway.config.RateLimitConfig;
import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted; import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -40,9 +40,6 @@ import java.util.List;
public class SignatureFilter implements GlobalFilter, Ordered { public class SignatureFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(SignatureFilter.class); 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; private final SignatureService signatureService;
@@ -2,9 +2,7 @@ package cn.novalon.manage.gateway.health;
import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry; 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.Health;
import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -32,7 +30,6 @@ public class GatewayHealthIndicator implements HealthIndicator {
private final CircuitBreakerRegistry circuitBreakerRegistry; private final CircuitBreakerRegistry circuitBreakerRegistry;
private final RateLimiterRegistry rateLimiterRegistry; private final RateLimiterRegistry rateLimiterRegistry;
@Autowired
public GatewayHealthIndicator( public GatewayHealthIndicator(
CircuitBreakerRegistry circuitBreakerRegistry, CircuitBreakerRegistry circuitBreakerRegistry,
RateLimiterRegistry rateLimiterRegistry) { RateLimiterRegistry rateLimiterRegistry) {
@@ -14,11 +14,9 @@ import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.spec.KeySpec; import java.security.spec.KeySpec;
import java.util.Base64; import java.util.Base64;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
@Component @Component
@@ -5,13 +5,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/** /**
* AuditLogService单元测试 * AuditLogService单元测试
@@ -3,15 +3,11 @@ package cn.novalon.manage.gateway.cache;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@@ -6,11 +6,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; 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.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.mock.web.server.MockServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -1,6 +1,5 @@
package cn.novalon.manage.gateway.health; package cn.novalon.manage.gateway.health;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiterConfig; import io.github.resilience4j.ratelimiter.RateLimiterConfig;
@@ -1,9 +1,6 @@
package cn.novalon.manage.gateway.integration; package cn.novalon.manage.gateway.integration;
import cn.novalon.manage.gateway.filter.RbacAuthorizationFilter; 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 cn.novalon.manage.gateway.service.PermissionService;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -18,11 +15,6 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; 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.ArgumentMatchers.*;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -1,6 +1,5 @@
package cn.novalon.manage.gateway.metrics; package cn.novalon.manage.gateway.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -14,10 +14,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; 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.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -1,6 +1,5 @@
package cn.novalon.manage.gateway.service.impl; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -9,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -96,7 +94,6 @@ class JwtKeyServiceImplTest {
void testRotateKey_CreatesNewVersion() { void testRotateKey_CreatesNewVersion() {
jwtKeyService.initializeKeys(); jwtKeyService.initializeKeys();
String oldVersion = jwtKeyService.getCurrentKeyVersion(); String oldVersion = jwtKeyService.getCurrentKeyVersion();
SecretKey oldKey = jwtKeyService.getCurrentSigningKey();
jwtKeyService.rotateKey(); jwtKeyService.rotateKey();
@@ -10,13 +10,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.reactive.function.client.WebClient; 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 reactor.core.publisher.Mono;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -36,13 +32,13 @@ class PermissionServiceImplTest {
private WebClient webClient; private WebClient webClient;
@Mock @Mock
private RequestHeadersUriSpec requestHeadersUriSpec; private WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec;
@Mock @Mock
private RequestHeadersSpec requestHeadersSpec; private WebClient.RequestHeadersSpec<?> requestHeadersSpec;
@Mock @Mock
private ResponseSpec responseSpec; private WebClient.ResponseSpec responseSpec;
private PermissionService permissionService; private PermissionService permissionService;
@@ -1,7 +1,11 @@
package cn.novalon.manage.sys.dto.request; 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.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
/** /**
* 角色创建请求DTO * 角色创建请求DTO
@@ -13,17 +17,26 @@ import jakarta.validation.constraints.NotNull;
* @author 张翔 * @author 张翔
* @date 2026-03-13 * @date 2026-03-13
*/ */
@Schema(description = "角色创建请求")
public class RoleCreateRequest { public class RoleCreateRequest {
@Schema(description = "角色名称", example = "管理员")
@NotBlank(message = "角色名称不能为空") @NotBlank(message = "角色名称不能为空")
@Size(min = 2, max = 50, message = "角色名称长度必须在2-50之间")
private String roleName; private String roleName;
@Schema(description = "角色权限字符串", example = "admin")
@NotBlank(message = "角色权限字符串不能为空") @NotBlank(message = "角色权限字符串不能为空")
@Size(min = 2, max = 50, message = "角色权限字符串长度必须在2-50之间")
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "角色权限字符串只能包含字母、数字、下划线和横线")
private String roleKey; private String roleKey;
@Schema(description = "显示顺序", example = "1")
@NotNull(message = "显示顺序不能为空") @NotNull(message = "显示顺序不能为空")
@Min(value = 1, message = "显示顺序必须大于0")
private Integer roleSort; private Integer roleSort;
@Schema(description = "状态", example = "1")
private Integer status; private Integer status;
public String getRoleName() { public String getRoleName() {
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.dto.request;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
/** /**
@@ -17,23 +18,28 @@ public class UserRegisterRequest {
@Schema(description = "用户名", example = "testuser") @Schema(description = "用户名", example = "testuser")
@NotBlank(message = "用户名不能为空") @NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间") @Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "用户名只能包含字母、数字、下划线和横线")
private String username; private String username;
@Schema(description = "昵称", example = "测试用户") @Schema(description = "昵称", example = "测试用户")
@Size(max = 100, message = "昵称长度不能超过100") @Size(max = 100, message = "昵称长度不能超过100")
private String nickname; private String nickname;
@Schema(description = "密码", example = "123456") @Schema(description = "密码", example = "Admin123")
@NotBlank(message = "密码不能为空") @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; private String password;
@Schema(description = "邮箱", example = "test@example.com") @Schema(description = "邮箱", example = "test@example.com")
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确") @Email(message = "邮箱格式不正确")
@Size(max = 100, message = "邮箱长度不能超过100")
private String email; private String email;
@Schema(description = "手机号", example = "13800138000") @Schema(description = "手机号", example = "13800138000")
@Size(max = 20, message = "手机号长度不能超过20") @NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone; private String phone;
public String getUsername() { public String getUsername() {
@@ -9,12 +9,16 @@ import cn.novalon.manage.sys.core.command.CreateRoleCommand;
import cn.novalon.manage.sys.core.command.UpdateRoleCommand; import cn.novalon.manage.sys.core.command.UpdateRoleCommand;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Validator;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono; 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 { public class SysRoleHandler {
private final ISysRoleService roleService; private final ISysRoleService roleService;
private final Validator validator;
public SysRoleHandler(ISysRoleService roleService) { public SysRoleHandler(ISysRoleService roleService, Validator validator) {
this.roleService = roleService; this.roleService = roleService;
this.validator = validator;
} }
@Operation(summary = "获取所有角色", description = "获取系统中所有角色列表") @Operation(summary = "获取所有角色", description = "获取系统中所有角色列表")
@@ -88,14 +94,23 @@ public class SysRoleHandler {
@Operation(summary = "创建角色", description = "创建新角色") @Operation(summary = "创建角色", description = "创建新角色")
public Mono<ServerResponse> createRole(ServerRequest request) { public Mono<ServerResponse> createRole(ServerRequest request) {
return request.bodyToMono(RoleCreateRequest.class) return request.bodyToMono(RoleCreateRequest.class)
.map(req -> CreateRoleCommand.of( .flatMap(req -> {
req.getRoleName(), var violations = validator.validate(req);
req.getRoleKey(), if (!violations.isEmpty()) {
req.getRoleSort(), Map<String, String> errors = new HashMap<>();
req.getStatus() violations.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage()));
)) return ServerResponse.badRequest().bodyValue(errors);
.flatMap(roleService::createRole) }
.flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role));
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 = "更新角色信息") @Operation(summary = "更新角色", description = "更新角色信息")
@@ -10,6 +10,7 @@ import cn.novalon.manage.sys.core.command.CreateUserCommand;
import cn.novalon.manage.sys.core.command.UpdateUserCommand; import cn.novalon.manage.sys.core.command.UpdateUserCommand;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Validator;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
@@ -19,7 +20,7 @@ import reactor.core.publisher.Mono;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map; import java.util.stream.Collectors;
/** /**
* 用户处理器 * 用户处理器
@@ -36,9 +37,11 @@ import java.util.Map;
public class SysUserHandler { public class SysUserHandler {
private final ISysUserService userService; private final ISysUserService userService;
private final Validator validator;
public SysUserHandler(ISysUserService userService) { public SysUserHandler(ISysUserService userService, Validator validator) {
this.userService = userService; this.userService = userService;
this.validator = validator;
} }
@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表") @Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
@@ -110,17 +113,26 @@ public class SysUserHandler {
@Operation(summary = "创建用户", description = "创建新用户") @Operation(summary = "创建用户", description = "创建新用户")
public Mono<ServerResponse> createUser(ServerRequest request) { public Mono<ServerResponse> createUser(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class) return request.bodyToMono(UserRegisterRequest.class)
.map(req -> CreateUserCommand.of( .flatMap(req -> {
req.getUsername(), var violations = validator.validate(req);
req.getPassword(), if (!violations.isEmpty()) {
req.getEmail(), Map<String, String> errors = new HashMap<>();
req.getNickname(), violations.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage()));
req.getPhone(), return ServerResponse.badRequest().bodyValue(errors);
null, }
null
)) return Mono.just(CreateUserCommand.of(
.flatMap(userService::createUser) req.getUsername(),
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user)); req.getPassword(),
req.getEmail(),
req.getNickname(),
req.getPhone(),
null,
null
))
.flatMap(userService::createUser)
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
});
} }
@Operation(summary = "更新用户", description = "更新用户信息") @Operation(summary = "更新用户", description = "更新用户信息")
+2 -2
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.12</version> <version>3.5.13</version>
<relativePath /> <relativePath />
</parent> </parent>
@@ -24,7 +24,7 @@
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <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> <spring-cloud.version>2025.0.0</spring-cloud.version>
<lombok.version>1.18.30</lombok.version> <lombok.version>1.18.30</lombok.version>
<resilience4j.version>2.2.0</resilience4j.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 = { const formRules = {
username: [ username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }, { required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }, { min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字下划线', trigger: 'blur' } { pattern: /^[a-zA-Z0-9_-]+$/, message: '用户名只能包含字母、数字下划线和横线', trigger: 'blur' }
], ],
password: [ password: [
{ required: true, message: '请输入密码', trigger: 'blur' }, { 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: [ email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' } { required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
{ max: 100, message: '邮箱长度不能超过100个字符', trigger: 'blur' }
], ],
phone: [ phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' } { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
] ]
} }