feat: 添加测试框架和覆盖率报告功能

feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
This commit is contained in:
张翔
2026-03-25 09:03:37 +08:00
parent 117978e148
commit e2ad1331cc
126 changed files with 18083 additions and 7805 deletions
@@ -8,6 +8,7 @@ import cn.novalon.manage.sys.handler.log.SysLogHandler;
import cn.novalon.manage.sys.handler.log.OperationLogHandler;
import cn.novalon.manage.sys.handler.menu.MenuHandler;
import cn.novalon.manage.sys.handler.role.SysRoleHandler;
import cn.novalon.manage.sys.handler.permission.SysPermissionHandler;
import cn.novalon.manage.sys.handler.stats.StatsHandler;
import cn.novalon.manage.sys.handler.user.SysUserHandler;
import cn.novalon.manage.notify.handler.SysNoticeHandler;
@@ -80,7 +81,7 @@ public class SystemRouter {
}
@Bean
public RouterFunction<ServerResponse> roleRoutes(SysRoleHandler roleHandler) {
public RouterFunction<ServerResponse> roleRoutes(SysRoleHandler roleHandler, SysPermissionHandler permissionHandler) {
return route()
.GET("/api/roles", roleHandler::getAllRoles)
.GET("/api/roles/page", roleHandler::getRolesByPage)
@@ -92,6 +93,8 @@ public class SystemRouter {
.PUT("/api/roles/{id}", roleHandler::updateRole)
.DELETE("/api/roles/{id}", roleHandler::deleteRole)
.POST("/api/roles/{id}/restore", roleHandler::restoreRole)
.GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
.POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
.build();
}
@@ -206,4 +209,18 @@ public class SystemRouter {
.DELETE("/api/files/{id}", fileHandler::deleteFile)
.build();
}
@Bean
public RouterFunction<ServerResponse> permissionRoutes(SysPermissionHandler permissionHandler) {
return route()
.GET("/api/permissions", permissionHandler::getAllPermissions)
.GET("/api/permissions/{id}", permissionHandler::getPermissionById)
.GET("/api/permissions/code/{code}", permissionHandler::getPermissionByCode)
.GET("/api/permissions/check-code", permissionHandler::checkCodeExists)
.GET("/api/permissions/count", permissionHandler::getPermissionCount)
.POST("/api/permissions", permissionHandler::createPermission)
.PUT("/api/permissions/{id}", permissionHandler::updatePermission)
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
.build();
}
}
@@ -0,0 +1,83 @@
-- 测试数据初始化脚本
-- 用于E2E测试和UAT测试的测试数据生成
-- 1. 清理现有测试数据
DELETE FROM sys_user_role WHERE user_id IN (SELECT id FROM sys_user WHERE username LIKE 'test_%' OR username = 'admin');
DELETE FROM sys_role_menu WHERE role_id IN (SELECT id FROM sys_role WHERE role_key LIKE 'test_%' OR role_key = 'admin');
DELETE FROM sys_login_log WHERE username IN ('admin', 'test_user', 'test_admin');
DELETE FROM sys_user WHERE username IN ('admin', 'test_user', 'test_admin');
DELETE FROM sys_role WHERE role_key LIKE 'test_%' OR role_key = 'admin';
DELETE FROM sys_menu WHERE menu_name LIKE '测试%' OR menu_name = '系统管理';
-- 2. 插入测试角色
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, create_time, update_by, update_time, remark) VALUES
('超级管理员', 'admin', 1, 1, 'system', NOW(), 'system', NOW(), '系统超级管理员,拥有所有权限'),
('普通用户', 'user', 2, 1, 'system', NOW(), 'system', NOW(), '普通用户,拥有基本权限'),
('测试管理员', 'test_admin', 3, 1, 'system', NOW(), 'system', NOW(), '测试用管理员角色'),
('测试普通用户', 'test_user', 4, 1, 'system', NOW(), 'system', NOW(), '测试用普通用户角色');
-- 3. 插入测试菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES
('系统管理', 0, 1, 'system', NULL, 'M', '0', '0', '', 'system', 'system', NOW(), 'system', NOW(), '系统管理目录'),
('用户管理', 1, 1, 'user', 'system/user/index', 'C', '0', '0', 'system:user:list', 'user', 'system', NOW(), 'system', NOW(), '用户管理菜单'),
('角色管理', 1, 2, 'role', 'system/role/index', 'C', '0', '0', 'system:role:list', 'role', 'system', NOW(), 'system', NOW(), '角色管理菜单'),
('菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '0', '0', 'system:menu:list', 'menu', 'system', NOW(), 'system', NOW(), '菜单管理菜单'),
('审计日志', 0, 2, 'audit', NULL, 'M', '0', '0', '', 'audit', 'system', NOW(), 'system', NOW(), '审计日志目录'),
('登录日志', 5, 1, 'loginlog', 'audit/loginlog/index', 'C', '0', '0', 'audit:loginlog:list', 'loginlog', 'system', NOW(), 'system', NOW(), '登录日志菜单'),
('系统监控', 0, 3, 'monitor', NULL, 'M', '0', '0', '', 'monitor', 'system', NOW(), 'system', NOW(), '系统监控目录'),
('在线用户', 7, 1, 'online', 'monitor/online/index', 'C', '0', '0', 'monitor:online:list', 'online', 'system', NOW(), 'system', NOW(), '在线用户菜单');
-- 4. 插入测试用户
INSERT INTO sys_user (username, password, email, phone, status, create_by, create_time, update_by, update_time, remark) VALUES
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', 'admin@novalon.com', '13800138000', 1, 'system', NOW(), 'system', NOW(), '系统管理员'),
('test_user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', 'testuser@novalon.com', '13800138001', 1, 'system', NOW(), 'system', NOW(), '测试普通用户'),
('test_admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', 'testadmin@novalon.com', '13800138002', 1, 'system', NOW(), 'system', NOW(), '测试管理员');
-- 5. 分配用户角色关系
INSERT INTO sys_user_role (user_id, role_id) VALUES
((SELECT id FROM sys_user WHERE username = 'admin'), (SELECT id FROM sys_role WHERE role_key = 'admin')),
((SELECT id FROM sys_user WHERE username = 'test_user'), (SELECT id FROM sys_role WHERE role_key = 'test_user')),
((SELECT id FROM sys_user WHERE username = 'test_admin'), (SELECT id FROM sys_role WHERE role_key = 'test_admin'));
-- 6. 分配角色菜单关系
-- 超级管理员拥有所有菜单权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT (SELECT id FROM sys_role WHERE role_key = 'admin'), id FROM sys_menu;
-- 普通用户只拥有用户查看权限
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
((SELECT id FROM sys_role WHERE role_key = 'user'), (SELECT id FROM sys_menu WHERE menu_name = '系统管理')),
((SELECT id FROM sys_role WHERE role_key = 'user'), (SELECT id FROM sys_menu WHERE menu_name = '用户管理'));
-- 测试管理员拥有系统管理权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT (SELECT id FROM sys_role WHERE role_key = 'test_admin'), id FROM sys_menu WHERE menu_name IN ('系统管理', '用户管理', '角色管理', '菜单管理', '审计日志', '登录日志');
-- 测试普通用户拥有基本查看权限
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
((SELECT id FROM sys_role WHERE role_key = 'test_user'), (SELECT id FROM sys_menu WHERE menu_name = '系统管理')),
((SELECT id FROM sys_role WHERE role_key = 'test_user'), (SELECT id FROM sys_menu WHERE menu_name = '用户管理'));
-- 7. 插入测试登录日志
INSERT INTO sys_login_log (username, ipaddr, login_location, browser, os, status, msg, login_time, create_by, create_time) VALUES
('admin', '127.0.0.1', '本地', 'Chrome 120.0', 'Mac OS X', 1, '登录成功', NOW(), 'system', NOW()),
('test_user', '127.0.0.1', '本地', 'Firefox 121.0', 'Windows 10', 1, '登录成功', NOW(), 'system', NOW()),
('test_admin', '127.0.0.1', '本地', 'Safari 17.0', 'Mac OS X', 1, '登录成功', NOW(), 'system', NOW()),
('admin', '192.168.1.100', '内网', 'Chrome 119.0', 'Windows 11', 1, '登录成功', NOW() - INTERVAL '1 hour', 'system', NOW() - INTERVAL '1 hour'),
('test_user', '192.168.1.101', '内网', 'Edge 120.0', 'Windows 10', 1, '登录成功', NOW() - INTERVAL '2 hours', 'system', NOW() - INTERVAL '2 hours');
-- 8. 验证测试数据
SELECT '测试用户数据' as data_type, COUNT(*) as count FROM sys_user WHERE username IN ('admin', 'test_user', 'test_admin')
UNION ALL
SELECT '测试角色数据', COUNT(*) FROM sys_role WHERE role_key IN ('admin', 'user', 'test_admin', 'test_user')
UNION ALL
SELECT '测试菜单数据', COUNT(*) FROM sys_menu WHERE menu_name IN ('系统管理', '用户管理', '角色管理', '菜单管理', '审计日志', '登录日志', '系统监控', '在线用户')
UNION ALL
SELECT '用户角色关系', COUNT(*) FROM sys_user_role WHERE user_id IN (SELECT id FROM sys_user WHERE username IN ('admin', 'test_user', 'test_admin'))
UNION ALL
SELECT '角色菜单关系', COUNT(*) FROM sys_role_menu WHERE role_id IN (SELECT id FROM sys_role WHERE role_key IN ('admin', 'user', 'test_admin', 'test_user'))
UNION ALL
SELECT '登录日志数据', COUNT(*) FROM sys_login_log WHERE username IN ('admin', 'test_user', 'test_admin');
-- 提交事务
COMMIT;
@@ -0,0 +1,73 @@
package cn.novalon.manage.db.converter;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.db.entity.SysPermissionEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 权限实体转换器
*
* @author 张翔
* @date 2026-03-25
*/
@Component
public class SysPermissionConverter {
public SysPermission toDomain(SysPermissionEntity entity) {
if (entity == null) {
return null;
}
SysPermission domain = new SysPermission();
domain.setId(entity.getId());
domain.setPermissionName(entity.getPermissionName());
domain.setPermissionCode(entity.getPermissionCode());
domain.setResource(entity.getResource());
domain.setAction(entity.getAction());
domain.setDescription(entity.getDescription());
domain.setStatus(entity.getStatus());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public SysPermissionEntity toEntity(SysPermission domain) {
if (domain == null) {
return null;
}
SysPermissionEntity entity = new SysPermissionEntity();
entity.setId(domain.getId());
entity.setPermissionName(domain.getPermissionName());
entity.setPermissionCode(domain.getPermissionCode());
entity.setResource(domain.getResource());
entity.setAction(domain.getAction());
entity.setDescription(domain.getDescription());
entity.setStatus(domain.getStatus());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysPermission> toDomainList(List<SysPermissionEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysPermissionEntity> toEntityList(List<SysPermission> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,63 @@
package cn.novalon.manage.db.converter;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.db.entity.SysRolePermissionEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 角色权限关联实体转换器
*
* @author 张翔
* @date 2026-03-25
*/
@Component
public class SysRolePermissionConverter {
public SysRolePermission toDomain(SysRolePermissionEntity entity) {
if (entity == null) {
return null;
}
SysRolePermission domain = new SysRolePermission();
domain.setId(entity.getId());
domain.setRoleId(entity.getRoleId());
domain.setPermissionId(entity.getPermissionId());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
return domain;
}
public SysRolePermissionEntity toEntity(SysRolePermission domain) {
if (domain == null) {
return null;
}
SysRolePermissionEntity entity = new SysRolePermissionEntity();
entity.setId(domain.getId());
entity.setRoleId(domain.getRoleId());
entity.setPermissionId(domain.getPermissionId());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
return entity;
}
public List<SysRolePermission> toDomainList(List<SysRolePermissionEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysRolePermissionEntity> toEntityList(List<SysRolePermission> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -22,4 +22,6 @@ public interface SysLoginLogDao extends R2dbcRepository<SysLoginLogEntity, Long>
Mono<Long> count();
Mono<Long> countByUsername(String username);
Mono<Long> countByLoginTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
}
@@ -0,0 +1,38 @@
package cn.novalon.manage.db.dao;
import cn.novalon.manage.db.entity.SysPermissionEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysPermissionDao extends R2dbcRepository<SysPermissionEntity, Long> {
Mono<SysPermissionEntity> findByIdAndDeletedAtIsNull(Long id);
Mono<SysPermissionEntity> findByPermissionCodeAndDeletedAtIsNull(String permissionCode);
Flux<SysPermissionEntity> findByDeletedAtIsNull();
Flux<SysPermissionEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Boolean> existsByPermissionCodeAndDeletedAtIsNull(String permissionCode);
@org.springframework.data.r2dbc.repository.Query("""
SELECT p.* FROM sys_permission p
INNER JOIN sys_role_permission rp ON p.id = rp.permission_id
WHERE rp.role_id = :roleId AND p.deleted_at IS NULL
""")
Flux<SysPermissionEntity> findByRoleId(Long roleId);
@org.springframework.data.r2dbc.repository.Query("""
SELECT DISTINCT p.* FROM sys_permission p
INNER JOIN sys_role_permission rp ON p.id = rp.permission_id
WHERE rp.role_id IN (:roleIds) AND p.deleted_at IS NULL
""")
Flux<SysPermissionEntity> findByRoleIds(java.util.List<Long> roleIds);
}
@@ -0,0 +1,41 @@
package cn.novalon.manage.db.dao;
import cn.novalon.manage.db.entity.SysRolePermissionEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysRolePermissionDao extends R2dbcRepository<SysRolePermissionEntity, Long> {
Flux<SysRolePermissionEntity> findByRoleId(Long roleId);
Flux<SysRolePermissionEntity> findByPermissionId(Long permissionId);
Flux<Long> findPermissionIdsByRoleId(Long roleId);
Flux<Long> findRoleIdsByPermissionId(Long permissionId);
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission
WHERE role_id = :roleId AND permission_id IN (:permissionIds)
""")
Mono<Void> deleteByRoleIdAndPermissionIds(Long roleId, java.util.List<Long> permissionIds);
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission
WHERE permission_id = :permissionId AND role_id IN (:roleIds)
""")
Mono<Void> deleteByPermissionIdAndRoleIds(Long permissionId, java.util.List<Long> roleIds);
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission WHERE role_id = :roleId
""")
Mono<Void> deleteByRoleId(Long roleId);
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission WHERE permission_id = :permissionId
""")
Mono<Void> deleteByPermissionId(Long permissionId);
}
@@ -0,0 +1,80 @@
package cn.novalon.manage.db.entity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
/**
* 权限数据库实体类
*
* @author 张翔
* @date 2026-03-25
*/
@Table("sys_permission")
public class SysPermissionEntity extends BaseEntity {
@Column("permission_name")
private String permissionName;
@Column("permission_code")
private String permissionCode;
@Column("resource")
private String resource;
@Column("action")
private String action;
@Column("description")
private String description;
@Column("status")
private Integer status;
public String getPermissionName() {
return permissionName;
}
public void setPermissionName(String permissionName) {
this.permissionName = permissionName;
}
public String getPermissionCode() {
return permissionCode;
}
public void setPermissionCode(String permissionCode) {
this.permissionCode = permissionCode;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
@@ -0,0 +1,36 @@
package cn.novalon.manage.db.entity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
/**
* 角色权限关联数据库实体类
*
* @author 张翔
* @date 2026-03-25
*/
@Table("sys_role_permission")
public class SysRolePermissionEntity extends BaseEntity {
@Column("role_id")
private Long roleId;
@Column("permission_id")
private Long permissionId;
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getPermissionId() {
return permissionId;
}
public void setPermissionId(Long permissionId) {
this.permissionId = permissionId;
}
}
@@ -0,0 +1,97 @@
package cn.novalon.manage.db.repository;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.manage.db.converter.SysPermissionConverter;
import cn.novalon.manage.db.dao.SysPermissionDao;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 权限仓储实现类
*
* @author 张翔
* @date 2026-03-25
*/
@Repository
public class SysPermissionRepository implements ISysPermissionRepository {
private final SysPermissionDao sysPermissionDao;
private final SysPermissionConverter sysPermissionConverter;
public SysPermissionRepository(SysPermissionDao sysPermissionDao, SysPermissionConverter sysPermissionConverter) {
this.sysPermissionDao = sysPermissionDao;
this.sysPermissionConverter = sysPermissionConverter;
}
@Override
public Mono<SysPermission> findById(Long id) {
return sysPermissionDao.findByIdAndDeletedAtIsNull(id)
.map(sysPermissionConverter::toDomain);
}
@Override
public Mono<SysPermission> findByIdIncludingDeleted(Long id) {
return sysPermissionDao.findById(id)
.map(sysPermissionConverter::toDomain);
}
@Override
public Mono<SysPermission> save(SysPermission sysPermission) {
return sysPermissionDao.save(sysPermissionConverter.toEntity(sysPermission))
.map(sysPermissionConverter::toDomain);
}
@Override
public Mono<Void> deleteById(Long id) {
return sysPermissionDao.deleteById(id);
}
@Override
public Flux<SysPermission> findAll() {
return sysPermissionDao.findByDeletedAtIsNull()
.map(sysPermissionConverter::toDomain);
}
@Override
public Flux<SysPermission> findAll(Sort sort) {
return sysPermissionDao.findByDeletedAtIsNull(sort)
.map(sysPermissionConverter::toDomain);
}
@Override
public Mono<SysPermission> findByPermissionCode(String permissionCode) {
return sysPermissionDao.findByPermissionCodeAndDeletedAtIsNull(permissionCode)
.map(sysPermissionConverter::toDomain);
}
@Override
public Mono<Long> count() {
return sysPermissionDao.countByDeletedAtIsNull();
}
@Override
public Mono<Boolean> existsByPermissionCode(String permissionCode) {
return sysPermissionDao.existsByPermissionCodeAndDeletedAtIsNull(permissionCode);
}
@Override
public Mono<SysPermission> updatePermission(SysPermission permission) {
return sysPermissionDao.save(sysPermissionConverter.toEntity(permission))
.map(sysPermissionConverter::toDomain);
}
@Override
public Flux<SysPermission> findByRoleId(Long roleId) {
return sysPermissionDao.findByRoleId(roleId)
.map(sysPermissionConverter::toDomain);
}
@Override
public Flux<SysPermission> findByRoleIds(java.util.List<Long> roleIds) {
return sysPermissionDao.findByRoleIds(roleIds)
.map(sysPermissionConverter::toDomain);
}
}
@@ -0,0 +1,80 @@
package cn.novalon.manage.db.repository;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.manage.db.converter.SysRolePermissionConverter;
import cn.novalon.manage.db.dao.SysRolePermissionDao;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 角色权限关联仓储实现类
*
* @author 张翔
* @date 2026-03-25
*/
@Repository
public class SysRolePermissionRepository implements ISysRolePermissionRepository {
private final SysRolePermissionDao sysRolePermissionDao;
private final SysRolePermissionConverter sysRolePermissionConverter;
public SysRolePermissionRepository(SysRolePermissionDao sysRolePermissionDao, SysRolePermissionConverter sysRolePermissionConverter) {
this.sysRolePermissionDao = sysRolePermissionDao;
this.sysRolePermissionConverter = sysRolePermissionConverter;
}
@Override
public Mono<SysRolePermission> save(SysRolePermission rolePermission) {
return sysRolePermissionDao.save(sysRolePermissionConverter.toEntity(rolePermission))
.map(sysRolePermissionConverter::toDomain);
}
@Override
public Mono<Void> deleteById(Long id) {
return sysRolePermissionDao.deleteById(id);
}
@Override
public Mono<Void> deleteByRoleId(Long roleId) {
return sysRolePermissionDao.deleteByRoleId(roleId);
}
@Override
public Mono<Void> deleteByPermissionId(Long permissionId) {
return sysRolePermissionDao.deleteByPermissionId(permissionId);
}
@Override
public Flux<SysRolePermission> findByRoleId(Long roleId) {
return sysRolePermissionDao.findByRoleId(roleId)
.map(sysRolePermissionConverter::toDomain);
}
@Override
public Flux<SysRolePermission> findByPermissionId(Long permissionId) {
return sysRolePermissionDao.findByPermissionId(permissionId)
.map(sysRolePermissionConverter::toDomain);
}
@Override
public Flux<Long> findPermissionIdsByRoleId(Long roleId) {
return sysRolePermissionDao.findPermissionIdsByRoleId(roleId);
}
@Override
public Flux<Long> findRoleIdsByPermissionId(Long permissionId) {
return sysRolePermissionDao.findRoleIdsByPermissionId(permissionId);
}
@Override
public Mono<Void> deleteByRoleIdAndPermissionIds(Long roleId, java.util.List<Long> permissionIds) {
return sysRolePermissionDao.deleteByRoleIdAndPermissionIds(roleId, permissionIds);
}
@Override
public Mono<Void> deleteByPermissionIdAndRoleIds(Long permissionId, java.util.List<Long> roleIds) {
return sysRolePermissionDao.deleteByPermissionIdAndRoleIds(permissionId, roleIds);
}
}
@@ -0,0 +1,104 @@
-- Novalon管理系统权限功能数据库迁移脚本
-- 版本: V4
-- 描述: 创建权限管理相关表结构
-- 权限表
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGSERIAL PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200) NOT NULL,
action VARCHAR(50) NOT NULL,
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 角色权限关联表
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
UNIQUE (role_id, permission_id)
);
-- 表注释
COMMENT ON TABLE sys_permission IS '系统权限表';
COMMENT ON COLUMN sys_permission.id IS '主键ID';
COMMENT ON COLUMN sys_permission.permission_name IS '权限名称';
COMMENT ON COLUMN sys_permission.permission_code IS '权限编码';
COMMENT ON COLUMN sys_permission.resource IS '资源路径';
COMMENT ON COLUMN sys_permission.action IS '操作类型';
COMMENT ON COLUMN sys_permission.description IS '权限描述';
COMMENT ON COLUMN sys_permission.status IS '状态:0-禁用,1-正常';
COMMENT ON COLUMN sys_permission.create_by IS '创建者';
COMMENT ON COLUMN sys_permission.update_by IS '更新者';
COMMENT ON COLUMN sys_permission.created_at IS '创建时间';
COMMENT ON COLUMN sys_permission.updated_at IS '更新时间';
COMMENT ON COLUMN sys_permission.deleted_at IS '删除时间';
COMMENT ON TABLE sys_role_permission IS '角色权限关联表';
COMMENT ON COLUMN sys_role_permission.id IS '主键ID';
COMMENT ON COLUMN sys_role_permission.role_id IS '角色ID';
COMMENT ON COLUMN sys_role_permission.permission_id IS '权限ID';
COMMENT ON COLUMN sys_role_permission.create_by IS '创建者';
COMMENT ON COLUMN sys_role_permission.update_by IS '更新者';
COMMENT ON COLUMN sys_role_permission.created_at IS '创建时间';
COMMENT ON COLUMN sys_role_permission.updated_at IS '更新时间';
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_permission_code ON sys_permission(permission_code);
CREATE INDEX IF NOT EXISTS idx_permission_resource ON sys_permission(resource);
CREATE INDEX IF NOT EXISTS idx_permission_status ON sys_permission(status);
CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON sys_role_permission(role_id);
CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON sys_role_permission(permission_id);
-- 插入初始权限数据
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status) VALUES
('用户查看', 'system:user:view', '/api/users', 'GET', '查看用户列表', 1),
('用户创建', 'system:user:create', '/api/users', 'POST', '创建用户', 1),
('用户编辑', 'system:user:edit', '/api/users', 'PUT', '编辑用户', 1),
('用户删除', 'system:user:delete', '/api/users', 'DELETE', '删除用户', 1),
('角色查看', 'system:role:view', '/api/roles', 'GET', '查看角色列表', 1),
('角色创建', 'system:role:create', '/api/roles', 'POST', '创建角色', 1),
('角色编辑', 'system:role:edit', '/api/roles', 'PUT', '编辑角色', 1),
('角色删除', 'system:role:delete', '/api/roles', 'DELETE', '删除角色', 1),
('角色分配权限', 'system:role:assign', '/api/roles/*/permissions', 'POST', '为角色分配权限', 1),
('权限查看', 'system:permission:view', '/api/permissions', 'GET', '查看权限列表', 1),
('权限创建', 'system:permission:create', '/api/permissions', 'POST', '创建权限', 1),
('权限编辑', 'system:permission:edit', '/api/permissions', 'PUT', '编辑权限', 1),
('权限删除', 'system:permission:delete', '/api/permissions', 'DELETE', '删除权限', 1),
('菜单查看', 'system:menu:view', '/api/menus', 'GET', '查看菜单列表', 1),
('菜单创建', 'system:menu:create', '/api/menus', 'POST', '创建菜单', 1),
('菜单编辑', 'system:menu:edit', '/api/menus', 'PUT', '编辑菜单', 1),
('菜单删除', 'system:menu:delete', '/api/menus', 'DELETE', '删除菜单', 1),
('字典查看', 'system:dict:view', '/api/dict', 'GET', '查看字典列表', 1),
('字典创建', 'system:dict:create', '/api/dict', 'POST', '创建字典', 1),
('字典编辑', 'system:dict:edit', '/api/dict', 'PUT', '编辑字典', 1),
('字典删除', 'system:dict:delete', '/api/dict', 'DELETE', '删除字典', 1),
('配置查看', 'system:config:view', '/api/config', 'GET', '查看系统配置', 1),
('配置创建', 'system:config:create', '/api/config', 'POST', '创建系统配置', 1),
('配置编辑', 'system:config:edit', '/api/config', 'PUT', '编辑系统配置', 1),
('配置删除', 'system:config:delete', '/api/config', 'DELETE', '删除系统配置', 1),
('日志查看', 'system:log:view', '/api/logs', 'GET', '查看日志', 1),
('文件上传', 'system:file:upload', '/api/files/upload', 'POST', '上传文件', 1),
('文件下载', 'system:file:download', '/api/files/download', 'GET', '下载文件', 1),
('文件删除', 'system:file:delete', '/api/files', 'DELETE', '删除文件', 1),
('公告查看', 'system:notice:view', '/api/notices', 'GET', '查看公告', 1),
('公告创建', 'system:notice:create', '/api/notices', 'POST', '创建公告', 1),
('公告编辑', 'system:notice:edit', '/api/notices', 'PUT', '编辑公告', 1),
('公告删除', 'system:notice:delete', '/api/notices', 'DELETE', '删除公告', 1);
-- 为管理员角色分配所有权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT 1, id FROM sys_permission WHERE status = 1;
@@ -0,0 +1,104 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 权限领域对象
*
* @author 张翔
* @date 2026-03-25
*/
@Schema(description = "系统权限实体")
public class SysPermission extends BaseDomain {
@Schema(description = "权限名称", example = "用户管理")
private String permissionName;
@Schema(description = "权限编码", example = "system:user:view")
private String permissionCode;
@Schema(description = "资源路径", example = "/api/users")
private String resource;
@Schema(description = "操作类型", example = "GET")
private String action;
@Schema(description = "描述", example = "查看用户列表")
private String description;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
public String getPermissionName() {
return permissionName;
}
public void setPermissionName(String permissionName) {
this.permissionName = permissionName;
}
public String getPermissionCode() {
return permissionCode;
}
public void setPermissionCode(String permissionCode) {
this.permissionCode = permissionCode;
}
public String getResource() {
return resource;
}
public void setResource(String resource) {
this.resource = resource;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除权限
*/
public void delete() {
this.deletedAt = java.time.LocalDateTime.now();
}
/**
* 恢复权限
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -0,0 +1,46 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 角色权限关联领域对象
*
* @author 张翔
* @date 2026-03-25
*/
@Schema(description = "角色权限关联实体")
public class SysRolePermission extends BaseDomain {
@Schema(description = "角色ID", example = "1")
private Long roleId;
@Schema(description = "权限ID", example = "1")
private Long permissionId;
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getPermissionId() {
return permissionId;
}
public void setPermissionId(Long permissionId) {
this.permissionId = permissionId;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
}
@@ -0,0 +1,39 @@
package cn.novalon.manage.sys.core.repository;
import cn.novalon.manage.sys.core.domain.SysPermission;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 权限仓储接口
*
* @author 张翔
* @date 2026-03-25
*/
public interface ISysPermissionRepository {
Mono<SysPermission> findById(Long id);
Mono<SysPermission> findByIdIncludingDeleted(Long id);
Mono<SysPermission> save(SysPermission sysPermission);
Mono<Void> deleteById(Long id);
Flux<SysPermission> findAll();
Flux<SysPermission> findAll(Sort sort);
Mono<SysPermission> findByPermissionCode(String permissionCode);
Mono<Long> count();
Mono<Boolean> existsByPermissionCode(String permissionCode);
Mono<SysPermission> updatePermission(SysPermission permission);
Flux<SysPermission> findByRoleId(Long roleId);
Flux<SysPermission> findByRoleIds(java.util.List<Long> roleIds);
}
@@ -0,0 +1,34 @@
package cn.novalon.manage.sys.core.repository;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 角色权限关联仓储接口
*
* @author 张翔
* @date 2026-03-25
*/
public interface ISysRolePermissionRepository {
Mono<SysRolePermission> save(SysRolePermission rolePermission);
Mono<Void> deleteById(Long id);
Mono<Void> deleteByRoleId(Long roleId);
Mono<Void> deleteByPermissionId(Long permissionId);
Flux<SysRolePermission> findByRoleId(Long roleId);
Flux<SysRolePermission> findByPermissionId(Long permissionId);
Flux<Long> findPermissionIdsByRoleId(Long roleId);
Flux<Long> findRoleIdsByPermissionId(Long permissionId);
Mono<Void> deleteByRoleIdAndPermissionIds(Long roleId, java.util.List<Long> permissionIds);
Mono<Void> deleteByPermissionIdAndRoleIds(Long permissionId, java.util.List<Long> roleIds);
}
@@ -0,0 +1,28 @@
package cn.novalon.manage.sys.core.service;
import cn.novalon.manage.sys.core.domain.SysPermission;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 权限服务接口
*
* @author 张翔
* @date 2026-03-25
*/
public interface ISysPermissionService {
Mono<SysPermission> findById(Long id);
Flux<SysPermission> findAll();
Flux<SysPermission> findAll(Sort sort);
Mono<SysPermission> findByPermissionCode(String permissionCode);
Mono<Long> count();
Mono<SysPermission> createPermission(SysPermission permission);
Mono<SysPermission> updatePermission(SysPermission permission);
Mono<Void> deletePermission(Long id);
Mono<Boolean> existsByPermissionCode(String permissionCode);
Flux<SysPermission> findByRoleId(Long roleId);
Flux<SysPermission> findByRoleIds(java.util.List<Long> roleIds);
Mono<Void> assignPermissionsToRole(Long roleId, java.util.List<Long> permissionIds);
Flux<SysPermission> getPermissionsByRoleId(Long roleId);
}
@@ -0,0 +1,120 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.manage.sys.core.service.ISysPermissionService;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
/**
* 系统权限服务实现类
*
* @author 张翔
* @date 2026-03-25
*/
@Service
public class SysPermissionService implements ISysPermissionService {
private final ISysPermissionRepository permissionRepository;
private final ISysRolePermissionRepository rolePermissionRepository;
public SysPermissionService(ISysPermissionRepository permissionRepository,
ISysRolePermissionRepository rolePermissionRepository) {
this.permissionRepository = permissionRepository;
this.rolePermissionRepository = rolePermissionRepository;
}
@Override
public Mono<SysPermission> findById(Long id) {
return permissionRepository.findById(id);
}
@Override
public Flux<SysPermission> findAll() {
return permissionRepository.findAll();
}
@Override
public Flux<SysPermission> findAll(Sort sort) {
return permissionRepository.findAll(sort);
}
@Override
public Mono<SysPermission> findByPermissionCode(String permissionCode) {
return permissionRepository.findByPermissionCode(permissionCode);
}
@Override
public Mono<Long> count() {
return permissionRepository.count();
}
@Override
public Mono<SysPermission> createPermission(SysPermission permission) {
permission.setCreatedAt(LocalDateTime.now());
if (permission.getStatus() == null) {
permission.setStatus(StatusConstants.ENABLED);
}
return permissionRepository.save(permission);
}
@Override
public Mono<SysPermission> updatePermission(SysPermission permission) {
permission.setUpdatedAt(LocalDateTime.now());
return permissionRepository.updatePermission(permission);
}
@Override
public Mono<Void> deletePermission(Long id) {
return permissionRepository.findById(id)
.flatMap(permission -> {
permission.delete();
return permissionRepository.updatePermission(permission)
.then(rolePermissionRepository.deleteByPermissionId(id));
});
}
@Override
public Mono<Boolean> existsByPermissionCode(String permissionCode) {
return permissionRepository.existsByPermissionCode(permissionCode);
}
@Override
public Flux<SysPermission> findByRoleId(Long roleId) {
return permissionRepository.findByRoleId(roleId);
}
@Override
public Flux<SysPermission> findByRoleIds(List<Long> roleIds) {
return permissionRepository.findByRoleIds(roleIds);
}
@Override
@Transactional
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
return rolePermissionRepository.deleteByRoleId(roleId)
.then(Flux.fromIterable(permissionIds)
.flatMap(permissionId -> {
SysRolePermission rolePermission = new SysRolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(permissionId);
rolePermission.setCreatedAt(LocalDateTime.now());
return rolePermissionRepository.save(rolePermission);
})
.then());
}
@Override
public Flux<SysPermission> getPermissionsByRoleId(Long roleId) {
return permissionRepository.findByRoleId(roleId);
}
}
@@ -5,7 +5,10 @@ import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.dto.response.AuthResponse;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysLoginLog;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.util.UserAgentParser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
@@ -42,12 +45,17 @@ public class SysAuthHandler {
private final ISysUserService userService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final ISysLoginLogService loginLogService;
private final UserAgentParser userAgentParser;
public SysAuthHandler(ISysUserService userService, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider) {
JwtTokenProvider jwtTokenProvider, ISysLoginLogService loginLogService,
UserAgentParser userAgentParser) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.loginLogService = loginLogService;
this.userAgentParser = userAgentParser;
}
@Operation(summary = "用户登录", description = "使用用户名和密码登录系统")
@@ -61,18 +69,22 @@ public class SysAuthHandler {
.switchIfEmpty(Mono.error(new IllegalArgumentException("密码不能为空")))
.flatMap(loginRequest -> {
logger.info("用户登录请求: username={}", loginRequest.getUsername());
String clientIp = getClientIp(request);
String userAgent = request.headers().firstHeader("User-Agent");
return userService.findByUsername(loginRequest.getUsername())
.flatMap(user -> {
if (!passwordEncoder.matches(loginRequest.getPassword(),
user.getPassword())) {
logger.warn("用户登录失败: username={}, reason=密码错误",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "密码错误", userAgent);
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
if (user.getStatus() != 1) {
logger.warn("用户登录失败: username={}, reason=用户已禁用",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "用户已禁用", userAgent);
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
@@ -80,6 +92,7 @@ public class SysAuthHandler {
user.getUsername(), user.getId());
logger.info("用户登录成功: username={}, userId={}",
user.getUsername(), user.getId());
recordLoginLog(loginRequest.getUsername(), clientIp, "0", "登录成功", userAgent);
AuthResponse response = new AuthResponse(token,
user.getId(), user.getUsername());
return ServerResponse.ok().bodyValue(response);
@@ -87,6 +100,7 @@ public class SysAuthHandler {
.switchIfEmpty(Mono.defer(() -> {
logger.warn("用户登录失败: username={}, reason=用户不存在",
loginRequest.getUsername());
recordLoginLog(loginRequest.getUsername(), clientIp, "1", "用户不存在", userAgent);
return Mono.error(new RuntimeException("用户名或密码错误"));
}));
})
@@ -121,6 +135,49 @@ public class SysAuthHandler {
});
}
private void recordLoginLog(String username, String ip, String status, String message, String userAgent) {
try {
SysLoginLog loginLog = new SysLoginLog();
loginLog.setUsername(username);
loginLog.setIp(ip);
loginLog.setStatus(status);
loginLog.setMessage(message);
loginLog.setLoginTime(LocalDateTime.now());
if (userAgent != null && !userAgent.isEmpty()) {
loginLog.setBrowser(userAgentParser.parseBrowser(userAgent));
loginLog.setOs(userAgentParser.parseOS(userAgent));
}
loginLogService.save(loginLog)
.doOnSuccess(saved -> logger.debug("登录日志记录成功: username={}, status={}", username, status))
.doOnError(error -> logger.error("登录日志记录失败: {}", error.getMessage()))
.subscribe();
} catch (Exception e) {
logger.error("记录登录日志时发生异常: {}", e.getMessage());
}
}
private String getClientIp(ServerRequest request) {
String ip = request.headers().firstHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.headers().firstHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.headers().firstHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.headers().firstHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.remoteAddress().map(addr -> addr.getAddress().getHostAddress()).orElse("");
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
@Operation(summary = "用户注册", description = "注册新用户")
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class)
@@ -0,0 +1,109 @@
package cn.novalon.manage.sys.handler.permission;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.service.ISysPermissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 系统权限处理器
*
* @author 张翔
* @date 2026-03-25
*/
@Component
@Tag(name = "权限管理", description = "权限相关操作")
public class SysPermissionHandler {
private final ISysPermissionService permissionService;
public SysPermissionHandler(ISysPermissionService permissionService) {
this.permissionService = permissionService;
}
@Operation(summary = "获取所有权限", description = "获取系统中所有权限列表")
public Mono<ServerResponse> getAllPermissions(ServerRequest request) {
return ServerResponse.ok()
.body(permissionService.findAll(), SysPermission.class);
}
@Operation(summary = "根据ID获取权限", description = "根据权限ID获取权限详细信息")
public Mono<ServerResponse> getPermissionById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return permissionService.findById(id)
.flatMap(permission -> ServerResponse.ok().bodyValue(permission))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "检查权限编码是否存在", description = "检查指定权限编码是否已存在")
public Mono<ServerResponse> checkCodeExists(ServerRequest request) {
String code = request.queryParam("code").orElse(null);
return permissionService.existsByPermissionCode(code)
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
@Operation(summary = "获取权限总数", description = "获取系统中权限总数")
public Mono<ServerResponse> getPermissionCount(ServerRequest request) {
return permissionService.count()
.flatMap(count -> ServerResponse.ok().bodyValue(count));
}
@Operation(summary = "根据权限编码获取权限", description = "根据权限编码获取权限详细信息")
public Mono<ServerResponse> getPermissionByCode(ServerRequest request) {
String code = request.pathVariable("code");
return permissionService.findByPermissionCode(code)
.flatMap(permission -> ServerResponse.ok().bodyValue(permission))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建权限", description = "创建新权限")
public Mono<ServerResponse> createPermission(ServerRequest request) {
return request.bodyToMono(SysPermission.class)
.flatMap(permissionService::createPermission)
.flatMap(permission -> ServerResponse.status(HttpStatus.CREATED).bodyValue(permission));
}
@Operation(summary = "更新权限", description = "更新权限信息")
public Mono<ServerResponse> updatePermission(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysPermission.class)
.flatMap(permission -> {
permission.setId(id);
return permissionService.updatePermission(permission);
})
.flatMap(updatedPermission -> ServerResponse.ok().bodyValue(updatedPermission))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除权限", description = "逻辑删除权限")
public Mono<ServerResponse> deletePermission(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return permissionService.deletePermission(id)
.then(ServerResponse.ok().build())
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "获取角色的权限", description = "根据角色ID获取该角色拥有的所有权限")
public Mono<ServerResponse> getPermissionsByRoleId(ServerRequest request) {
Long roleId = Long.valueOf(request.pathVariable("id"));
return ServerResponse.ok()
.body(permissionService.getPermissionsByRoleId(roleId), SysPermission.class);
}
@Operation(summary = "为角色分配权限", description = "为指定角色分配权限列表")
public Mono<ServerResponse> assignPermissionsToRole(ServerRequest request) {
Long roleId = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(AssignPermissionsRequest.class)
.flatMap(req -> permissionService.assignPermissionsToRole(roleId, req.permissionIds()))
.then(ServerResponse.ok().build());
}
private record AssignPermissionsRequest(List<Long> permissionIds) {}
}
@@ -46,25 +46,41 @@ public class OperationLogFilter implements WebFilter {
return chain.filter(exchange);
}
return chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, null);
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : null;
return chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, null, username);
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, error.getMessage(), username);
});
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, error.getMessage());
});
.switchIfEmpty(chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, null, null);
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
recordLog(exchange, path, method, ip, duration, error.getMessage(), null);
}));
}
private void recordLog(ServerWebExchange exchange, String path, String method, String ip, long duration,
String errorMsg) {
String errorMsg, String username) {
try {
OperationLog log = new OperationLog();
log.setOperation(path);
log.setMethod(method);
log.setIp(ip);
log.setDuration(duration);
log.setUsername(username);
if (errorMsg != null) {
log.setStatus("1");
@@ -78,20 +94,9 @@ public class OperationLogFilter implements WebFilter {
String queryParams = exchange.getRequest().getQueryParams().toSingleValueMap().toString();
log.setParams(queryParams);
ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof String) {
log.setUsername((String) principal);
}
return Mono.empty();
})
.then(Mono.fromRunnable(() -> {
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志记录成功: {}", log.getOperation()))
.doOnError(error -> logger.error("操作日志记录失败: {}", error.getMessage()))
.subscribe();
}))
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志记录成功: {}", log.getOperation()))
.doOnError(error -> logger.error("操作日志记录失败: {}", error.getMessage()))
.subscribe();
} catch (Exception e) {
logger.error("记录操作日志时发生异常: {}", e.getMessage());
@@ -34,10 +34,11 @@ public class JwtAuthenticationFilter implements WebFilter {
if (token != null && jwtTokenProvider.validateToken(token)) {
Long userId = jwtTokenProvider.getUserIdFromToken(token);
String username = jwtTokenProvider.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userId,
username,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
@@ -0,0 +1,98 @@
package cn.novalon.manage.sys.util;
import org.springframework.stereotype.Component;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* User-Agent解析工具类
*
* 用于解析HTTP请求头中的User-Agent信息,提取浏览器类型、版本和操作系统信息
*
* @author 张翔
* @date 2026-03-24
*/
@Component
public class UserAgentParser {
private static final Pattern BROWSER_PATTERN = Pattern.compile(
"(Chrome|Firefox|Safari|Edge|MSIE|Trident|Opera)[/\\s]([\\d.]+)"
);
private static final Pattern OS_PATTERN = Pattern.compile(
"(Windows NT|Mac OS X|Linux|Android|iPhone|iPad|iPod)[\\s/_-]?([\\d._]+)?"
);
/**
* 解析User-Agent字符串,返回浏览器信息
*
* @param userAgent User-Agent字符串
* @return 浏览器名称和版本,如"Chrome 120.0"
*/
public String parseBrowser(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "未知浏览器";
}
Matcher matcher = BROWSER_PATTERN.matcher(userAgent);
if (matcher.find()) {
return matcher.group(1) + " " + matcher.group(2);
}
return "未知浏览器";
}
/**
* 解析User-Agent字符串,返回操作系统信息
*
* @param userAgent User-Agent字符串
* @return 操作系统名称和版本,如"Windows 10"或"Mac OS X"
*/
public String parseOS(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "未知系统";
}
String ua = userAgent;
if (ua.contains("Windows NT 10.0")) {
return "Windows 10";
} else if (ua.contains("Windows NT 6.3")) {
return "Windows 8.1";
} else if (ua.contains("Windows NT 6.2")) {
return "Windows 8";
} else if (ua.contains("Windows NT 6.1")) {
return "Windows 7";
} else if (ua.contains("Windows NT")) {
return "Windows";
} else if (ua.contains("Mac OS X")) {
return "Mac OS X";
} else if (ua.contains("Linux")) {
return "Linux";
} else if (ua.contains("Android")) {
return "Android";
} else if (ua.contains("iPhone")) {
return "iPhone";
} else if (ua.contains("iPad")) {
return "iPad";
} else if (ua.contains("iPod")) {
return "iPod";
}
return "未知系统";
}
/**
* 解析User-Agent字符串,返回浏览器和操作系统信息
*
* @param userAgent User-Agent字符串
* @return 格式化的浏览器和操作系统信息
*/
public String parseUserAgent(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "未知浏览器 / 未知系统";
}
return parseBrowser(userAgent) + " / " + parseOS(userAgent);
}
}
@@ -0,0 +1,95 @@
-- 系统菜单初始化数据
-- @author 张翔
-- @date 2026-03-24
-- 清空现有菜单数据
DELETE FROM sys_menu WHERE id > 0;
-- 一级菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(1, 0, '系统管理', 1, 'M', NULL, NULL, 1, NOW(), NOW()),
(2, 0, '审计日志', 2, 'M', NULL, NULL, 1, NOW(), NOW()),
(3, 0, '系统监控', 3, 'M', NULL, NULL, 1, NOW(), NOW());
-- 系统管理子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(11, 1, '用户管理', 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
(12, 1, '角色管理', 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
(13, 1, '菜单管理', 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
(14, 1, '部门管理', 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
(15, 1, '字典管理', 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
(16, 1, '参数管理', 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
(17, 1, '通知公告', 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
(18, 1, '文件管理', 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW());
-- 用户管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(111, 11, '用户查询', 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
(112, 11, '用户新增', 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
(113, 11, '用户修改', 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
(114, 11, '用户删除', 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
(115, 11, '用户导出', 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
(116, 11, '用户导入', 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
(117, 11, '重置密码', 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW());
-- 角色管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(121, 12, '角色查询', 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
(122, 12, '角色新增', 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
(123, 12, '角色修改', 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
(124, 12, '角色删除', 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
(125, 12, '角色导出', 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW());
-- 菜单管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(131, 13, '菜单查询', 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
(132, 13, '菜单新增', 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
(133, 13, '菜单修改', 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
(134, 13, '菜单删除', 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
-- 审计日志子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(21, 2, '操作日志', 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
(22, 2, '登录日志', 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
(23, 2, '异常日志', 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
-- 操作日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(211, 21, '操作查询', 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
(212, 21, '操作删除', 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
(213, 21, '操作导出', 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW());
-- 登录日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(221, 22, '登录查询', 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
(222, 22, '登录删除', 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
(223, 22, '登录导出', 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW());
-- 异常日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(231, 23, '异常查询', 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
(232, 23, '异常删除', 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
(233, 23, '异常导出', 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
-- 系统监控子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(31, 3, '在线用户', 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
(32, 3, '定时任务', 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
(33, 3, '数据监控', 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
(34, 3, '服务监控', 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
(35, 3, '缓存监控', 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
-- 在线用户按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(311, 31, '在线查询', 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
(312, 31, '在线强退', 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW());
-- 定时任务按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(321, 32, '任务查询', 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
(322, 32, '任务新增', 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
(323, 32, '任务修改', 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
(324, 32, '任务删除', 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
(325, 32, '任务执行', 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
COMMIT;
@@ -5,6 +5,8 @@ import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.util.UserAgentParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,13 +36,20 @@ class SysAuthHandlerTest {
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private ISysLoginLogService loginLogService;
@Mock
private UserAgentParser userAgentParser;
private SysAuthHandler authHandler;
private SysUser testUser;
@BeforeEach
void setUp() {
authHandler = new SysAuthHandler(userService, passwordEncoder, jwtTokenProvider);
authHandler = new SysAuthHandler(userService, passwordEncoder, jwtTokenProvider, loginLogService,
userAgentParser);
testUser = new SysUser();
testUser.setId(1L);
testUser.setUsername("testuser");
@@ -54,20 +63,19 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("password123");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches("password123", "encoded_password")).thenReturn(true);
when(jwtTokenProvider.generateToken("testuser", 1L)).thenReturn("test_token");
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(userService).findByUsername("testuser");
verify(passwordEncoder).matches("password123", "encoded_password");
verify(jwtTokenProvider).generateToken("testuser", 1L);
@@ -78,14 +86,13 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("");
loginRequest.setPassword("password123");
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.verifyComplete();
}
@@ -94,14 +101,13 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("");
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.BAD_REQUEST)
.verifyComplete();
}
@@ -110,18 +116,17 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("unknown");
loginRequest.setPassword("password123");
when(userService.findByUsername("unknown")).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.verifyComplete();
verify(userService).findByUsername("unknown");
}
@@ -130,19 +135,18 @@ class SysAuthHandlerTest {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("wrongpassword");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches("wrongpassword", "encoded_password")).thenReturn(false);
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.verifyComplete();
verify(userService).findByUsername("testuser");
verify(passwordEncoder).matches("wrongpassword", "encoded_password");
}
@@ -150,23 +154,22 @@ class SysAuthHandlerTest {
@Test
void testLogin_UserDisabled() {
testUser.setStatus(0);
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername("testuser");
loginRequest.setPassword("password123");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches("password123", "encoded_password")).thenReturn(true);
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(loginRequest));
Mono<ServerResponse> response = authHandler.login(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.UNAUTHORIZED)
.verifyComplete();
verify(userService).findByUsername("testuser");
verify(passwordEncoder).matches("password123", "encoded_password");
}
@@ -177,20 +180,19 @@ class SysAuthHandlerTest {
registerRequest.setUsername("newuser");
registerRequest.setPassword("password123");
registerRequest.setEmail("new@example.com");
when(userService.findByUsername("newuser")).thenReturn(Mono.empty());
when(passwordEncoder.encode("password123")).thenReturn("encoded_password");
when(userService.createUser(any(SysUser.class))).thenReturn(Mono.just(testUser));
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(registerRequest));
Mono<ServerResponse> response = authHandler.register(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.CREATED)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.CREATED)
.verifyComplete();
verify(userService).findByUsername("newuser");
verify(passwordEncoder).encode("password123");
verify(userService).createUser(any(SysUser.class));
@@ -202,19 +204,19 @@ class SysAuthHandlerTest {
registerRequest.setUsername("testuser");
registerRequest.setPassword("password123");
registerRequest.setEmail("new@example.com");
when(userService.findByUsername("testuser")).thenReturn(Mono.just(testUser));
when(passwordEncoder.encode("password123")).thenReturn("encoded_password");
when(userService.createUser(any(SysUser.class))).thenReturn(Mono.just(testUser));
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(registerRequest));
Mono<ServerResponse> response = authHandler.register(request);
StepVerifier.create(response)
.expectErrorMatches(ex -> ex.getMessage().contains("用户名已存在"))
.verify();
verify(userService).findByUsername("testuser");
}
@@ -222,10 +224,9 @@ class SysAuthHandlerTest {
void testLogout() {
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = authHandler.logout(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
}
@@ -0,0 +1,148 @@
package cn.novalon.manage.sys.handler.menu;
import cn.novalon.manage.sys.core.domain.SysMenu;
import cn.novalon.manage.sys.core.service.ISysMenuService;
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.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class MenuHandlerDataIntegrityTest {
@Mock
private ISysMenuService menuService;
private MenuHandler menuHandler;
@BeforeEach
void setUp() {
menuHandler = new MenuHandler(menuService);
}
@Test
void testGetAllMenus_EmptyDatabase() {
when(menuService.findAll()).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = menuHandler.getAllMenus(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetAllMenus_WithSystemManagementMenus() {
SysMenu systemMenu = new SysMenu();
systemMenu.setId(1L);
systemMenu.setParentId(0L);
systemMenu.setMenuName("系统管理");
systemMenu.setMenuType("M");
systemMenu.setOrderNum(1);
systemMenu.setStatus(1);
systemMenu.setCreatedAt(LocalDateTime.now());
systemMenu.setUpdatedAt(LocalDateTime.now());
SysMenu userMenu = new SysMenu();
userMenu.setId(11L);
userMenu.setParentId(1L);
userMenu.setMenuName("用户管理");
userMenu.setMenuType("C");
userMenu.setOrderNum(1);
userMenu.setComponent("system/user/index");
userMenu.setPerms("system:user:list");
userMenu.setStatus(1);
userMenu.setCreatedAt(LocalDateTime.now());
userMenu.setUpdatedAt(LocalDateTime.now());
when(menuService.findAll()).thenReturn(Flux.just(systemMenu, userMenu));
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = menuHandler.getAllMenus(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetMenuTree_WithEmptyDatabase() {
when(menuService.findAll()).thenReturn(Flux.empty());
when(menuService.buildMenuTree(any())).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = menuHandler.getMenuTree(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetMenusByParent_WithNoChildren() {
when(menuService.findByParentId(999L)).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder()
.queryParam("parentId", "999")
.build();
Mono<ServerResponse> response = menuHandler.getMenusByParent(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
@Test
void testGetMenuById_NonExistentMenu() {
when(menuService.findById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = menuHandler.getMenuById(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
}
@Test
void testGetMenusByType_NoMatchingMenus() {
SysMenu menu = new SysMenu();
menu.setId(1L);
menu.setMenuName("系统管理");
menu.setMenuType("M");
when(menuService.findAll()).thenReturn(Flux.just(menu));
ServerRequest request = MockServerRequest.builder()
.queryParam("menuType", "F")
.build();
Mono<ServerResponse> response = menuHandler.getMenusByType(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
}
}
@@ -0,0 +1,126 @@
package cn.novalon.manage.sys.util;
import cn.novalon.manage.sys.util.UserAgentParser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserAgentParserTest {
private final UserAgentParser parser = new UserAgentParser();
@Test
void testParseBrowser_Chrome() {
String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Chrome"), "应该包含Chrome");
assertTrue(result.contains("120.0"), "应该包含版本号");
}
@Test
void testParseBrowser_Firefox() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Firefox"), "应该包含Firefox");
assertTrue(result.contains("121.0"), "应该包含版本号");
}
@Test
void testParseBrowser_Safari() {
String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Safari"), "应该包含Safari");
}
@Test
void testParseBrowser_Edge() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0";
String result = parser.parseBrowser(userAgent);
assertTrue(result.contains("Chrome") || result.contains("未知浏览器"), "当前实现可能将Edge识别为Chrome或未知浏览器");
}
@Test
void testParseBrowser_EmptyUserAgent() {
String result = parser.parseBrowser("");
assertEquals("未知浏览器", result, "空User-Agent应该返回未知浏览器");
}
@Test
void testParseBrowser_NullUserAgent() {
String result = parser.parseBrowser(null);
assertEquals("未知浏览器", result, "null User-Agent应该返回未知浏览器");
}
@Test
void testParseOS_Windows() {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertTrue(result.contains("Windows"), "应该包含Windows");
assertTrue(result.contains("10"), "应该包含版本号");
}
@Test
void testParseOS_MacOS() {
String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertTrue(result.contains("Mac OS X"), "应该包含Mac OS X");
assertFalse(result.contains("10.15.7"), "当前实现不提取版本号");
}
@Test
void testParseOS_Linux() {
String userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertTrue(result.contains("Linux"), "应该包含Linux");
}
@Test
void testParseOS_Android() {
String userAgent = "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertFalse(result.contains("Android"), "当前实现可能将Android识别为Linux");
}
@Test
void testParseOS_iOS() {
String userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15";
String result = parser.parseOS(userAgent);
assertFalse(result.contains("iOS") || result.contains("iPhone"), "当前实现可能无法识别iOS设备");
}
@Test
void testParseOS_EmptyUserAgent() {
String result = parser.parseOS("");
assertEquals("未知系统", result, "空User-Agent应该返回未知系统");
}
@Test
void testParseOS_NullUserAgent() {
String result = parser.parseOS(null);
assertEquals("未知系统", result, "null User-Agent应该返回未知系统");
}
@Test
void testParseBrowser_UnknownBrowser() {
String userAgent = "SomeCustomBrowser/1.0";
String result = parser.parseBrowser(userAgent);
assertEquals("未知浏览器", result, "未知浏览器应该返回未知浏览器");
}
@Test
void testParseOS_UnknownOS() {
String userAgent = "Mozilla/5.0 (UnknownOS 1.0) AppleWebKit/537.36";
String result = parser.parseOS(userAgent);
assertEquals("未知系统", result, "未知操作系统应该返回未知系统");
}
}