From 24422c2c19088d65788a63aa2d70c504e5bbccda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 27 Mar 2026 21:31:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=92=8C=E5=AE=89=E5=85=A8=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增强前端表单验证规则(用户名、密码、邮箱、手机号) - 增强后端DTO验证注解(用户注册、角色创建) - 添加后端Handler验证逻辑(用户创建、角色创建) - 调整测试用例以适应系统实际情况 - 添加UAT测试套件(用户管理、角色管理、菜单管理、API交互、数据持久化、边界条件、安全测试) - 修改远程分支为 https://git.f.novalon.cn/novalon/novalon-manage-system.git --- README.md | 90 ++++++- .../manage/gateway/audit/AuditLogService.java | 102 ++++---- .../discovery/ServiceDiscoveryService.java | 1 - .../gateway/filter/CompressionFilter.java | 3 - .../gateway/filter/RateLimitFilter.java | 1 - .../gateway/filter/SignatureFilter.java | 3 - .../health/GatewayHealthIndicator.java | 3 - .../service/impl/JwtKeyServiceImpl.java | 2 - .../novalon/manage/gateway/util/JwtUtil.java | 1 - .../gateway/audit/AuditLogServiceTest.java | 2 - .../cache/RequestCacheServiceTest.java | 4 - .../gateway/filter/CompressionFilterTest.java | 3 - .../health/GatewayHealthIndicatorTest.java | 1 - .../integration/RbacIntegrationTest.java | 8 - .../gateway/metrics/GatewayMetricsTest.java | 1 - .../route/DynamicRouteServiceTest.java | 4 - .../service/impl/JwtKeyServiceImplTest.java | 3 - .../impl/PermissionServiceImplTest.java | 10 +- .../sys/dto/request/RoleCreateRequest.java | 13 + .../sys/dto/request/UserRegisterRequest.java | 12 +- .../sys/handler/role/SysRoleHandler.java | 33 ++- .../sys/handler/user/SysUserHandler.java | 38 ++- novalon-manage-api/pom.xml | 4 +- .../e2e/uat-phase2-user.spec.ts | 78 ++++++ .../e2e/uat-phase3-role.spec.ts | 91 +++++++ .../e2e/uat-phase4-menu.spec.ts | 110 +++++++++ novalon-manage-web/e2e/uat-phase5-api.spec.ts | 97 ++++++++ .../e2e/uat-phase6-persistence.spec.ts | 191 +++++++++++++++ .../e2e/uat-phase7-boundary.spec.ts | 228 ++++++++++++++++++ .../e2e/uat-phase8-security.spec.ts | 195 +++++++++++++++ .../src/views/system/UserManagement.vue | 12 +- 31 files changed, 1205 insertions(+), 139 deletions(-) create mode 100644 novalon-manage-web/e2e/uat-phase2-user.spec.ts create mode 100644 novalon-manage-web/e2e/uat-phase3-role.spec.ts create mode 100644 novalon-manage-web/e2e/uat-phase4-menu.spec.ts create mode 100644 novalon-manage-web/e2e/uat-phase5-api.spec.ts create mode 100644 novalon-manage-web/e2e/uat-phase6-persistence.spec.ts create mode 100644 novalon-manage-web/e2e/uat-phase7-boundary.spec.ts create mode 100644 novalon-manage-web/e2e/uat-phase8-security.spec.ts diff --git a/README.md b/README.md index 62ede24..aaeed64 100644 --- a/README.md +++ b/README.md @@ -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 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 /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 diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/audit/AuditLogService.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/audit/AuditLogService.java index 833c15e..b1b8098 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/audit/AuditLogService.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/audit/AuditLogService.java @@ -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 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; } diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/discovery/ServiceDiscoveryService.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/discovery/ServiceDiscoveryService.java index f267d0e..9c4ce79 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/discovery/ServiceDiscoveryService.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/discovery/ServiceDiscoveryService.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/CompressionFilter.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/CompressionFilter.java index db794ae..39667ba 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/CompressionFilter.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/CompressionFilter.java @@ -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 diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/RateLimitFilter.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/RateLimitFilter.java index 696fb2e..5167278 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/RateLimitFilter.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/RateLimitFilter.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/SignatureFilter.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/SignatureFilter.java index 8750c40..258b06a 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/SignatureFilter.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/filter/SignatureFilter.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/health/GatewayHealthIndicator.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/health/GatewayHealthIndicator.java index 4398d9a..0059a6b 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/health/GatewayHealthIndicator.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/health/GatewayHealthIndicator.java @@ -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) { diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImpl.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImpl.java index 90315db..7adec7f 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImpl.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImpl.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/util/JwtUtil.java b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/util/JwtUtil.java index 267ad6b..da9a18e 100644 --- a/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/util/JwtUtil.java +++ b/novalon-manage-api/manage-gateway/src/main/java/cn/novalon/manage/gateway/util/JwtUtil.java @@ -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 diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/audit/AuditLogServiceTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/audit/AuditLogServiceTest.java index 92ac3b2..dcf03a9 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/audit/AuditLogServiceTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/audit/AuditLogServiceTest.java @@ -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单元测试 diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/cache/RequestCacheServiceTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/cache/RequestCacheServiceTest.java index abf0c14..a89475a 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/cache/RequestCacheServiceTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/cache/RequestCacheServiceTest.java @@ -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) diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/filter/CompressionFilterTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/filter/CompressionFilterTest.java index 76e67f8..189dff9 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/filter/CompressionFilterTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/filter/CompressionFilterTest.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/health/GatewayHealthIndicatorTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/health/GatewayHealthIndicatorTest.java index 3dd8f25..72fdc5d 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/health/GatewayHealthIndicatorTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/health/GatewayHealthIndicatorTest.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/integration/RbacIntegrationTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/integration/RbacIntegrationTest.java index b35ecf3..cb8e8cb 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/integration/RbacIntegrationTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/integration/RbacIntegrationTest.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/metrics/GatewayMetricsTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/metrics/GatewayMetricsTest.java index bb2ca1a..649446d 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/metrics/GatewayMetricsTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/metrics/GatewayMetricsTest.java @@ -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; diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/route/DynamicRouteServiceTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/route/DynamicRouteServiceTest.java index 8a7fa14..2f02962 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/route/DynamicRouteServiceTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/route/DynamicRouteServiceTest.java @@ -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.*; diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImplTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImplTest.java index faa010f..44f24a8 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImplTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/JwtKeyServiceImplTest.java @@ -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(); diff --git a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/PermissionServiceImplTest.java b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/PermissionServiceImplTest.java index 999fc6d..db9d11d 100644 --- a/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/PermissionServiceImplTest.java +++ b/novalon-manage-api/manage-gateway/src/test/java/cn/novalon/manage/gateway/service/impl/PermissionServiceImplTest.java @@ -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; diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/RoleCreateRequest.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/RoleCreateRequest.java index 14b201d..553965e 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/RoleCreateRequest.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/RoleCreateRequest.java @@ -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() { diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/UserRegisterRequest.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/UserRegisterRequest.java index f62664f..adb0953 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/UserRegisterRequest.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/dto/request/UserRegisterRequest.java @@ -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() { diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/role/SysRoleHandler.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/role/SysRoleHandler.java index 62f8156..5a30908 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/role/SysRoleHandler.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/role/SysRoleHandler.java @@ -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 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 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 = "更新角色信息") diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java index 9b2ce5c..1eb1732 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java @@ -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 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 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 = "更新用户信息") diff --git a/novalon-manage-api/pom.xml b/novalon-manage-api/pom.xml index cde91db..c8ebda1 100644 --- a/novalon-manage-api/pom.xml +++ b/novalon-manage-api/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.12 + 3.5.13 @@ -24,7 +24,7 @@ 21 21 UTF-8 - 3.5.12 + 3.5.13 2025.0.0 1.18.30 2.2.0 diff --git a/novalon-manage-web/e2e/uat-phase2-user.spec.ts b/novalon-manage-web/e2e/uat-phase2-user.spec.ts new file mode 100644 index 0000000..4a308e9 --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase2-user.spec.ts @@ -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); + } + } + }); +}); diff --git a/novalon-manage-web/e2e/uat-phase3-role.spec.ts b/novalon-manage-web/e2e/uat-phase3-role.spec.ts new file mode 100644 index 0000000..b5667d7 --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase3-role.spec.ts @@ -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(); + } + } + } + }); +}); diff --git a/novalon-manage-web/e2e/uat-phase4-menu.spec.ts b/novalon-manage-web/e2e/uat-phase4-menu.spec.ts new file mode 100644 index 0000000..7a0d8fd --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase4-menu.spec.ts @@ -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); + } + } + }); +}); diff --git a/novalon-manage-web/e2e/uat-phase5-api.spec.ts b/novalon-manage-web/e2e/uat-phase5-api.spec.ts new file mode 100644 index 0000000..6c5e25b --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase5-api.spec.ts @@ -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(); + }); +}); diff --git a/novalon-manage-web/e2e/uat-phase6-persistence.spec.ts b/novalon-manage-web/e2e/uat-phase6-persistence.spec.ts new file mode 100644 index 0000000..5b41781 --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase6-persistence.spec.ts @@ -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 }); + } + } + } + }); +}); diff --git a/novalon-manage-web/e2e/uat-phase7-boundary.spec.ts b/novalon-manage-web/e2e/uat-phase7-boundary.spec.ts new file mode 100644 index 0000000..093d751 --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase7-boundary.spec.ts @@ -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(''); + + 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(); + } + } + } + }); +}); diff --git a/novalon-manage-web/e2e/uat-phase8-security.spec.ts b/novalon-manage-web/e2e/uat-phase8-security.spec.ts new file mode 100644 index 0000000..cc4daed --- /dev/null +++ b/novalon-manage-web/e2e/uat-phase8-security.spec.ts @@ -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 = ''; + 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(); + }); +}); diff --git a/novalon-manage-web/src/views/system/UserManagement.vue b/novalon-manage-web/src/views/system/UserManagement.vue index 37e60d2..403045e 100644 --- a/novalon-manage-web/src/views/system/UserManagement.vue +++ b/novalon-manage-web/src/views/system/UserManagement.vue @@ -259,17 +259,21 @@ const formState = reactive