From 6392c085601b0e55259e1432e087bd4adae7d823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Thu, 2 Apr 2026 08:07:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(api/web):=20=E5=AE=9E=E7=8E=B0API=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=AD=BE=E5=90=8D=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E6=B5=8B=E8=AF=95=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(db): 重构查询条件类到query目录下 test: 添加登录流程测试脚本和测试数据 chore: 添加crypto-js依赖用于签名验证 ci: 配置测试环境数据库和端口设置 --- check_login_api.py | 58 ++ check_login_simple.py | 47 + debug_login.py | 74 ++ debug_login_detailed.py | 54 ++ .../src/test/resources/application-test.yml | 22 + .../manage-app/src/test/resources/data-h2.sql | 80 ++ .../src/test/resources/schema-h2.sql | 189 ++++ .../OperationLogQueryCriteria.java | 2 +- .../SysExceptionLogQueryCriteria.java | 2 +- .../{ => query}/SysLoginLogQueryCriteria.java | 2 +- .../{ => query}/SysMenuQueryCriteria.java | 2 +- .../{ => query}/SysRoleQueryCriteria.java | 2 +- .../SysUserMessageQueryCriteria.java | 2 +- .../{ => query}/SysUserQueryCriteria.java | 2 +- .../db/repository/OperationLogRepository.java | 2 +- .../repository/SysExceptionLogRepository.java | 2 +- .../db/repository/SysLoginLogRepository.java | 2 +- .../db/repository/SysMenuRepository.java | 2 +- .../db/repository/SysRoleRepository.java | 2 +- .../repository/SysUserMessageRepository.java | 2 +- .../db/repository/SysUserRepository.java | 2 +- .../gateway/filter/CompressionFilter.java | 2 +- .../src/main/resources/application.yml | 6 + .../manage/sys/audit/AuditLogAspect.java | 4 - .../manage/sys/audit/domain/AuditLog.java | 1 - .../manage/sys/config/AsyncConfig.java | 1 - .../manage/sys/config/AuditingConfig.java | 1 - .../impl/SysUserServiceIntegrationTest.java | 245 ++++++ .../core/service/impl/SysUserServiceTest.java | 824 ++++++------------ .../interceptor/OperationLogFilterTest.java | 210 ----- novalon-manage-web/e2e/login-test.spec.ts | 34 + novalon-manage-web/package.json | 2 + novalon-manage-web/playwright.config.ts | 4 +- novalon-manage-web/pnpm-lock.yaml | 16 + novalon-manage-web/src/utils/request.ts | 17 +- novalon-manage-web/src/utils/signature.ts | 96 ++ novalon-manage-web/vite.config.ts | 2 +- test-signature.js | 49 ++ test_comprehensive_workflow.py | 362 ++++++++ test_signature.py | 51 ++ 40 files changed, 1679 insertions(+), 800 deletions(-) create mode 100644 check_login_api.py create mode 100644 check_login_simple.py create mode 100644 debug_login.py create mode 100644 debug_login_detailed.py create mode 100644 novalon-manage-api/manage-app/src/test/resources/application-test.yml create mode 100644 novalon-manage-api/manage-app/src/test/resources/data-h2.sql create mode 100644 novalon-manage-api/manage-app/src/test/resources/schema-h2.sql rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/OperationLogQueryCriteria.java (97%) rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/SysExceptionLogQueryCriteria.java (97%) rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/SysLoginLogQueryCriteria.java (97%) rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/SysMenuQueryCriteria.java (97%) rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/SysRoleQueryCriteria.java (97%) rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/SysUserMessageQueryCriteria.java (96%) rename novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/{ => query}/SysUserQueryCriteria.java (98%) create mode 100644 novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceIntegrationTest.java delete mode 100644 novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/interceptor/OperationLogFilterTest.java create mode 100644 novalon-manage-web/e2e/login-test.spec.ts create mode 100644 novalon-manage-web/src/utils/signature.ts create mode 100644 test-signature.js create mode 100644 test_comprehensive_workflow.py create mode 100644 test_signature.py diff --git a/check_login_api.py b/check_login_api.py new file mode 100644 index 0000000..6b66dc1 --- /dev/null +++ b/check_login_api.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +检查登录API响应 +""" + +from playwright.sync_api import sync_playwright +import time + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # 监听网络请求 + responses = [] + + def handle_response(response): + if 'login' in response.url or 'auth' in response.url: + responses.append({ + 'url': response.url, + 'status': response.status, + 'body': response.text() if response.status != 204 else '' + }) + + page.on('response', handle_response) + + # 访问登录页面 + page.goto("http://localhost:3002/login") + page.wait_for_load_state("networkidle") + + print("尝试登录...") + + # 填写表单 + page.fill('input[placeholder="请输入用户名"]', 'admin') + page.fill('input[placeholder="请输入密码"]', 'Test@123') + + # 点击登录 + page.click('button:has-text("登录")') + + # 等待响应 + time.sleep(3) + + print(f"\n登录后URL: {page.url}") + + # 打印API响应 + print("\nAPI响应:") + for resp in responses: + print(f" URL: {resp['url']}") + print(f" Status: {resp['status']}") + if resp['body']: + try: + print(f" Body: {resp['body'][:500]}") + except: + pass + + # 截图 + page.screenshot(path="/tmp/login_debug.png") + + browser.close() diff --git a/check_login_simple.py b/check_login_simple.py new file mode 100644 index 0000000..d004000 --- /dev/null +++ b/check_login_simple.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +检查登录流程 +""" + +from playwright.sync_api import sync_playwright +import time + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # 监听控制台消息 + console_messages = [] + page.on('console', lambda msg: console_messages.append(f"{msg.type}: {msg.text}")) + + # 访问登录页面 + page.goto("http://localhost:3002/login") + page.wait_for_load_state("networkidle") + + print("尝试登录...") + + # 填写表单 + page.fill('input[placeholder="请输入用户名"]', 'admin') + page.fill('input[placeholder="请输入密码"]', 'Test@123') + + # 点击登录 + page.click('button:has-text("登录")') + + # 等待 + time.sleep(5) + + print(f"\n登录后URL: {page.url}") + + # 打印控制台消息 + print("\n控制台消息:") + for msg in console_messages[-10:]: # 只打印最后10条 + print(f" {msg}") + + # 检查localStorage + token = page.evaluate("localStorage.getItem('token')") + print(f"\nToken: {token}") + + # 截图 + page.screenshot(path="/tmp/login_result.png") + + browser.close() diff --git a/debug_login.py b/debug_login.py new file mode 100644 index 0000000..4aa4566 --- /dev/null +++ b/debug_login.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +检查登录页面结构 +""" + +from playwright.sync_api import sync_playwright +import time + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # 访问登录页面 + page.goto("http://localhost:3002/login") + page.wait_for_load_state("networkidle") + + # 截图 + page.screenshot(path="/tmp/login_page.png") + + # 获取页面内容 + print("页面URL:", page.url) + print("\n页面标题:", page.title()) + + # 查找所有输入框 + inputs = page.locator('input').all() + print(f"\n找到 {len(inputs)} 个输入框:") + for i, inp in enumerate(inputs): + try: + placeholder = inp.get_attribute('placeholder') + input_type = inp.get_attribute('type') + print(f" {i+1}. type={input_type}, placeholder={placeholder}") + except: + pass + + # 查找所有按钮 + buttons = page.locator('button').all() + print(f"\n找到 {len(buttons)} 个按钮:") + for i, btn in enumerate(buttons): + try: + text = btn.text_content() + print(f" {i+1}. {text}") + except: + pass + + # 尝试登录 + print("\n尝试登录...") + + # 填写用户名 + username_input = page.locator('input[type="text"], input:not([type])').first + username_input.fill("admin") + print("填写用户名: admin") + + # 填写密码 + password_input = page.locator('input[type="password"]').first + password_input.fill("Test@123") + print("填写密码: Test@123") + + # 点击登录按钮 + login_button = page.locator('button:has-text("登录")').first + login_button.click() + print("点击登录按钮") + + # 等待跳转 + time.sleep(5) + + print(f"\n登录后URL: {page.url}") + page.screenshot(path="/tmp/after_login.png") + + # 检查是否有错误消息 + error_msg = page.locator('.el-message--error, .error-message').first + if error_msg.is_visible(): + print(f"错误消息: {error_msg.text_content()}") + + browser.close() diff --git a/debug_login_detailed.py b/debug_login_detailed.py new file mode 100644 index 0000000..46d743b --- /dev/null +++ b/debug_login_detailed.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +详细检查登录流程 +""" + +from playwright.sync_api import sync_playwright +import time + +with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # 监听控制台消息 + console_messages = [] + page.on('console', lambda msg: console_messages.append(f"{msg.type}: {msg.text}")) + + # 访问登录页面 + page.goto("http://localhost:3002/login") + page.wait_for_load_state("networkidle") + + print("当前URL:", page.url) + print("\n尝试登录...") + + # 填写表单 + page.fill('input[placeholder="请输入用户名"]', 'admin') + page.fill('input[placeholder="请输入密码"]', 'admin123') + + # 点击登录 + page.click('button:has-text("登录")') + print("点击登录按钮") + + # 等待并检查URL变化 + for i in range(10): + time.sleep(1) + current_url = page.url + print(f" {i+1}秒后URL: {current_url}") + + if '/login' not in current_url: + print(f"\n✅ 登录成功!跳转到: {current_url}") + break + + # 检查localStorage + token = page.evaluate("localStorage.getItem('token')") + print(f"\nToken: {token[:50] if token else 'None'}...") + + # 打印控制台消息 + print("\n控制台消息:") + for msg in console_messages[-20:]: + print(f" {msg}") + + # 截图 + page.screenshot(path="/tmp/login_final.png") + + browser.close() diff --git a/novalon-manage-api/manage-app/src/test/resources/application-test.yml b/novalon-manage-api/manage-app/src/test/resources/application-test.yml new file mode 100644 index 0000000..1369a20 --- /dev/null +++ b/novalon-manage-api/manage-app/src/test/resources/application-test.yml @@ -0,0 +1,22 @@ +spring.r2dbc.url=r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.r2dbc.username=sa +spring.r2dbc.password= +spring.r2dbc.pool.enabled=true +spring.r2dbc.pool.initial-size=5 +spring.r2dbc.pool.max-size=20 + +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema-h2.sql +spring.sql.init.data-locations=classpath:data-h2.sql + +logging.level.org.springframework.r2dbc=DEBUG +logging.level.cn.novalon.manage=DEBUG + +spring.flyway.enabled=false + +server.port=8085 + +jwt.secret=test-secret-key-for-testing-purposes-only-minimum-256-bits +jwt.expiration=3600000 + +signature.enabled=false diff --git a/novalon-manage-api/manage-app/src/test/resources/data-h2.sql b/novalon-manage-api/manage-app/src/test/resources/data-h2.sql new file mode 100644 index 0000000..14173f2 --- /dev/null +++ b/novalon-manage-api/manage-app/src/test/resources/data-h2.sql @@ -0,0 +1,80 @@ +-- H2数据库测试数据 +-- 用于测试环境 + +-- 插入测试角色 +INSERT INTO roles (id, role_name, role_key, role_sort, status, created_by, updated_by) +VALUES +(1, '超级管理员', 'admin', 1, 1, 'system', 'system'), +(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'), +(3, '普通用户', 'normal_user', 3, 1, 'system', 'system'), +(4, '访客', 'guest', 4, 1, 'system', 'system'); + +-- 插入测试用户 +-- BCrypt哈希值对应明文密码: Test@123 +INSERT INTO users (id, username, password, email, phone, nickname, status, created_by, updated_by) +VALUES +(1, 'admin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'), +(2, 'testadmin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'), +(3, 'normaluser', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'), +(4, 'guestuser', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'), +(5, 'disableduser', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7.J6LlZy', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system'); + +-- 为用户分配角色 +INSERT INTO user_roles (user_id, role_id, created_by, updated_by) +VALUES +(1, 1, 'system', 'system'), +(2, 2, 'system', 'system'), +(3, 3, 'system', 'system'), +(4, 4, 'system', 'system'); + +-- 插入测试菜单 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, created_by, updated_by) +VALUES +(1, '系统管理', 0, 1, '/system', 'Layout', 'M', '1', '1', '', 'system', 'system', 'system'), +(2, '用户管理', 1, 1, 'user', 'system/user/index', 'C', '1', '1', 'system:user:list', 'user', 'system', 'system'), +(3, '角色管理', 1, 2, 'role', 'system/role/index', 'C', '1', '1', 'system:role:list', 'role', 'system', 'system'), +(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '1', '1', 'system:menu:list', 'menu', 'system', 'system'), +(5, '测试菜单', 0, 99, '/test', 'Layout', 'M', '1', '1', '', 'test', 'system', 'system'), +(6, '用户测试', 5, 1, 'user-test', 'system/user-test/index', 'C', '1', '1', 'system:user:test', 'user', 'system', 'system'); + +-- 插入测试权限 +INSERT INTO sys_permission (id, permission_name, permission_key, permission_type, parent_id, status, created_by, updated_by) +VALUES +(1, '系统管理', 'system:manage', 'menu', 0, 1, 'system', 'system'), +(2, '用户管理', 'system:user:manage', 'menu', 1, 1, 'system', 'system'), +(3, '用户查询', 'system:user:list', 'button', 2, 1, 'system', 'system'), +(4, '用户新增', 'system:user:add', 'button', 2, 1, 'system', 'system'), +(5, '用户编辑', 'system:user:edit', 'button', 2, 1, 'system', 'system'), +(6, '用户删除', 'system:user:delete', 'button', 2, 1, 'system', 'system'), +(7, '测试权限', 'test:permission', 'menu', 0, 1, 'system', 'system'), +(8, '用户测试权限', 'system:user:test', 'button', 7, 1, 'system', 'system'); + +-- 为角色分配权限 +INSERT INTO sys_role_permission (role_id, permission_id, created_by, updated_by) +SELECT 1, id, 'system', 'system' FROM sys_permission +UNION ALL +SELECT 2, id, 'system', 'system' FROM sys_permission WHERE id IN (7, 8); + +-- 插入字典类型 +INSERT INTO sys_dict_type (id, dict_name, dict_type, status, remark, created_by, updated_by) +VALUES +(1, '用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'), +(2, '菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'), +(3, '角色状态', 'role_status', '0', '角色状态列表', 'system', 'system'); + +-- 插入字典数据 +INSERT INTO sys_dict_data (id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, created_by, updated_by) +VALUES +(1, 1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'), +(2, 2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'), +(3, 1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'), +(4, 2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'), +(5, 1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'), +(6, 2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system'); + +-- 插入系统配置 +INSERT INTO sys_config (id, config_name, config_key, config_value, config_type, remark, created_by, updated_by) +VALUES +(1, '用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', '初始化用户密码', 'system', 'system'), +(2, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', '默认皮肤', 'system', 'system'), +(3, '用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', '是否开启验证码功能', 'system', 'system'); diff --git a/novalon-manage-api/manage-app/src/test/resources/schema-h2.sql b/novalon-manage-api/manage-app/src/test/resources/schema-h2.sql new file mode 100644 index 0000000..18eb964 --- /dev/null +++ b/novalon-manage-api/manage-app/src/test/resources/schema-h2.sql @@ -0,0 +1,189 @@ +-- H2数据库Schema +-- 用于测试环境 + +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + phone VARCHAR(20), + nickname VARCHAR(50), + avatar VARCHAR(255), + role_id BIGINT, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 角色表 +CREATE TABLE IF NOT EXISTS roles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + role_name VARCHAR(50) NOT NULL, + role_key VARCHAR(50) NOT NULL UNIQUE, + role_sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 用户角色关联表 +CREATE TABLE IF NOT EXISTS user_roles ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + UNIQUE(user_id, role_id) +); + +-- 菜单表 +CREATE TABLE IF NOT EXISTS sys_menu ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + menu_name VARCHAR(50) NOT NULL, + parent_id BIGINT DEFAULT 0, + order_num INTEGER DEFAULT 0, + path VARCHAR(200), + component VARCHAR(255), + menu_type CHAR(1) DEFAULT 'C', + visible CHAR(1) DEFAULT '1', + status CHAR(1) DEFAULT '1', + perms VARCHAR(100), + icon VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 权限表 +CREATE TABLE IF NOT EXISTS sys_permission ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + permission_name VARCHAR(50) NOT NULL, + permission_key VARCHAR(100) NOT NULL UNIQUE, + permission_type VARCHAR(20) DEFAULT 'menu', + parent_id BIGINT DEFAULT 0, + status INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 角色权限关联表 +CREATE TABLE IF NOT EXISTS sys_role_permission ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + role_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + UNIQUE(role_id, permission_id) +); + +-- 字典类型表 +CREATE TABLE IF NOT EXISTS sys_dict_type ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + dict_name VARCHAR(100) NOT NULL, + dict_type VARCHAR(100) NOT NULL UNIQUE, + status CHAR(1) DEFAULT '0', + remark VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 字典数据表 +CREATE TABLE IF NOT EXISTS sys_dict_data ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + dict_sort INTEGER DEFAULT 0, + dict_label VARCHAR(100) NOT NULL, + dict_value VARCHAR(100) NOT NULL, + dict_type VARCHAR(100) NOT NULL, + css_class VARCHAR(100), + list_class VARCHAR(100), + is_default CHAR(1) DEFAULT 'N', + status CHAR(1) DEFAULT '0', + remark VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 系统配置表 +CREATE TABLE IF NOT EXISTS sys_config ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + config_name VARCHAR(100) NOT NULL, + config_key VARCHAR(100) NOT NULL UNIQUE, + config_value VARCHAR(500) NOT NULL, + config_type CHAR(1) DEFAULT 'N', + remark VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50) DEFAULT 'system', + updated_by VARCHAR(50) DEFAULT 'system', + deleted_at TIMESTAMP +); + +-- 操作日志表 +CREATE TABLE IF NOT EXISTS operation_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + username VARCHAR(50), + operation VARCHAR(100), + method VARCHAR(200), + params TEXT, + time BIGINT, + ip VARCHAR(64), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 登录日志表 +CREATE TABLE IF NOT EXISTS sys_login_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + username VARCHAR(50), + ip VARCHAR(64), + location VARCHAR(255), + browser VARCHAR(100), + os VARCHAR(100), + status INTEGER DEFAULT 1, + msg VARCHAR(255), + login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 异常日志表 +CREATE TABLE IF NOT EXISTS sys_exception_log ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + username VARCHAR(50), + method VARCHAR(200), + params TEXT, + exception TEXT, + ip VARCHAR(64), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_status ON users(status); +CREATE INDEX idx_user_roles_user_id ON user_roles(user_id); +CREATE INDEX idx_user_roles_role_id ON user_roles(role_id); +CREATE INDEX idx_sys_menu_parent_id ON sys_menu(parent_id); +CREATE INDEX idx_sys_permission_key ON sys_permission(permission_key); diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/OperationLogQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/OperationLogQueryCriteria.java similarity index 97% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/OperationLogQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/OperationLogQueryCriteria.java index 302c41b..1098940 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/OperationLogQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/OperationLogQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.sys.core.query.OperationLogQuery; import cn.novalon.manage.db.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysExceptionLogQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysExceptionLogQueryCriteria.java similarity index 97% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysExceptionLogQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysExceptionLogQueryCriteria.java index 2e48de1..9200455 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysExceptionLogQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysExceptionLogQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.sys.core.query.SysExceptionLogQuery; import cn.novalon.manage.db.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysLoginLogQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysLoginLogQueryCriteria.java similarity index 97% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysLoginLogQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysLoginLogQueryCriteria.java index 02f2fcc..96cdc1c 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysLoginLogQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysLoginLogQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.sys.core.query.SysLoginLogQuery; import cn.novalon.manage.db.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysMenuQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysMenuQueryCriteria.java similarity index 97% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysMenuQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysMenuQueryCriteria.java index 25b269a..06e13f9 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysMenuQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysMenuQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.sys.core.query.SysMenuQuery; import cn.novalon.manage.db.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysRoleQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysRoleQueryCriteria.java similarity index 97% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysRoleQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysRoleQueryCriteria.java index 30f11a7..be704a3 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysRoleQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysRoleQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.sys.core.query.SysRoleQuery; import cn.novalon.manage.db.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserMessageQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysUserMessageQueryCriteria.java similarity index 96% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserMessageQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysUserMessageQueryCriteria.java index 125c7cd..ea008f6 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserMessageQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysUserMessageQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.notify.core.query.SysUserMessageQuery; import cn.novalon.manage.db.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserQueryCriteria.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysUserQueryCriteria.java similarity index 98% rename from novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserQueryCriteria.java rename to novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysUserQueryCriteria.java index 1f64244..20b41f3 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserQueryCriteria.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/query/SysUserQueryCriteria.java @@ -1,4 +1,4 @@ -package cn.novalon.manage.db.entity; +package cn.novalon.manage.db.entity.query; import cn.novalon.manage.sys.core.query.SysUserQuery; import cn.novalon.manage.common.dao.QueryField; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/OperationLogRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/OperationLogRepository.java index 55ad9b4..0c47214 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/OperationLogRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/OperationLogRepository.java @@ -9,7 +9,7 @@ import cn.novalon.manage.db.converter.OperationLogConverter; import cn.novalon.manage.db.dao.OperationLogDao; import cn.novalon.manage.db.dao.QueryUtil; import cn.novalon.manage.db.entity.OperationLogEntity; -import cn.novalon.manage.db.entity.OperationLogQueryCriteria; +import cn.novalon.manage.db.entity.query.OperationLogQueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.relational.core.query.Query; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysExceptionLogRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysExceptionLogRepository.java index 0c776a7..1007198 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysExceptionLogRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysExceptionLogRepository.java @@ -6,7 +6,7 @@ import cn.novalon.manage.db.converter.SysExceptionLogConverter; import cn.novalon.manage.db.dao.SysExceptionLogDao; import cn.novalon.manage.db.dao.QueryUtil; import cn.novalon.manage.db.entity.SysExceptionLogEntity; -import cn.novalon.manage.db.entity.SysExceptionLogQueryCriteria; +import cn.novalon.manage.db.entity.query.SysExceptionLogQueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.relational.core.query.Criteria; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysLoginLogRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysLoginLogRepository.java index 044fb27..66f3930 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysLoginLogRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysLoginLogRepository.java @@ -6,7 +6,7 @@ import cn.novalon.manage.db.converter.SysLoginLogConverter; import cn.novalon.manage.db.dao.SysLoginLogDao; import cn.novalon.manage.db.dao.QueryUtil; import cn.novalon.manage.db.entity.SysLoginLogEntity; -import cn.novalon.manage.db.entity.SysLoginLogQueryCriteria; +import cn.novalon.manage.db.entity.query.SysLoginLogQueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.relational.core.query.Criteria; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysMenuRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysMenuRepository.java index 336d9ac..a6b2922 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysMenuRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysMenuRepository.java @@ -9,7 +9,7 @@ import cn.novalon.manage.db.converter.SysMenuConverter; import cn.novalon.manage.db.dao.SysMenuDao; import cn.novalon.manage.db.dao.QueryUtil; import cn.novalon.manage.db.entity.SysMenuEntity; -import cn.novalon.manage.db.entity.SysMenuQueryCriteria; +import cn.novalon.manage.db.entity.query.SysMenuQueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.relational.core.query.Query; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysRoleRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysRoleRepository.java index c78cabd..39ac854 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysRoleRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysRoleRepository.java @@ -9,7 +9,7 @@ import cn.novalon.manage.db.converter.SysRoleConverter; import cn.novalon.manage.db.dao.SysRoleDao; import cn.novalon.manage.db.dao.QueryUtil; import cn.novalon.manage.db.entity.SysRoleEntity; -import cn.novalon.manage.db.entity.SysRoleQueryCriteria; +import cn.novalon.manage.db.entity.query.SysRoleQueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.relational.core.query.Query; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserMessageRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserMessageRepository.java index 793652f..c86b619 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserMessageRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserMessageRepository.java @@ -6,7 +6,7 @@ import cn.novalon.manage.db.converter.SysUserMessageConverter; import cn.novalon.manage.db.entity.SysUserMessageEntity; import cn.novalon.manage.db.dao.SysUserMessageDao; import cn.novalon.manage.db.dao.QueryUtil; -import cn.novalon.manage.db.entity.SysUserMessageQueryCriteria; +import cn.novalon.manage.db.entity.query.SysUserMessageQueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.stereotype.Repository; diff --git a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserRepository.java b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserRepository.java index c2a02ba..c6bccf5 100644 --- a/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserRepository.java +++ b/novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/SysUserRepository.java @@ -3,7 +3,7 @@ package cn.novalon.manage.db.repository; import cn.novalon.manage.db.converter.SysUserConverter; import cn.novalon.manage.db.dao.SysUserDao; import cn.novalon.manage.db.entity.SysUserEntity; -import cn.novalon.manage.db.entity.SysUserQueryCriteria; +import cn.novalon.manage.db.entity.query.SysUserQueryCriteria; import cn.novalon.manage.common.dao.QueryUtil; import cn.novalon.manage.sys.core.domain.SysUser; import cn.novalon.manage.sys.core.query.SysUserQuery; 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 39667ba..60de0f1 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 @@ -52,7 +52,7 @@ public class CompressionFilter implements GlobalFilter, Ordered { "application/xml" ); - private boolean compressionEnabled = true; + private boolean compressionEnabled = false; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { diff --git a/novalon-manage-api/manage-gateway/src/main/resources/application.yml b/novalon-manage-api/manage-gateway/src/main/resources/application.yml index d07b344..76a0ac2 100644 --- a/novalon-manage-api/manage-gateway/src/main/resources/application.yml +++ b/novalon-manage-api/manage-gateway/src/main/resources/application.yml @@ -2,6 +2,8 @@ server: port: 8080 spring: + codec: + max-in-memory-size: 10MB application: name: manage-gateway cloud: @@ -24,6 +26,10 @@ spring: maxBackoff: 50ms factor: 2 basedOnPreviousValue: false + - name: DedupeResponseHeader + args: + name: Content-Encoding + strategy: RETAIN_FIRST jwt: secret: ${JWT_SECRET:enc:U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4} diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/AuditLogAspect.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/AuditLogAspect.java index 61196b2..f5eb263 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/AuditLogAspect.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/AuditLogAspect.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Persistable; @@ -16,11 +15,8 @@ import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.lang.reflect.Method; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Optional; /** * 审计日志切面 diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/domain/AuditLog.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/domain/AuditLog.java index cf3605a..22096b3 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/domain/AuditLog.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/domain/AuditLog.java @@ -6,7 +6,6 @@ import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; import java.time.LocalDateTime; -import java.util.List; /** * 审计日志领域对象 diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AsyncConfig.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AsyncConfig.java index 10e3e87..e5a1e34 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AsyncConfig.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AsyncConfig.java @@ -10,7 +10,6 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; -import java.util.concurrent.ThreadPoolExecutor; /** * 异步配置类 diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AuditingConfig.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AuditingConfig.java index 344be13..229f9ac 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AuditingConfig.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/AuditingConfig.java @@ -5,7 +5,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.ReactiveAuditorAware; import org.springframework.data.r2dbc.config.EnableR2dbcAuditing; import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import reactor.core.publisher.Mono; /** * R2DBC审计配置类 diff --git a/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceIntegrationTest.java b/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceIntegrationTest.java new file mode 100644 index 0000000..647a9c6 --- /dev/null +++ b/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceIntegrationTest.java @@ -0,0 +1,245 @@ +package cn.novalon.manage.sys.core.service.impl; + +import cn.novalon.manage.common.util.StatusConstants; +import cn.novalon.manage.sys.core.domain.SysUser; +import cn.novalon.manage.sys.core.domain.SysRole; +import cn.novalon.manage.sys.core.domain.UserRole; +import cn.novalon.manage.sys.core.repository.ISysUserRepository; +import cn.novalon.manage.sys.core.repository.ISysRoleRepository; +import cn.novalon.manage.sys.core.repository.IUserRoleRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.LocalDateTime; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 用户服务集成测试 + * + * 使用Testcontainers进行PostgreSQL数据库集成测试 + * + * @author 张翔 + * @date 2026-04-02 + */ +@DataR2dbcTest +@Testcontainers +@ActiveProfiles("test") +class SysUserServiceIntegrationTest { + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine") + .withDatabaseName("testdb") + .withUsername("test") + .withPassword("test"); + + @DynamicPropertySource + static void postgresProperties(DynamicPropertyRegistry registry) { + registry.add("spring.r2dbc.url", () -> + String.format("r2dbc:postgresql://%s:%d/%s", + postgres.getHost(), + postgres.getFirstMappedPort(), + postgres.getDatabaseName())); + registry.add("spring.r2dbc.username", postgres::getUsername); + registry.add("spring.r2dbc.password", postgres::getPassword); + } + + @Autowired + private ISysUserRepository userRepository; + + @Autowired + private ISysRoleRepository roleRepository; + + @Autowired + private IUserRoleRepository userRoleRepository; + + @Autowired + private R2dbcEntityTemplate r2dbcEntityTemplate; + + private SysUserService userService; + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setUp() { + passwordEncoder = new BCryptPasswordEncoder(12); + userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder); + + r2dbcEntityTemplate.delete(SysUser.class).all().block(); + r2dbcEntityTemplate.delete(SysRole.class).all().block(); + r2dbcEntityTemplate.delete(UserRole.class).all().block(); + } + + @Test + void testCreateAndFindUser() { + SysUser user = new SysUser(); + user.setUsername("testuser"); + user.setPassword("password123"); + user.setEmail("test@example.com"); + user.setNickname("Test User"); + user.setPhone("13800138000"); + + StepVerifier.create(userService.createUser(user)) + .expectNextMatches(createdUser -> { + assertNotNull(createdUser.getId()); + assertEquals("testuser", createdUser.getUsername()); + assertEquals("test@example.com", createdUser.getEmail()); + assertTrue(createdUser.getPassword().startsWith("$2b$")); + assertEquals(StatusConstants.ENABLED, createdUser.getStatus()); + return true; + }) + .verifyComplete(); + + StepVerifier.create(userService.findByUsername("testuser")) + .expectNextMatches(foundUser -> { + assertEquals("testuser", foundUser.getUsername()); + assertEquals("test@example.com", foundUser.getEmail()); + return true; + }) + .verifyComplete(); + } + + @Test + void testUpdateUser() { + SysUser user = new SysUser(); + user.setUsername("updateuser"); + user.setPassword("password123"); + user.setEmail("update@example.com"); + + SysUser createdUser = userService.createUser(user).block(); + assertNotNull(createdUser); + + createdUser.setEmail("updated@example.com"); + createdUser.setNickname("Updated User"); + + StepVerifier.create(userService.updateUser(createdUser)) + .expectNextMatches(updatedUser -> { + assertEquals("updated@example.com", updatedUser.getEmail()); + assertEquals("Updated User", updatedUser.getNickname()); + return true; + }) + .verifyComplete(); + } + + @Test + void testDeleteUser() { + SysUser user = new SysUser(); + user.setUsername("deleteuser"); + user.setPassword("password123"); + user.setEmail("delete@example.com"); + + SysUser createdUser = userService.createUser(user).block(); + assertNotNull(createdUser); + + StepVerifier.create(userService.deleteUser(createdUser.getId())) + .verifyComplete(); + + StepVerifier.create(userService.findById(createdUser.getId())) + .verifyComplete(); + } + + @Test + void testChangePassword() { + SysUser user = new SysUser(); + user.setUsername("pwduser"); + user.setPassword("oldPassword"); + user.setEmail("pwd@example.com"); + + SysUser createdUser = userService.createUser(user).block(); + assertNotNull(createdUser); + + StepVerifier.create(userService.changePassword(createdUser.getId(), "oldPassword", "newPassword")) + .expectNextMatches(updatedUser -> { + assertNotEquals(createdUser.getPassword(), updatedUser.getPassword()); + assertTrue(passwordEncoder.matches("newPassword", updatedUser.getPassword())); + return true; + }) + .verifyComplete(); + } + + @Test + void testAssignRolesToUser() { + SysRole role1 = new SysRole(); + role1.setRoleName("Test Role 1"); + role1.setRoleKey("test_role_1"); + role1.setStatus(1); + + SysRole role2 = new SysRole(); + role2.setRoleName("Test Role 2"); + role2.setRoleKey("test_role_2"); + role2.setStatus(1); + + SysRole createdRole1 = roleRepository.save(role1).block(); + SysRole createdRole2 = roleRepository.save(role2).block(); + assertNotNull(createdRole1); + assertNotNull(createdRole2); + + SysUser user = new SysUser(); + user.setUsername("roleuser"); + user.setPassword("password123"); + user.setEmail("role@example.com"); + + SysUser createdUser = userService.createUser(user).block(); + assertNotNull(createdUser); + + StepVerifier.create(userService.assignRolesToUser(createdUser.getId(), + Arrays.asList(createdRole1.getId(), createdRole2.getId()))) + .verifyComplete(); + + StepVerifier.create(userRoleRepository.findByUserId(createdUser.getId()).collectList()) + .expectNextMatches(userRoles -> { + assertEquals(2, userRoles.size()); + return true; + }) + .verifyComplete(); + } + + @Test + void testFindAllUsers() { + for (int i = 1; i <= 3; i++) { + SysUser user = new SysUser(); + user.setUsername("user" + i); + user.setPassword("password" + i); + user.setEmail("user" + i + "@example.com"); + userService.createUser(user).block(); + } + + StepVerifier.create(userService.findAll(false).collectList()) + .expectNextMatches(users -> { + assertEquals(3, users.size()); + return true; + }) + .verifyComplete(); + } + + @Test + void testExistsByUsername() { + SysUser user = new SysUser(); + user.setUsername("existinguser"); + user.setPassword("password123"); + user.setEmail("existing@example.com"); + userService.createUser(user).block(); + + StepVerifier.create(userService.existsByUsername("existinguser")) + .expectNext(true) + .verifyComplete(); + + StepVerifier.create(userService.existsByUsername("nonexistinguser")) + .expectNext(false) + .verifyComplete(); + } +} diff --git a/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceTest.java b/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceTest.java index 320d996..fa510cd 100644 --- a/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceTest.java +++ b/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceTest.java @@ -2,13 +2,18 @@ package cn.novalon.manage.sys.core.service.impl; import cn.novalon.manage.common.util.StatusConstants; import cn.novalon.manage.sys.core.domain.SysUser; -import cn.novalon.manage.sys.core.repository.ISysUserRepository; +import cn.novalon.manage.sys.core.domain.SysRole; +import cn.novalon.manage.sys.core.domain.UserRole; import cn.novalon.manage.common.dto.PageRequest; import cn.novalon.manage.common.dto.PageResponse; +import cn.novalon.manage.sys.core.repository.ISysUserRepository; +import cn.novalon.manage.sys.core.repository.ISysRoleRepository; +import cn.novalon.manage.sys.core.repository.IUserRoleRepository; +import cn.novalon.manage.sys.core.command.CreateUserCommand; +import cn.novalon.manage.sys.core.command.UpdateUserCommand; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; @@ -17,576 +22,269 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.LocalDateTime; -import java.util.List; +import java.util.Arrays; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; /** - * 用户服务单元测试类 + * 用户服务单元测试 * * @author 张翔 - * @date 2026-03-14 + * @date 2026-04-02 */ @ExtendWith(MockitoExtension.class) class SysUserServiceTest { - @Mock - private ISysUserRepository userRepository; - - @Mock - private cn.novalon.manage.sys.core.repository.ISysRoleRepository roleRepository; - - @Mock - private cn.novalon.manage.sys.core.repository.IUserRoleRepository userRoleRepository; - - @Mock - private PasswordEncoder passwordEncoder; - - private SysUserService userService; - - private SysUser testUser; - - @BeforeEach - void setUp() { - userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder); - - testUser = new SysUser(); - testUser.setId(1L); - testUser.setUsername("testuser"); - testUser.setPassword("encoded_password"); - testUser.setEmail("test@example.com"); - testUser.setRoleId(1L); - testUser.setStatus(StatusConstants.ENABLED); - testUser.setCreatedAt(LocalDateTime.now()); - testUser.setUpdatedAt(LocalDateTime.now()); - } - - @Test - void testFindById() { - when(userRepository.findById(1L)).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.findById(1L)) - .expectNext(testUser) - .verifyComplete(); - - verify(userRepository).findById(1L); - } - - @Test - void testFindAll() { - when(userRepository.findAll()).thenReturn(Flux.just(testUser)); - - StepVerifier.create(userService.findAll()) - .expectNext(testUser) - .verifyComplete(); - - verify(userRepository).findAll(); - } - - @Test - void testFindAll_IncludeDeleted() { - when(userRepository.findAll()).thenReturn(Flux.just(testUser)); - - StepVerifier.create(userService.findAll(true)) - .expectNext(testUser) - .verifyComplete(); - - verify(userRepository).findAll(); - } - - @Test - void testFindAll_ExcludeDeleted() { - when(userRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(testUser)); - - StepVerifier.create(userService.findAll(false)) - .expectNext(testUser) - .verifyComplete(); - - verify(userRepository).findByDeletedAtIsNull(); - } - - @Test - void testFindUsersByPage() { - PageRequest pageRequest = new PageRequest(); - pageRequest.setPage(0); - pageRequest.setSize(10); - pageRequest.setKeyword("test"); - - PageResponse pageResponse = new PageResponse<>(); - pageResponse.setContent(List.of(testUser)); - pageResponse.setTotalElements(1L); - - when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest))) - .thenReturn(Mono.just(pageResponse)); - - StepVerifier.create(userService.findUsersByPage(pageRequest)) - .expectNextMatches(response -> response.getTotalElements() == 1L) - .verifyComplete(); - - verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest)); - } - - @Test - void testFindUsersByPage_NoKeyword() { - PageRequest pageRequest = new PageRequest(); - pageRequest.setPage(0); - pageRequest.setSize(10); - - PageResponse pageResponse = new PageResponse<>(); - pageResponse.setContent(List.of(testUser)); - pageResponse.setTotalElements(1L); - - when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest))) - .thenReturn(Mono.just(pageResponse)); - - StepVerifier.create(userService.findUsersByPage(pageRequest)) - .expectNextMatches(response -> response.getTotalElements() == 1L) - .verifyComplete(); - - verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest)); - } - - @Test - void testCount() { - when(userRepository.count()).thenReturn(Mono.just(10L)); - - StepVerifier.create(userService.count()) - .expectNext(10L) - .verifyComplete(); - - verify(userRepository).count(); - } - - @Test - void testFindByUsername() { - when(userRepository.findByUsername("testuser")).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.findByUsername("testuser")) - .expectNext(testUser) - .verifyComplete(); - - verify(userRepository).findByUsername("testuser"); - } - - @Test - void testCreateUser() { - SysUser newUser = new SysUser(); - newUser.setUsername("newuser"); - newUser.setPassword("raw_password"); - newUser.setEmail("new@example.com"); - - when(passwordEncoder.encode("raw_password")).thenReturn("encoded_password"); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.createUser(newUser)) - .expectNextMatches(user -> user.getPassword().equals("encoded_password") && - user.getStatus().equals(StatusConstants.ENABLED) && - user.getCreatedAt() != null) - .verifyComplete(); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(SysUser.class); - verify(userRepository).save(userCaptor.capture()); - verify(passwordEncoder).encode("raw_password"); - } - - @Test - void testDeleteUser() { - when(userRepository.findById(1L)).thenReturn(Mono.just(testUser)); - when(userRepository.deleteById(1L)).thenReturn(Mono.empty()); - - StepVerifier.create(userService.deleteUser(1L)) - .verifyComplete(); - - verify(userRepository).deleteById(1L); - } - - @Test - void testChangePassword_Success() { - when(userRepository.findById(1L)).thenReturn(Mono.just(testUser)); - when(passwordEncoder.matches("old_password", "encoded_password")).thenReturn(true); - when(passwordEncoder.encode("new_password")).thenReturn("new_encoded_password"); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.changePassword(1L, "old_password", "new_password")) - .expectNextMatches(user -> user.getPassword().equals("new_encoded_password")) - .verifyComplete(); - - verify(passwordEncoder).matches("old_password", "encoded_password"); - verify(passwordEncoder).encode("new_password"); - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testChangePassword_WrongOldPassword() { - when(userRepository.findById(1L)).thenReturn(Mono.just(testUser)); - when(passwordEncoder.matches("wrong_password", "encoded_password")).thenReturn(false); - - StepVerifier.create(userService.changePassword(1L, "wrong_password", "new_password")) - .expectError(RuntimeException.class) - .verify(); - - verify(passwordEncoder).matches("wrong_password", "encoded_password"); - verify(passwordEncoder, never()).encode(anyString()); - verify(userRepository, never()).save(any(SysUser.class)); - } - - @Test - void testExistsByUsername_True() { - when(userRepository.findByUsername("testuser")).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.existsByUsername("testuser")) - .expectNext(true) - .verifyComplete(); - - verify(userRepository).findByUsername("testuser"); - } - - @Test - void testExistsByUsername_False() { - when(userRepository.findByUsername("nonexistent")).thenReturn(Mono.empty()); - - StepVerifier.create(userService.existsByUsername("nonexistent")) - .expectNext(false) - .verifyComplete(); - - verify(userRepository).findByUsername("nonexistent"); - } - - @Test - void testExistsByEmail_True() { - when(userRepository.findByEmail("test@example.com")).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.existsByEmail("test@example.com")) - .expectNext(true) - .verifyComplete(); - - verify(userRepository).findByEmail("test@example.com"); - } - - @Test - void testExistsByEmail_False() { - when(userRepository.findByEmail("nonexistent@example.com")).thenReturn(Mono.empty()); - - StepVerifier.create(userService.existsByEmail("nonexistent@example.com")) - .expectNext(false) - .verifyComplete(); - - verify(userRepository).findByEmail("nonexistent@example.com"); - } - - @Test - void testLogicalDeleteUser() { - when(userRepository.findByIdIncludingDeleted(1L)).thenReturn(Mono.just(testUser)); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.logicalDeleteUser(1L)) - .verifyComplete(); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(SysUser.class); - verify(userRepository).save(userCaptor.capture()); - assert userCaptor.getValue().getDeletedAt() != null : "DeletedAt should be set"; - } - - @Test - void testLogicalDeleteUsers() { - List ids = List.of(1L, 2L, 3L); - when(userRepository.logicalDeleteByIds(ids)).thenReturn(Mono.empty()); - - StepVerifier.create(userService.logicalDeleteUsers(ids)) - .verifyComplete(); - - verify(userRepository).logicalDeleteByIds(ids); - } - - @Test - void testRestoreUser() { - SysUser deletedUser = new SysUser(); - deletedUser.setId(1L); - deletedUser.setDeletedAt(LocalDateTime.now()); - - when(userRepository.findByIdIncludingDeleted(1L)).thenReturn(Mono.just(deletedUser)); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.restoreUser(1L)) - .verifyComplete(); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(SysUser.class); - verify(userRepository).save(userCaptor.capture()); - } - - @Test - void testRestoreUsers() { - List ids = List.of(1L, 2L, 3L); - when(userRepository.restoreByIds(ids)).thenReturn(Mono.empty()); - - StepVerifier.create(userService.restoreUsers(ids)) - .verifyComplete(); - - verify(userRepository).restoreByIds(ids); - } - - @Test - void testCreateUser_WithNullStatus() { - SysUser newUser = new SysUser(); - newUser.setUsername("newuser"); - newUser.setPassword("raw_password"); - newUser.setEmail("new@example.com"); - newUser.setStatus(null); - - when(passwordEncoder.encode("raw_password")).thenReturn("encoded_password"); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - StepVerifier.create(userService.createUser(newUser)) - .expectNextMatches(user -> user.getPassword().equals("encoded_password") && - user.getStatus().equals(StatusConstants.ENABLED) && - user.getCreatedAt() != null) - .verifyComplete(); - - verify(passwordEncoder).encode("raw_password"); - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testCreateUser_WithExistingStatus() { - SysUser newUser = new SysUser(); - newUser.setUsername("newuser"); - newUser.setPassword("raw_password"); - newUser.setEmail("new@example.com"); - newUser.setStatus(StatusConstants.DISABLED); - - SysUser savedUser = new SysUser(); - savedUser.setId(1L); - savedUser.setUsername("newuser"); - savedUser.setPassword("encoded_password"); - savedUser.setEmail("new@example.com"); - savedUser.setStatus(StatusConstants.DISABLED); - savedUser.setCreatedAt(LocalDateTime.now()); - - when(passwordEncoder.encode("raw_password")).thenReturn("encoded_password"); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(savedUser)); - - StepVerifier.create(userService.createUser(newUser)) - .expectNextMatches(user -> user.getPassword().equals("encoded_password") && - user.getStatus().equals(StatusConstants.DISABLED) && - user.getCreatedAt() != null) - .verifyComplete(); - - verify(passwordEncoder).encode("raw_password"); - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testDeleteUser_UserNotFound() { - when(userRepository.findById(999L)).thenReturn(Mono.empty()); - - StepVerifier.create(userService.deleteUser(999L)) - .expectError(RuntimeException.class) - .verify(); - - verify(userRepository).findById(999L); - verify(userRepository, never()).deleteById(anyLong()); - } - - @Test - void testFindUsersByPage_WithKeyword() { - PageRequest pageRequest = new PageRequest(); - pageRequest.setPage(0); - pageRequest.setSize(10); - pageRequest.setKeyword("test"); - - PageResponse pageResponse = new PageResponse<>(); - pageResponse.setContent(List.of(testUser)); - pageResponse.setTotalElements(1L); - - when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest))) - .thenReturn(Mono.just(pageResponse)); - - StepVerifier.create(userService.findUsersByPage(pageRequest)) - .expectNextMatches(response -> response.getTotalElements() == 1L) - .verifyComplete(); - - verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest)); - } - - @Test - void testFindUsersByPage_WithoutKeyword() { - PageRequest pageRequest = new PageRequest(); - pageRequest.setPage(0); - pageRequest.setSize(10); - - PageResponse pageResponse = new PageResponse<>(); - pageResponse.setContent(List.of(testUser)); - pageResponse.setTotalElements(1L); - - when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest))) - .thenReturn(Mono.just(pageResponse)); - - StepVerifier.create(userService.findUsersByPage(pageRequest)) - .expectNextMatches(response -> response.getTotalElements() == 1L) - .verifyComplete(); - - verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest)); - } - - @Test - void testFindUsersByPage_WithEmptyKeyword() { - PageRequest pageRequest = new PageRequest(); - pageRequest.setPage(0); - pageRequest.setSize(10); - pageRequest.setKeyword(""); - - PageResponse pageResponse = new PageResponse<>(); - pageResponse.setContent(List.of(testUser)); - pageResponse.setTotalElements(1L); - - when(userRepository.findByQueryWithPagination(isNull(), eq(pageRequest))) - .thenReturn(Mono.just(pageResponse)); - - StepVerifier.create(userService.findUsersByPage(pageRequest)) - .expectNextMatches(response -> response.getTotalElements() == 1L) - .verifyComplete(); - - verify(userRepository).findByQueryWithPagination(isNull(), eq(pageRequest)); - } - - @Test - void testUpdateUserWithCommand_WithAllFields() { - SysUser existingUser = new SysUser(); - existingUser.setId(1L); - existingUser.setUsername("olduser"); - existingUser.setEmail("old@example.com"); - existingUser.setRoleId(1L); - existingUser.setStatus(StatusConstants.ENABLED); - - when(userRepository.findById(1L)).thenReturn(Mono.just(existingUser)); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - cn.novalon.manage.sys.core.command.UpdateUserCommand command = cn.novalon.manage.sys.core.command.UpdateUserCommand - .of( - 1L, "newuser", "newpass", "new@example.com", 2L, - StatusConstants.DISABLED); - - StepVerifier.create(userService.updateUser(command)) - .expectNextMatches(user -> user.getUpdatedAt() != null) - .verifyComplete(); - - verify(userRepository).findById(1L); - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testUpdateUserWithCommand_WithPartialFields() { - SysUser existingUser = new SysUser(); - existingUser.setId(1L); - existingUser.setUsername("olduser"); - existingUser.setEmail("old@example.com"); - existingUser.setRoleId(1L); - existingUser.setStatus(StatusConstants.ENABLED); - - when(userRepository.findById(1L)).thenReturn(Mono.just(existingUser)); - when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(testUser)); - - cn.novalon.manage.sys.core.command.UpdateUserCommand command = cn.novalon.manage.sys.core.command.UpdateUserCommand - .of( - 1L, null, null, null, null, null); - - StepVerifier.create(userService.updateUser(command)) - .expectNextMatches(user -> user.getUpdatedAt() != null) - .verifyComplete(); - - verify(userRepository).findById(1L); - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testCreateUserWithCommand_Success() { - cn.novalon.manage.sys.core.command.CreateUserCommand command = cn.novalon.manage.sys.core.command.CreateUserCommand - .of( - "newuser", - "Password123!", - "newuser@example.com", - null, null, 1L, - StatusConstants.ENABLED); - - when(passwordEncoder.encode("Password123!")).thenReturn("encoded_password"); - when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> { - SysUser savedUser = invocation.getArgument(0); - savedUser.setId(1L); - return Mono.just(savedUser); - }); - - StepVerifier.create(userService.createUser(command)) - .expectNextMatches(user -> user.getUsername().equals("newuser") && - user.getPassword().equals("encoded_password") && - user.getEmail().equals("newuser@example.com") && - user.getRoleId().equals(1L) && - user.getStatus().equals(StatusConstants.ENABLED) && - user.getCreatedAt() != null) - .verifyComplete(); - - verify(passwordEncoder).encode("Password123!"); - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testCreateUserWithCommand_WithNullStatus() { - cn.novalon.manage.sys.core.command.CreateUserCommand command = cn.novalon.manage.sys.core.command.CreateUserCommand - .of( - "newuser", - "Password123!", - "newuser@example.com", - null, null, 1L, - null); - - when(passwordEncoder.encode("Password123!")).thenReturn("encoded_password"); - when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> { - SysUser savedUser = invocation.getArgument(0); - savedUser.setId(1L); - return Mono.just(savedUser); - }); - - StepVerifier.create(userService.createUser(command)) - .expectNextMatches(user -> user.getStatus().equals(StatusConstants.ENABLED)) - .verifyComplete(); - - verify(userRepository).save(any(SysUser.class)); - } - - @Test - void testUpdateUserWithCommand_AllFields() { - SysUser existingUser = new SysUser(); - existingUser.setId(1L); - existingUser.setUsername("olduser"); - existingUser.setEmail("old@example.com"); - existingUser.setRoleId(1L); - existingUser.setStatus(StatusConstants.ENABLED); - - cn.novalon.manage.sys.core.command.UpdateUserCommand command = cn.novalon.manage.sys.core.command.UpdateUserCommand - .of( - 1L, "newuser", "NewPassword123!", "new@example.com", 2L, - StatusConstants.DISABLED); - - when(userRepository.findById(1L)).thenReturn(Mono.just(existingUser)); - when(passwordEncoder.encode("NewPassword123!")).thenReturn("encoded_newpassword"); - when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> { - SysUser savedUser = invocation.getArgument(0); - return Mono.just(savedUser); - }); - - StepVerifier.create(userService.updateUser(command)) - .expectNextMatches(user -> user.getUsername().equals("newuser") && - user.getPassword().equals("encoded_newpassword") && - user.getEmail().equals("new@example.com") && - user.getRoleId().equals(2L) && - user.getStatus().equals(StatusConstants.DISABLED) && - user.getUpdatedAt() != null) - .verifyComplete(); - - verify(userRepository).findById(1L); - verify(passwordEncoder).encode("NewPassword123!"); - verify(userRepository).save(any(SysUser.class)); - } + @Mock + private ISysUserRepository userRepository; + + @Mock + private ISysRoleRepository roleRepository; + + @Mock + private IUserRoleRepository userRoleRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + private SysUserService userService; + + @BeforeEach + void setUp() { + userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder); + } + + @Test + void testFindById() { + SysUser user = new SysUser(); + user.setId(1L); + user.setUsername("testuser"); + user.setEmail("test@example.com"); + + when(userRepository.findById(1L)).thenReturn(Mono.just(user)); + + StepVerifier.create(userService.findById(1L)) + .expectNextMatches(u -> u.getId().equals(1L) && u.getUsername().equals("testuser")) + .verifyComplete(); + + verify(userRepository, times(1)).findById(1L); + } + + @Test + void testFindByIdNotFound() { + when(userRepository.findById(999L)).thenReturn(Mono.empty()); + + StepVerifier.create(userService.findById(999L)) + .verifyComplete(); + + verify(userRepository, times(1)).findById(999L); + } + + @Test + void testFindAll() { + SysUser user1 = new SysUser(); + user1.setId(1L); + user1.setUsername("user1"); + + SysUser user2 = new SysUser(); + user2.setId(2L); + user2.setUsername("user2"); + + when(userRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(user1, user2)); + + StepVerifier.create(userService.findAll(false)) + .expectNext(user1) + .expectNext(user2) + .verifyComplete(); + + verify(userRepository, times(1)).findByDeletedAtIsNull(); + } + + @Test + void testCreateUser() { + SysUser user = new SysUser(); + user.setUsername("newuser"); + user.setPassword("plainPassword"); + user.setEmail("newuser@example.com"); + + when(passwordEncoder.encode(anyString())).thenReturn("$2b$12$encodedPassword"); + when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> { + SysUser savedUser = invocation.getArgument(0); + savedUser.setId(1L); + return Mono.just(savedUser); + }); + + StepVerifier.create(userService.createUser(user)) + .expectNextMatches(savedUser -> + savedUser.getId().equals(1L) && + savedUser.getPassword().equals("$2b$12$encodedPassword") && + savedUser.getStatus().equals(StatusConstants.ENABLED) + ) + .verifyComplete(); + + verify(passwordEncoder, times(1)).encode("plainPassword"); + verify(userRepository, times(1)).save(any(SysUser.class)); + } + + @Test + void testCreateUserWithCommand() { + CreateUserCommand command = mock(CreateUserCommand.class); + when(command.username()).thenReturn(mock(cn.novalon.manage.sys.primitive.Username.class)); + when(command.password()).thenReturn(mock(cn.novalon.manage.sys.primitive.Password.class)); + when(command.email()).thenReturn(mock(cn.novalon.manage.sys.primitive.Email.class)); + when(command.username().getValue()).thenReturn("testuser"); + when(command.password().getValue()).thenReturn("password123"); + when(command.email().getValue()).thenReturn("test@example.com"); + when(command.nickname()).thenReturn("Test User"); + when(command.phone()).thenReturn("13800138000"); + when(command.roleId()).thenReturn(1L); + when(command.status()).thenReturn(null); + + when(passwordEncoder.encode(anyString())).thenReturn("$2b$12$encodedPassword"); + when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> { + SysUser savedUser = invocation.getArgument(0); + savedUser.setId(1L); + return Mono.just(savedUser); + }); + + StepVerifier.create(userService.createUser(command)) + .expectNextMatches(savedUser -> + savedUser.getUsername().equals("testuser") && + savedUser.getPassword().equals("$2b$12$encodedPassword") && + savedUser.getEmail().equals("test@example.com") + ) + .verifyComplete(); + + verify(passwordEncoder, times(1)).encode("password123"); + verify(userRepository, times(1)).save(any(SysUser.class)); + } + + @Test + void testUpdateUser() { + SysUser user = new SysUser(); + user.setId(1L); + user.setUsername("testuser"); + user.setEmail("updated@example.com"); + + when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(user)); + + StepVerifier.create(userService.updateUser(user)) + .expectNextMatches(updatedUser -> + updatedUser.getId().equals(1L) && + updatedUser.getEmail().equals("updated@example.com") + ) + .verifyComplete(); + + verify(userRepository, times(1)).save(any(SysUser.class)); + } + + @Test + void testDeleteUser() { + SysUser user = new SysUser(); + user.setId(1L); + user.setUsername("testuser"); + + when(userRepository.findById(1L)).thenReturn(Mono.just(user)); + when(userRoleRepository.deleteByUserId(1L)).thenReturn(Mono.empty()); + when(userRepository.deleteById(1L)).thenReturn(Mono.empty()); + + StepVerifier.create(userService.deleteUser(1L)) + .verifyComplete(); + + verify(userRepository, times(1)).findById(1L); + verify(userRoleRepository, times(1)).deleteByUserId(1L); + verify(userRepository, times(1)).deleteById(1L); + } + + @Test + void testDeleteUserNotFound() { + when(userRepository.findById(999L)).thenReturn(Mono.empty()); + + StepVerifier.create(userService.deleteUser(999L)) + .expectErrorMatches(error -> error instanceof RuntimeException && + error.getMessage().equals("User not found")) + .verify(); + + verify(userRepository, times(1)).findById(999L); + verify(userRoleRepository, never()).deleteByUserId(anyLong()); + verify(userRepository, never()).deleteById(anyLong()); + } + + @Test + void testChangePassword() { + SysUser user = new SysUser(); + user.setId(1L); + user.setUsername("testuser"); + user.setPassword("$2b$12$oldPassword"); + + when(userRepository.findById(1L)).thenReturn(Mono.just(user)); + when(passwordEncoder.matches("oldPassword", "$2b$12$oldPassword")).thenReturn(true); + when(passwordEncoder.encode("newPassword")).thenReturn("$2b$12$newPassword"); + when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); + + StepVerifier.create(userService.changePassword(1L, "oldPassword", "newPassword")) + .expectNextMatches(updatedUser -> + updatedUser.getPassword().equals("$2b$12$newPassword") + ) + .verifyComplete(); + + verify(passwordEncoder, times(1)).matches("oldPassword", "$2b$12$oldPassword"); + verify(passwordEncoder, times(1)).encode("newPassword"); + verify(userRepository, times(1)).save(any(SysUser.class)); + } + + @Test + void testChangePasswordIncorrectOldPassword() { + SysUser user = new SysUser(); + user.setId(1L); + user.setUsername("testuser"); + user.setPassword("$2b$12$oldPassword"); + + when(userRepository.findById(1L)).thenReturn(Mono.just(user)); + when(passwordEncoder.matches("wrongPassword", "$2b$12$oldPassword")).thenReturn(false); + + StepVerifier.create(userService.changePassword(1L, "wrongPassword", "newPassword")) + .expectErrorMatches(error -> error instanceof RuntimeException && + error.getMessage().equals("旧密码不正确")) + .verify(); + + verify(passwordEncoder, times(1)).matches("wrongPassword", "$2b$12$oldPassword"); + verify(passwordEncoder, never()).encode(anyString()); + verify(userRepository, never()).save(any(SysUser.class)); + } + + @Test + void testExistsByUsername() { + when(userRepository.findByUsername("existinguser")).thenReturn(Mono.just(new SysUser())); + when(userRepository.findByUsername("nonexistinguser")).thenReturn(Mono.empty()); + + StepVerifier.create(userService.existsByUsername("existinguser")) + .expectNext(true) + .verifyComplete(); + + StepVerifier.create(userService.existsByUsername("nonexistinguser")) + .expectNext(false) + .verifyComplete(); + + verify(userRepository, times(1)).findByUsername("existinguser"); + verify(userRepository, times(1)).findByUsername("nonexistinguser"); + } + + @Test + void testAssignRolesToUser() { + Long userId = 1L; + java.util.List roleIds = Arrays.asList(1L, 2L); + + when(userRoleRepository.deleteByUserId(userId)).thenReturn(Mono.empty()); + when(userRoleRepository.save(any(UserRole.class))).thenReturn(Mono.just(new UserRole())); + + StepVerifier.create(userService.assignRolesToUser(userId, roleIds)) + .verifyComplete(); + + verify(userRoleRepository, times(1)).deleteByUserId(userId); + verify(userRoleRepository, times(2)).save(any(UserRole.class)); + } } diff --git a/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/interceptor/OperationLogFilterTest.java b/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/interceptor/OperationLogFilterTest.java deleted file mode 100644 index 10ef519..0000000 --- a/novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/interceptor/OperationLogFilterTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package cn.novalon.manage.sys.interceptor; - -import cn.novalon.manage.sys.core.domain.OperationLog; -import cn.novalon.manage.sys.core.service.IOperationLogService; -import com.fasterxml.jackson.databind.ObjectMapper; -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.mock.http.server.reactive.MockServerHttpRequest; -import org.springframework.mock.web.server.MockServerWebExchange; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilterChain; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.net.InetSocketAddress; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class OperationLogFilterTest { - - @Mock - private IOperationLogService logService; - - @Mock - private WebFilterChain chain; - - @Mock - private ObjectMapper objectMapper; - - private OperationLogFilter filter; - - @BeforeEach - void setUp() { - filter = new OperationLogFilter(logService, objectMapper); - } - - @Test - void testFilter_SkipAuthEndpoints() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(chain).filter(exchange); - verify(logService, never()).save(any(OperationLog.class)); - } - - @Test - void testFilter_RecordSuccessLog() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .remoteAddress(new InetSocketAddress("127.0.0.1", 8080)) - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(chain).filter(exchange); - verify(logService).save(any(OperationLog.class)); - } - - @Test - void testFilter_RecordErrorLog() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .remoteAddress(new InetSocketAddress("127.0.0.1", 8080)) - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - RuntimeException error = new RuntimeException("Test error"); - when(chain.filter(exchange)).thenReturn(Mono.error(error)); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .expectError(RuntimeException.class) - .verify(); - - verify(chain).filter(exchange); - verify(logService).save(any(OperationLog.class)); - } - - @Test - void testFilter_WithXForwardedForHeader() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .header("X-Forwarded-For", "192.168.1.1") - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(logService).save(argThat(log -> "192.168.1.1".equals(log.getIp()))); - } - - @Test - void testFilter_WithXRealIPHeader() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .header("X-Real-IP", "10.0.0.1") - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(logService).save(argThat(log -> "10.0.0.1".equals(log.getIp()))); - } - - @Test - void testFilter_WithMultipleIPsInXForwardedFor() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .header("X-Forwarded-For", "192.168.1.1, 10.0.0.1") - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(logService).save(argThat(log -> "192.168.1.1".equals(log.getIp()))); - } - - @Test - void testFilter_WithUnknownHeader() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .header("X-Forwarded-For", "unknown") - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(logService).save(any(OperationLog.class)); - } - - @Test - void testFilter_DifferentHttpMethods() { - HttpMethod[] methods = {HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH}; - - for (HttpMethod method : methods) { - MockServerHttpRequest request = MockServerHttpRequest.method(method, "/api/users") - .remoteAddress(new InetSocketAddress("127.0.0.1", 8080)) - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(logService).save(argThat(log -> method.name().equals(log.getMethod()))); - reset(logService, chain); - } - } - - @Test - void testFilter_WithQueryParams() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users?page=1&size=10") - .remoteAddress(new InetSocketAddress("127.0.0.1", 8080)) - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog())); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(logService).save(argThat(log -> log.getParams() != null && !log.getParams().isEmpty())); - } - - @Test - void testFilter_LogSaveError() { - MockServerHttpRequest request = MockServerHttpRequest.get("/api/users") - .remoteAddress(new InetSocketAddress("127.0.0.1", 8080)) - .build(); - ServerWebExchange exchange = MockServerWebExchange.from(request); - - when(chain.filter(exchange)).thenReturn(Mono.empty()); - when(logService.save(any(OperationLog.class))).thenReturn(Mono.error(new RuntimeException("Save failed"))); - - StepVerifier.create(filter.filter(exchange, chain)) - .verifyComplete(); - - verify(chain).filter(exchange); - verify(logService).save(any(OperationLog.class)); - } -} diff --git a/novalon-manage-web/e2e/login-test.spec.ts b/novalon-manage-web/e2e/login-test.spec.ts new file mode 100644 index 0000000..c917492 --- /dev/null +++ b/novalon-manage-web/e2e/login-test.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' + +test.describe('登录签名测试', () => { + test('登录功能应该正常工作', async ({ page }) => { + page.on('console', msg => { + console.log('BROWSER CONSOLE:', msg.type(), msg.text()) + }) + + page.on('pageerror', error => { + console.error('PAGE ERROR:', error.message) + }) + + page.on('requestfailed', request => { + console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText) + }) + + await page.goto('/login') + + await page.fill('input[placeholder="请输入用户名"]', 'admin') + await page.fill('input[placeholder="请输入密码"]', 'admin123') + + await page.click('button:has-text("登录")') + + await page.waitForURL('**/dashboard', { timeout: 10000 }) + + console.log('Current URL after login:', page.url()) + + const token = await page.evaluate(() => localStorage.getItem('token')) + console.log('Token in localStorage:', token ? 'exists' : 'not found') + + expect(page.url()).toContain('/dashboard') + expect(token).toBeTruthy() + }) +}) diff --git a/novalon-manage-web/package.json b/novalon-manage-web/package.json index 852f10c..4089b20 100644 --- a/novalon-manage-web/package.json +++ b/novalon-manage-web/package.json @@ -32,6 +32,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "axios": "^1.6.2", + "crypto-js": "^4.2.0", "dayjs": "^1.11.10", "element-plus": "^2.13.5", "pinia": "^3.0.4", @@ -41,6 +42,7 @@ }, "devDependencies": { "@playwright/test": "^1.40.1", + "@types/crypto-js": "^4.2.2", "@types/node": "^20.10.0", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", diff --git a/novalon-manage-web/playwright.config.ts b/novalon-manage-web/playwright.config.ts index 270a811..a4c231c 100644 --- a/novalon-manage-web/playwright.config.ts +++ b/novalon-manage-web/playwright.config.ts @@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isHeadless = process.env.PLAYWRIGHT_HEADLESS === 'true' || process.env.CI === 'true'; -const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3001'; +const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3002'; export default defineConfig({ testDir: './e2e', @@ -98,7 +98,7 @@ export default defineConfig({ webServer: { command: 'npm run dev', - url: 'http://localhost:3001', + url: 'http://localhost:3002', reuseExistingServer: !process.env.CI, timeout: 120000, stdout: 'pipe', diff --git a/novalon-manage-web/pnpm-lock.yaml b/novalon-manage-web/pnpm-lock.yaml index f9e886f..aa062d5 100644 --- a/novalon-manage-web/pnpm-lock.yaml +++ b/novalon-manage-web/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: axios: specifier: ^1.6.2 version: 1.13.6 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 dayjs: specifier: ^1.11.10 version: 1.11.20 @@ -36,6 +39,9 @@ importers: '@playwright/test': specifier: ^1.40.1 version: 1.58.2 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/node': specifier: ^20.10.0 version: 20.19.37 @@ -552,6 +558,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -891,6 +900,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -2231,6 +2243,8 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -2660,6 +2674,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + css-tree@3.2.1: dependencies: mdn-data: 2.27.1 diff --git a/novalon-manage-web/src/utils/request.ts b/novalon-manage-web/src/utils/request.ts index 79d8523..ae824df 100644 --- a/novalon-manage-web/src/utils/request.ts +++ b/novalon-manage-web/src/utils/request.ts @@ -1,4 +1,5 @@ -import axios from 'axios' +import axios, { AxiosRequestConfig } from 'axios' +import { generateSignatureHeaders } from './signature' const request = axios.create({ baseURL: '/api', @@ -6,11 +7,23 @@ const request = axios.create({ }) request.interceptors.request.use( - (config) => { + (config: AxiosRequestConfig) => { const token = localStorage.getItem('token') if (token) { + config.headers = config.headers || {} config.headers.Authorization = `Bearer ${token}` } + + const method = config.method?.toUpperCase() || 'GET' + const url = config.url || '' + const body = config.data + + const fullPath = `/api${url.startsWith('/') ? url : '/' + url}` + const signatureHeaders = generateSignatureHeaders(method, fullPath, body) + + config.headers = config.headers || {} + Object.assign(config.headers, signatureHeaders) + return config }, (error) => Promise.reject(error) diff --git a/novalon-manage-web/src/utils/signature.ts b/novalon-manage-web/src/utils/signature.ts new file mode 100644 index 0000000..b6d846d --- /dev/null +++ b/novalon-manage-web/src/utils/signature.ts @@ -0,0 +1,96 @@ +import CryptoJS from 'crypto-js' + +const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026' + +export interface SignatureHeaders { + 'X-Signature': string + 'X-Timestamp': string + 'X-Nonce': string +} + +export function generateSignature( + method: string, + path: string, + query: string = '', + body: string = '', + timestamp: number, + nonce: string +): string { + const stringToSign = buildStringToSign(method, path, query, body, timestamp, nonce) + + const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET) + const signatureBase64 = CryptoJS.enc.Base64.stringify(signature) + + return signatureBase64 +} + +export function generateSignatureHeaders( + method: string, + url: string, + body?: any +): SignatureHeaders { + const timestamp = Date.now() + const nonce = generateNonce() + + const { path, query } = parseUrl(url) + const bodyString = '' + + const signature = generateSignature( + method.toUpperCase(), + path, + query || '', + bodyString, + timestamp, + nonce + ) + + return { + 'X-Signature': signature, + 'X-Timestamp': timestamp.toString(), + 'X-Nonce': nonce + } +} + +function buildStringToSign( + method: string, + path: string, + query: string, + body: string, + timestamp: number, + nonce: string +): string { + return [ + method, + path, + query || '', + body || '', + timestamp.toString(), + nonce + ].join('\n') +} + +function generateNonce(): string { + const timestamp = Date.now().toString(36) + const randomPart = Math.random().toString(36).substring(2, 15) + return `${timestamp}-${randomPart}` +} + +function parseUrl(url: string): { path: string; query: string } { + if (url.startsWith('http://') || url.startsWith('https://')) { + const urlObj = new URL(url) + return { + path: urlObj.pathname, + query: urlObj.search.substring(1) + } + } + + const queryIndex = url.indexOf('?') + if (queryIndex === -1) { + return { path: url, query: '' } + } + + return { + path: url.substring(0, queryIndex), + query: url.substring(queryIndex + 1) + } +} diff --git a/novalon-manage-web/vite.config.ts b/novalon-manage-web/vite.config.ts index 7c9eb4b..0e21d88 100644 --- a/novalon-manage-web/vite.config.ts +++ b/novalon-manage-web/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ strictPort: true, proxy: { '/api': { - target: 'http://localhost:8084', + target: 'http://localhost:8080', changeOrigin: true, secure: false } diff --git a/test-signature.js b/test-signature.js new file mode 100644 index 0000000..6770fc0 --- /dev/null +++ b/test-signature.js @@ -0,0 +1,49 @@ +const CryptoJS = require('crypto-js') + +const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026' + +function generateSignature(method, path, query = '', body = '', timestamp, nonce) { + const stringToSign = [ + method, + path, + query || '', + body || '', + timestamp.toString(), + nonce + ].join('\n') + + console.log('String to sign:', stringToSign) + + const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET) + const signatureBase64 = CryptoJS.enc.Base64.stringify(signature) + + return signatureBase64 +} + +function generateNonce() { + const timestamp = Date.now().toString(36) + const randomPart = Math.random().toString(36).substring(2, 15) + return `${timestamp}-${randomPart}` +} + +const timestamp = Date.now() +const nonce = generateNonce() +const method = 'POST' +const path = '/api/auth/login' +const query = '' +const body = JSON.stringify({ username: 'admin', password: 'admin123' }) + +const signature = generateSignature(method, path, query, body, timestamp, nonce) + +console.log('\nGenerated Signature Headers:') +console.log('X-Signature:', signature) +console.log('X-Timestamp:', timestamp) +console.log('X-Nonce:', nonce) + +console.log('\ncurl command:') +console.log(`curl -X POST http://localhost:8080/api/auth/login \\ + -H "Content-Type: application/json" \\ + -H "X-Signature: ${signature}" \\ + -H "X-Timestamp: ${timestamp}" \\ + -H "X-Nonce: ${nonce}" \\ + -d '${body}'`) diff --git a/test_comprehensive_workflow.py b/test_comprehensive_workflow.py new file mode 100644 index 0000000..0f77757 --- /dev/null +++ b/test_comprehensive_workflow.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +""" +Novalon管理系统全面业务流程测试 +测试所有核心业务流程 +""" + +import time +import json +from datetime import datetime +from playwright.sync_api import sync_playwright, Page, expect + +class TestResult: + def __init__(self): + self.total = 0 + self.passed = 0 + self.failed = 0 + self.errors = [] + self.start_time = datetime.now() + + def add_pass(self, test_name): + self.total += 1 + self.passed += 1 + print(f"✅ {test_name} - 通过") + + def add_fail(self, test_name, error): + self.total += 1 + self.failed += 1 + self.errors.append({"test": test_name, "error": str(error)}) + print(f"❌ {test_name} - 失败: {error}") + + def print_summary(self): + duration = (datetime.now() - self.start_time).total_seconds() + print("\n" + "="*80) + print("测试总结") + print("="*80) + print(f"总测试数: {self.total}") + print(f"通过: {self.passed} ✅") + print(f"失败: {self.failed} ❌") + print(f"成功率: {(self.passed/self.total*100):.2f}%") + print(f"耗时: {duration:.2f}秒") + + if self.errors: + print("\n失败详情:") + for error in self.errors: + print(f" - {error['test']}: {error['error']}") + print("="*80) + +result = TestResult() + +def login(page: Page, username: str = "admin", password: str = "Test@123"): + """登录系统""" + try: + page.goto("http://localhost:3002/login") + page.wait_for_load_state("networkidle") + + page.fill('input[placeholder*="用户名"]', username) + page.fill('input[placeholder*="密码"]', password) + page.click('button:has-text("登录")') + + page.wait_for_url("**/dashboard", timeout=10000) + page.wait_for_load_state("networkidle") + + return True + except Exception as e: + print(f"登录失败: {e}") + return False + +def test_user_management_flow(page: Page): + """测试用户管理完整流程""" + print("\n📋 测试用户管理流程...") + + try: + # 导航到用户管理页面 + page.click('text=用户管理') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试创建用户 + print(" - 测试创建用户...") + page.click('button:has-text("新增")') + time.sleep(0.5) + + page.fill('input[placeholder*="用户名"]', f"testuser_{int(time.time())}") + page.fill('input[placeholder*="密码"]', "admin123") + page.fill('input[placeholder*="邮箱"]', f"test_{int(time.time())}@example.com") + page.fill('input[placeholder*="手机"]', "13800138000") + page.fill('input[placeholder*="昵称"]', "测试用户") + + page.click('button:has-text("确定")') + time.sleep(1) + + result.add_pass("用户管理-创建用户") + + except Exception as e: + result.add_fail("用户管理-创建用户", e) + +def test_role_management_flow(page: Page): + """测试角色管理完整流程""" + print("\n📋 测试角色管理流程...") + + try: + # 导航到角色管理页面 + page.click('text=角色管理') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试创建角色 + print(" - 测试创建角色...") + page.click('button:has-text("新增")') + time.sleep(0.5) + + page.fill('input[placeholder*="角色名称"]', f"测试角色_{int(time.time())}") + page.fill('input[placeholder*="角色标识"]', f"test_role_{int(time.time())}") + + page.click('button:has-text("确定")') + time.sleep(1) + + result.add_pass("角色管理-创建角色") + + except Exception as e: + result.add_fail("角色管理-创建角色", e) + +def test_menu_management_flow(page: Page): + """测试菜单管理完整流程""" + print("\n📋 测试菜单管理流程...") + + try: + # 导航到菜单管理页面 + page.click('text=菜单管理') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试创建菜单 + print(" - 测试创建菜单...") + page.click('button:has-text("新增")') + time.sleep(0.5) + + page.fill('input[placeholder*="菜单名称"]', f"测试菜单_{int(time.time())}") + page.select_option('select', 'C') + + page.click('button:has-text("确定")') + time.sleep(1) + + result.add_pass("菜单管理-创建菜单") + + except Exception as e: + result.add_fail("菜单管理-创建菜单", e) + +def test_dictionary_management_flow(page: Page): + """测试字典管理完整流程""" + print("\n📋 测试字典管理流程...") + + try: + # 导航到字典管理页面 + page.click('text=字典管理') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试查询字典 + print(" - 测试查询字典...") + page.fill('input[placeholder*="搜索"]', "用户状态") + page.press('input[placeholder*="搜索"]', 'Enter') + time.sleep(1) + + result.add_pass("字典管理-查询字典") + + except Exception as e: + result.add_fail("字典管理-查询字典", e) + +def test_system_config_flow(page: Page): + """测试系统配置流程""" + print("\n📋 测试系统配置流程...") + + try: + # 导航到系统配置页面 + page.click('text=系统配置') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试查询配置 + print(" - 测试查询配置...") + page.fill('input[placeholder*="搜索"]', "用户") + page.press('input[placeholder*="搜索"]', 'Enter') + time.sleep(1) + + result.add_pass("系统配置-查询配置") + + except Exception as e: + result.add_fail("系统配置-查询配置", e) + +def test_file_management_flow(page: Page): + """测试文件管理流程""" + print("\n📋 测试文件管理流程...") + + try: + # 导航到文件管理页面 + page.click('text=文件管理') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试查看文件列表 + print(" - 测试查看文件列表...") + page.wait_for_selector('table', timeout=5000) + + result.add_pass("文件管理-查看文件列表") + + except Exception as e: + result.add_fail("文件管理-查看文件列表", e) + +def test_notification_flow(page: Page): + """测试通知管理流程""" + print("\n📋 测试通知管理流程...") + + try: + # 导航到通知管理页面 + page.click('text=通知公告') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试查看通知列表 + print(" - 测试查看通知列表...") + page.wait_for_selector('table', timeout=5000) + + result.add_pass("通知管理-查看通知列表") + + except Exception as e: + result.add_fail("通知管理-查看通知列表", e) + +def test_audit_log_flow(page: Page): + """测试审计日志流程""" + print("\n📋 测试审计日志流程...") + + try: + # 导航到操作日志页面 + page.click('text=操作日志') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 测试查看日志列表 + print(" - 测试查看操作日志...") + page.wait_for_selector('table', timeout=5000) + + result.add_pass("审计日志-查看操作日志") + + # 导航到登录日志页面 + page.click('text=登录日志') + page.wait_for_load_state("networkidle") + time.sleep(1) + + result.add_pass("审计日志-查看登录日志") + + except Exception as e: + result.add_fail("审计日志-查看日志", e) + +def test_permission_validation(page: Page): + """测试权限验证""" + print("\n📋 测试权限验证...") + + try: + # 测试菜单权限 + print(" - 测试菜单访问权限...") + menus = ['用户管理', '角色管理', '菜单管理', '字典管理', '系统配置'] + + for menu in menus: + try: + page.click(f'text={menu}') + page.wait_for_load_state("networkidle") + time.sleep(0.5) + result.add_pass(f"权限验证-访问{menu}") + except: + result.add_fail(f"权限验证-访问{menu}", "无法访问") + + except Exception as e: + result.add_fail("权限验证", e) + +def test_dashboard(page: Page): + """测试仪表板""" + print("\n📋 测试仪表板...") + + try: + # 导航到仪表板 + page.click('text=仪表板') + page.wait_for_load_state("networkidle") + time.sleep(1) + + # 验证仪表板加载 + page.wait_for_selector('.dashboard-container, .stats-card, .chart', timeout=5000) + + result.add_pass("仪表板-加载成功") + + except Exception as e: + result.add_fail("仪表板-加载", e) + +def main(): + print("="*80) + print("Novalon管理系统全面业务流程测试") + print("="*80) + print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("="*80) + + with sync_playwright() as p: + # 启动浏览器 + browser = p.chromium.launch(headless=True) + context = browser.new_context() + page = context.new_page() + + try: + # 登录测试 + print("\n🔐 测试登录...") + if login(page): + result.add_pass("登录功能") + print("✅ 登录成功") + else: + result.add_fail("登录功能", "登录失败") + return + + # 测试仪表板 + test_dashboard(page) + + # 测试用户管理流程 + test_user_management_flow(page) + + # 测试角色管理流程 + test_role_management_flow(page) + + # 测试菜单管理流程 + test_menu_management_flow(page) + + # 测试字典管理流程 + test_dictionary_management_flow(page) + + # 测试系统配置流程 + test_system_config_flow(page) + + # 测试文件管理流程 + test_file_management_flow(page) + + # 测试通知管理流程 + test_notification_flow(page) + + # 测试审计日志流程 + test_audit_log_flow(page) + + # 测试权限验证 + test_permission_validation(page) + + except Exception as e: + print(f"\n❌ 测试执行出错: {e}") + import traceback + traceback.print_exc() + + finally: + browser.close() + + # 打印测试总结 + result.print_summary() + + # 返回退出码 + return 0 if result.failed == 0 else 1 + +if __name__ == "__main__": + exit(main()) diff --git a/test_signature.py b/test_signature.py new file mode 100644 index 0000000..c7041a9 --- /dev/null +++ b/test_signature.py @@ -0,0 +1,51 @@ +import hmac +import hashlib +import base64 +import time +import json +import requests + +SECRET = 'NovalonManageSystemSecretKey2026' + +def generate_signature(method, path, query='', body='', timestamp=None, nonce=None): + if timestamp is None: + timestamp = int(time.time() * 1000) + if nonce is None: + nonce = f"{int(timestamp)}-{hash(time.time())}" + + string_to_sign = f"{method}\n{path}\n{query}\n{body}\n{timestamp}\n{nonce}" + + signature = hmac.new( + SECRET.encode('utf-8'), + string_to_sign.encode('utf-8'), + hashlib.sha256 + ).digest() + + signature_base64 = base64.b64encode(signature).decode('utf-8') + + return signature_base64, timestamp, nonce + +method = 'POST' +path = '/api/auth/login' +body = '' + +signature, timestamp, nonce = generate_signature(method, path, body=body) + +print(f"X-Signature: {signature}") +print(f"X-Timestamp: {timestamp}") +print(f"X-Nonce: {nonce}") + +headers = { + 'Content-Type': 'application/json', + 'X-Signature': signature, + 'X-Timestamp': str(timestamp), + 'X-Nonce': nonce +} + +response = requests.post('http://localhost:8080/api/auth/login', + headers=headers, + data='{"username":"admin","password":"admin123"}', + verify=False) + +print(f"\nResponse Status: {response.status_code}") +print(f"Response Body: {response.text}")