feat: 完善系统配置审计通知功能并优化异常处理

- 新增异常处理体系(BaseException及其子类)
- 优化密码、邮箱、用户名等基础类型
- 添加字典管理、登录日志、操作日志的E2E测试
- 完善API集成测试和安全测试
- 添加性能测试配置和脚本
- 优化OpenAPI配置和全局异常处理器
This commit is contained in:
张翔
2026-03-24 14:05:35 +08:00
parent be5d5ede90
commit e4721053bd
47 changed files with 3006 additions and 816 deletions
@@ -1,5 +1,8 @@
package cn.novalon.manage.sys.core.command;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.ValidationException;
/**
* 创建公告命令对象
*
@@ -20,19 +23,19 @@ public record CreateNoticeCommand(
private static void validateNoticeTitle(String noticeTitle) {
if (noticeTitle == null || noticeTitle.trim().isEmpty()) {
throw new IllegalArgumentException("Notice title is required");
throw new ValidationException(ErrorCode.VALIDATION_REQUIRED, "Notice title is required");
}
}
private static void validateNoticeContent(String noticeContent) {
if (noticeContent == null || noticeContent.trim().isEmpty()) {
throw new IllegalArgumentException("Notice content is required");
throw new ValidationException(ErrorCode.VALIDATION_REQUIRED, "Notice content is required");
}
}
private static void validateNoticeType(String noticeType) {
if (noticeType != null && !noticeType.equals("1") && !noticeType.equals("2")) {
throw new IllegalArgumentException(
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE,
"Invalid notice type. Notice type must be 1 (notification) or 2 (announcement)");
}
}
@@ -1,5 +1,7 @@
package cn.novalon.manage.sys.core.command;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.ValidationException;
import cn.novalon.manage.common.util.StatusConstants;
/**
@@ -21,7 +23,8 @@ public record CreateRoleCommand(
private static void validateStatus(Integer status) {
if (status != null && status != StatusConstants.ENABLED && status != StatusConstants.DISABLED) {
throw new IllegalArgumentException("Invalid status value. Status must be 0 (disabled) or 1 (enabled)");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE,
"Invalid status value. Status must be 0 (disabled) or 1 (enabled)");
}
}
}
@@ -1,6 +1,7 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@@ -10,11 +11,19 @@ import java.time.LocalDateTime;
* @author 张翔
* @date 2026-03-13
*/
@Schema(description = "系统角色实体")
public class SysRole extends BaseDomain {
@Schema(description = "角色名称", example = "管理员")
private String roleName;
@Schema(description = "角色权限字符串", example = "admin")
private String roleKey;
@Schema(description = "显示顺序", example = "1")
private Integer roleSort;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
public String getRoleName() {
@@ -1,7 +1,7 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
/**
@@ -10,14 +10,28 @@ import java.time.LocalDateTime;
* @author 张翔
* @date 2026-03-13
*/
@Schema(description = "系统用户实体")
public class SysUser extends BaseDomain {
@Schema(description = "用户名", example = "admin")
private String username;
@Schema(description = "密码(加密后)", example = "$2a$10$...")
private String password;
@Schema(description = "昵称", example = "管理员")
private String nickname;
@Schema(description = "邮箱", example = "admin@example.com")
private String email;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "角色ID", example = "1")
private Long roleId;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
public String getUsername() {
@@ -1,5 +1,6 @@
package cn.novalon.manage.sys.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
/**
@@ -12,11 +13,14 @@ import jakarta.validation.constraints.NotBlank;
* @author 张翔
* @date 2026-03-13
*/
@Schema(description = "用户登录请求")
public class LoginRequest {
@Schema(description = "用户名", example = "admin")
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description = "密码", example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
@@ -1,5 +1,6 @@
package cn.novalon.manage.sys.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@@ -10,22 +11,28 @@ import jakarta.validation.constraints.Size;
* @author 张翔
* @date 2026-03-14
*/
@Schema(description = "用户注册请求")
public class UserRegisterRequest {
@Schema(description = "用户名", example = "testuser")
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
private String username;
@Schema(description = "昵称", example = "测试用户")
@Size(max = 100, message = "昵称长度不能超过100")
private String nickname;
@Schema(description = "密码", example = "123456")
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 100, message = "密码长度必须在6-100之间")
private String password;
@Schema(description = "邮箱", example = "test@example.com")
@Email(message = "邮箱格式不正确")
private String email;
@Schema(description = "手机号", example = "13800138000")
@Size(max = 20, message = "手机号长度不能超过20")
private String phone;
@@ -1,5 +1,6 @@
package cn.novalon.manage.sys.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
/**
@@ -8,14 +9,19 @@ import jakarta.validation.constraints.Email;
* @author 张翔
* @date 2026-03-14
*/
@Schema(description = "用户更新请求")
public class UserUpdateRequest {
@Schema(description = "邮箱", example = "newemail@example.com")
private String email;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
@Schema(description = "角色ID", example = "1")
private Long roleId;
@Schema(description = "是否清除角色关联", example = "false")
private Boolean clearRole;
@Email(message = "邮箱格式不正确")
@@ -1,15 +1,23 @@
package cn.novalon.manage.sys.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 认证响应DTO
*
* @author 张翔
* @date 2026-03-14
*/
@Schema(description = "用户认证响应")
public class AuthResponse {
@Schema(description = "JWT访问令牌", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String token;
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "用户名", example = "admin")
private String username;
public AuthResponse() {
@@ -6,6 +6,8 @@ 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.service.ISysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@@ -33,6 +35,7 @@ import java.util.stream.Collectors;
* @date 2026-03-13
*/
@Component
@Tag(name = "认证管理", description = "登录认证相关操作")
public class SysAuthHandler {
private static final Logger logger = LoggerFactory.getLogger(SysAuthHandler.class);
@@ -47,6 +50,7 @@ public class SysAuthHandler {
this.jwtTokenProvider = jwtTokenProvider;
}
@Operation(summary = "用户登录", description = "使用用户名和密码登录系统")
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(LoginRequest.class)
.filter(loginRequest -> loginRequest.getUsername() != null
@@ -117,6 +121,7 @@ public class SysAuthHandler {
});
}
@Operation(summary = "用户注册", description = "注册新用户")
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class)
.flatMap(registerRequest -> {
@@ -145,6 +150,7 @@ public class SysAuthHandler {
});
}
@Operation(summary = "用户登出", description = "用户登出系统")
public Mono<ServerResponse> logout(ServerRequest request) {
return ServerResponse.ok().build();
}
@@ -2,6 +2,8 @@ package cn.novalon.manage.sys.handler.config;
import cn.novalon.manage.sys.core.domain.SysConfig;
import cn.novalon.manage.sys.core.service.ISysConfigService;
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;
@@ -15,6 +17,7 @@ import reactor.core.publisher.Mono;
* @date 2026-03-14
*/
@Component
@Tag(name = "配置管理", description = "系统配置相关操作")
public class SysConfigHandler {
private final ISysConfigService configService;
@@ -23,11 +26,13 @@ public class SysConfigHandler {
this.configService = configService;
}
@Operation(summary = "获取所有配置", description = "获取系统中所有配置列表")
public Mono<ServerResponse> getAllConfigs(ServerRequest request) {
return ServerResponse.ok()
.body(configService.findAll(), SysConfig.class);
}
@Operation(summary = "根据ID获取配置", description = "根据配置ID获取配置详细信息")
public Mono<ServerResponse> getConfigById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return configService.findById(id)
@@ -35,6 +40,7 @@ public class SysConfigHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据键获取配置", description = "根据配置键获取配置详细信息")
public Mono<ServerResponse> getConfigByKey(ServerRequest request) {
String configKey = request.pathVariable("configKey");
return configService.findByConfigKey(configKey)
@@ -42,12 +48,14 @@ public class SysConfigHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建配置", description = "创建新配置")
public Mono<ServerResponse> createConfig(ServerRequest request) {
return request.bodyToMono(SysConfig.class)
.flatMap(configService::save)
.flatMap(config -> ServerResponse.status(HttpStatus.CREATED).bodyValue(config));
}
@Operation(summary = "更新配置", description = "更新配置信息")
public Mono<ServerResponse> updateConfig(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysConfig.class)
@@ -62,6 +70,7 @@ public class SysConfigHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除配置", description = "删除指定配置")
public Mono<ServerResponse> deleteConfig(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return configService.deleteById(id)
@@ -4,6 +4,8 @@ import cn.novalon.manage.sys.core.domain.SysDictType;
import cn.novalon.manage.sys.core.domain.SysDictData;
import cn.novalon.manage.sys.core.service.ISysDictTypeService;
import cn.novalon.manage.sys.core.service.ISysDictDataService;
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;
@@ -17,6 +19,7 @@ import reactor.core.publisher.Mono;
* @date 2026-03-14
*/
@Component
@Tag(name = "字典管理", description = "字典类型和字典数据相关操作")
public class SysDictHandler {
private final ISysDictTypeService dictTypeService;
@@ -27,11 +30,13 @@ public class SysDictHandler {
this.dictDataService = dictDataService;
}
@Operation(summary = "获取所有字典类型", description = "获取系统中所有字典类型列表")
public Mono<ServerResponse> getAllDictTypes(ServerRequest request) {
return ServerResponse.ok()
.body(dictTypeService.findAll(), SysDictType.class);
}
@Operation(summary = "根据ID获取字典类型", description = "根据字典类型ID获取详细信息")
public Mono<ServerResponse> getDictTypeById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return dictTypeService.findById(id)
@@ -39,6 +44,7 @@ public class SysDictHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据类型获取字典类型", description = "根据字典类型代码获取详细信息")
public Mono<ServerResponse> getDictTypeByType(ServerRequest request) {
String dictType = request.pathVariable("dictType");
return dictTypeService.findByDictType(dictType)
@@ -46,12 +52,14 @@ public class SysDictHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建字典类型", description = "创建新的字典类型")
public Mono<ServerResponse> createDictType(ServerRequest request) {
return request.bodyToMono(SysDictType.class)
.flatMap(dictTypeService::save)
.flatMap(dt -> ServerResponse.status(HttpStatus.CREATED).bodyValue(dt));
}
@Operation(summary = "更新字典类型", description = "更新字典类型信息")
public Mono<ServerResponse> updateDictType(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysDictType.class)
@@ -66,17 +74,20 @@ public class SysDictHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除字典类型", description = "删除指定字典类型")
public Mono<ServerResponse> deleteDictType(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return dictTypeService.deleteById(id)
.then(ServerResponse.noContent().build());
}
@Operation(summary = "获取所有字典数据", description = "获取系统中所有字典数据列表")
public Mono<ServerResponse> getAllDictData(ServerRequest request) {
return ServerResponse.ok()
.body(dictDataService.findAll(), SysDictData.class);
}
@Operation(summary = "根据ID获取字典数据", description = "根据字典数据ID获取详细信息")
public Mono<ServerResponse> getDictDataById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return dictDataService.findById(id)
@@ -84,18 +95,21 @@ public class SysDictHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据类型获取字典数据", description = "根据字典类型获取字典数据列表")
public Mono<ServerResponse> getDictDataByType(ServerRequest request) {
String dictType = request.pathVariable("dictType");
return ServerResponse.ok()
.body(dictDataService.findByDictType(dictType), SysDictData.class);
}
@Operation(summary = "创建字典数据", description = "创建新的字典数据")
public Mono<ServerResponse> createDictData(ServerRequest request) {
return request.bodyToMono(SysDictData.class)
.flatMap(dictDataService::save)
.flatMap(dd -> ServerResponse.status(HttpStatus.CREATED).bodyValue(dd));
}
@Operation(summary = "更新字典数据", description = "更新字典数据信息")
public Mono<ServerResponse> updateDictData(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysDictData.class)
@@ -114,6 +128,7 @@ public class SysDictHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除字典数据", description = "删除指定字典数据")
public Mono<ServerResponse> deleteDictData(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return dictDataService.deleteById(id)
@@ -2,6 +2,8 @@ package cn.novalon.manage.sys.handler.dictionary;
import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.core.service.IDictionaryService;
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;
@@ -15,6 +17,7 @@ import reactor.core.publisher.Mono;
* @date 2026-03-14
*/
@Component
@Tag(name = "字典管理", description = "字典数据相关操作")
public class DictionaryHandler {
private final IDictionaryService dictionaryService;
@@ -23,11 +26,13 @@ public class DictionaryHandler {
this.dictionaryService = dictionaryService;
}
@Operation(summary = "获取所有字典", description = "获取系统中所有字典列表")
public Mono<ServerResponse> getAllDictionaries(ServerRequest request) {
return ServerResponse.ok()
.body(dictionaryService.findAll(), Dictionary.class);
}
@Operation(summary = "根据ID获取字典", description = "根据字典ID获取字典详细信息")
public Mono<ServerResponse> getDictionaryById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return dictionaryService.findById(id)
@@ -35,12 +40,14 @@ public class DictionaryHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据类型获取字典", description = "根据字典类型获取字典列表")
public Mono<ServerResponse> getDictionariesByType(ServerRequest request) {
String type = request.pathVariable("type");
return ServerResponse.ok()
.body(dictionaryService.findByType(type), Dictionary.class);
}
@Operation(summary = "检查字典存在性", description = "检查指定类型和代码的字典是否存在")
public Mono<ServerResponse> checkTypeAndCodeExists(ServerRequest request) {
String type = request.queryParam("type").orElse(null);
String code = request.queryParam("code").orElse(null);
@@ -48,12 +55,14 @@ public class DictionaryHandler {
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
@Operation(summary = "创建字典", description = "创建新字典")
public Mono<ServerResponse> createDictionary(ServerRequest request) {
return request.bodyToMono(Dictionary.class)
.flatMap(dictionaryService::save)
.flatMap(dict -> ServerResponse.status(HttpStatus.CREATED).bodyValue(dict));
}
@Operation(summary = "更新字典", description = "更新字典信息")
public Mono<ServerResponse> updateDictionary(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(Dictionary.class)
@@ -62,6 +71,7 @@ public class DictionaryHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除字典", description = "删除指定字典")
public Mono<ServerResponse> deleteDictionary(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return dictionaryService.deleteById(id)
@@ -5,6 +5,8 @@ import cn.novalon.manage.sys.core.domain.SysExceptionLog;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.core.service.ISysExceptionLogService;
import cn.novalon.manage.common.dto.PageRequest;
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;
@@ -18,6 +20,7 @@ import reactor.core.publisher.Mono;
* @date 2026-03-14
*/
@Component
@Tag(name = "日志管理", description = "登录日志和异常日志相关操作")
public class SysLogHandler {
private final ISysLoginLogService loginLogService;
@@ -28,11 +31,13 @@ public class SysLogHandler {
this.exceptionLogService = exceptionLogService;
}
@Operation(summary = "获取所有登录日志", description = "获取系统中所有登录日志列表")
public Mono<ServerResponse> getAllLoginLogs(ServerRequest request) {
return ServerResponse.ok()
.body(loginLogService.findAll(), SysLoginLog.class);
}
@Operation(summary = "根据ID获取登录日志", description = "根据登录日志ID获取详细信息")
public Mono<ServerResponse> getLoginLogById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return loginLogService.findById(id)
@@ -40,12 +45,14 @@ public class SysLogHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建登录日志", description = "创建新的登录日志")
public Mono<ServerResponse> createLoginLog(ServerRequest request) {
return request.bodyToMono(SysLoginLog.class)
.flatMap(loginLogService::save)
.flatMap(log -> ServerResponse.status(HttpStatus.CREATED).bodyValue(log));
}
@Operation(summary = "分页获取登录日志", description = "根据分页参数获取登录日志列表")
public Mono<ServerResponse> getLoginLogsByPage(ServerRequest request) {
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
@@ -64,16 +71,19 @@ public class SysLogHandler {
.flatMap(response -> ServerResponse.ok().bodyValue(response));
}
@Operation(summary = "获取登录日志总数", description = "获取系统中登录日志总数")
public Mono<ServerResponse> getLoginLogCount(ServerRequest request) {
return loginLogService.count()
.flatMap(count -> ServerResponse.ok().bodyValue(count));
}
@Operation(summary = "获取所有异常日志", description = "获取系统中所有异常日志列表")
public Mono<ServerResponse> getAllExceptionLogs(ServerRequest request) {
return ServerResponse.ok()
.body(exceptionLogService.findAll(), SysExceptionLog.class);
}
@Operation(summary = "根据ID获取异常日志", description = "根据异常日志ID获取详细信息")
public Mono<ServerResponse> getExceptionLogById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return exceptionLogService.findById(id)
@@ -81,12 +91,14 @@ public class SysLogHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "创建异常日志", description = "创建新的异常日志")
public Mono<ServerResponse> createExceptionLog(ServerRequest request) {
return request.bodyToMono(SysExceptionLog.class)
.flatMap(exceptionLogService::save)
.flatMap(log -> ServerResponse.status(HttpStatus.CREATED).bodyValue(log));
}
@Operation(summary = "分页获取异常日志", description = "根据分页参数获取异常日志列表")
public Mono<ServerResponse> getExceptionLogsByPage(ServerRequest request) {
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
@@ -105,6 +117,7 @@ public class SysLogHandler {
.flatMap(response -> ServerResponse.ok().bodyValue(response));
}
@Operation(summary = "获取异常日志总数", description = "获取系统中异常日志总数")
public Mono<ServerResponse> getExceptionLogCount(ServerRequest request) {
return exceptionLogService.count()
.flatMap(count -> ServerResponse.ok().bodyValue(count));
@@ -6,6 +6,8 @@ import cn.novalon.manage.sys.dto.request.MenuCreateRequest;
import cn.novalon.manage.sys.dto.request.MenuUpdateRequest;
import cn.novalon.manage.sys.core.command.CreateMenuCommand;
import cn.novalon.manage.sys.core.command.UpdateMenuCommand;
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;
@@ -19,6 +21,7 @@ import reactor.core.publisher.Mono;
* @date 2026-03-14
*/
@Component
@Tag(name = "菜单管理", description = "系统菜单相关操作")
public class MenuHandler {
private final ISysMenuService menuService;
@@ -27,11 +30,13 @@ public class MenuHandler {
this.menuService = menuService;
}
@Operation(summary = "获取所有菜单", description = "获取系统中所有菜单列表")
public Mono<ServerResponse> getAllMenus(ServerRequest request) {
return ServerResponse.ok()
.body(menuService.findAll(), SysMenu.class);
}
@Operation(summary = "根据ID获取菜单", description = "根据菜单ID获取菜单详细信息")
public Mono<ServerResponse> getMenuById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return menuService.findById(id)
@@ -39,11 +44,13 @@ public class MenuHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "获取菜单树", description = "获取系统菜单树结构")
public Mono<ServerResponse> getMenuTree(ServerRequest request) {
return ServerResponse.ok()
.body(menuService.buildMenuTree(menuService.findAll()), SysMenu.class);
}
@Operation(summary = "根据父菜单获取子菜单", description = "根据父菜单ID获取子菜单列表")
public Mono<ServerResponse> getMenusByParent(ServerRequest request) {
Long parentId = request.queryParam("parentId")
.map(Long::valueOf)
@@ -52,12 +59,14 @@ public class MenuHandler {
.body(menuService.findByParentId(parentId), SysMenu.class);
}
@Operation(summary = "根据类型获取菜单", description = "根据菜单类型获取菜单列表")
public Mono<ServerResponse> getMenusByType(ServerRequest request) {
String menuType = request.queryParam("menuType").orElse(null);
return ServerResponse.ok()
.body(menuService.findAll().filter(menu -> menuType == null || menuType.equals(menu.getMenuType())), SysMenu.class);
}
@Operation(summary = "创建菜单", description = "创建新菜单")
public Mono<ServerResponse> createMenu(ServerRequest request) {
return request.bodyToMono(MenuCreateRequest.class)
.map(req -> CreateMenuCommand.of(
@@ -73,6 +82,7 @@ public class MenuHandler {
.flatMap(menu -> ServerResponse.status(HttpStatus.CREATED).bodyValue(menu));
}
@Operation(summary = "更新菜单", description = "更新菜单信息")
public Mono<ServerResponse> updateMenu(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(MenuUpdateRequest.class)
@@ -91,6 +101,7 @@ public class MenuHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除菜单", description = "删除指定菜单")
public Mono<ServerResponse> deleteMenu(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return menuService.deleteMenu(id)
@@ -3,6 +3,8 @@ package cn.novalon.manage.sys.handler.stats;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.sys.core.service.ISysRoleService;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
@@ -15,6 +17,7 @@ import reactor.core.publisher.Mono;
* @date 2026-03-14
*/
@Component
@Tag(name = "统计信息", description = "系统统计相关操作")
public class StatsHandler {
private final ISysUserService userService;
@@ -27,6 +30,7 @@ public class StatsHandler {
this.operationLogService = operationLogService;
}
@Operation(summary = "获取系统概览", description = "获取系统统计概览信息")
public Mono<ServerResponse> getOverview(ServerRequest request) {
return Mono.zip(
userService.count(),
@@ -1,5 +1,7 @@
package cn.novalon.manage.sys.primitive;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.ValidationException;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
@@ -26,7 +28,7 @@ public final class Email {
public static Email of(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("Email is required");
throw new ValidationException(ErrorCode.VALIDATION_REQUIRED, "Email is required");
}
validate(value);
return new Email(value);
@@ -42,7 +44,7 @@ public final class Email {
private static void validate(String value) {
if (!EMAIL_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("Invalid email format");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_FORMAT, "Invalid email format");
}
}
@@ -1,5 +1,7 @@
package cn.novalon.manage.sys.primitive;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.ValidationException;
import org.apache.commons.lang3.StringUtils;
/**
@@ -24,7 +26,7 @@ public final class Password {
public static Password of(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("Password is required");
throw new ValidationException(ErrorCode.VALIDATION_REQUIRED, "Password is required");
}
validate(value);
return new Password(value);
@@ -32,7 +34,8 @@ public final class Password {
private static void validate(String value) {
if (value.length() < MIN_LENGTH) {
throw new IllegalArgumentException("Password must be at least " + MIN_LENGTH + " characters long");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_LENGTH,
"Password must be at least " + MIN_LENGTH + " characters long");
}
boolean hasUppercase = value.chars().anyMatch(Character::isUpperCase);
@@ -41,7 +44,7 @@ public final class Password {
boolean hasSpecial = value.chars().anyMatch(c -> !Character.isLetterOrDigit(c));
if (!hasUppercase || !hasLowercase || !hasDigit || !hasSpecial) {
throw new IllegalArgumentException(
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE,
"Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character");
}
}
@@ -1,5 +1,7 @@
package cn.novalon.manage.sys.primitive;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.ValidationException;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
@@ -28,7 +30,7 @@ public final class Username {
public static Username of(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("Username is required");
throw new ValidationException(ErrorCode.VALIDATION_REQUIRED, "Username is required");
}
validate(value);
return new Username(value);
@@ -38,15 +40,18 @@ public final class Username {
String trimmed = value.trim();
if (trimmed.length() < MIN_LENGTH) {
throw new IllegalArgumentException("Username must be at least " + MIN_LENGTH + " characters long");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_LENGTH,
"Username must be at least " + MIN_LENGTH + " characters long");
}
if (trimmed.length() > MAX_LENGTH) {
throw new IllegalArgumentException("Username must be at most " + MAX_LENGTH + " characters long");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_LENGTH,
"Username must be at most " + MAX_LENGTH + " characters long");
}
if (!USERNAME_PATTERN.matcher(trimmed).matches()) {
throw new IllegalArgumentException("Username can only contain letters, numbers, and underscores");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_FORMAT,
"Username can only contain letters, numbers, and underscores");
}
}
@@ -0,0 +1,300 @@
package cn.novalon.manage.sys.primitive;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.ValidationException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
/**
* Password详细测试 - 提升分支覆盖率
*
* @author 张翔
* @date 2026-03-24
*/
class PasswordDetailedTest {
@Test
void testValidPassword() {
Password password = Password.of("Valid@123");
assertNotNull(password);
assertEquals("Valid@123", password.getValue());
}
@Test
void testNullPassword() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of(null);
});
assertEquals(ErrorCode.VALIDATION_REQUIRED, exception.getErrorCode());
}
@Test
void testEmptyPassword() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("");
});
assertEquals(ErrorCode.VALIDATION_REQUIRED, exception.getErrorCode());
}
@Test
void testWhitespacePassword() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of(" ");
});
assertEquals(ErrorCode.VALIDATION_REQUIRED, exception.getErrorCode());
}
@Test
void testTooShortPassword() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("Short1@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_LENGTH, exception.getErrorCode());
assertTrue(exception.getMessage().contains("at least 8 characters"));
}
@Test
void testExactlyMinLengthPassword() {
Password password = Password.of("Valid1@");
assertNotNull(password);
assertEquals("Valid1@", password.getValue());
}
@Test
void testPasswordWithoutUppercase() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("lowercase1@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
assertTrue(exception.getMessage().contains("uppercase letter"));
}
@Test
void testPasswordWithoutLowercase() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("UPPERCASE1@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
assertTrue(exception.getMessage().contains("lowercase letter"));
}
@Test
void testPasswordWithoutDigit() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("NoDigits@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
assertTrue(exception.getMessage().contains("digit"));
}
@Test
void testPasswordWithoutSpecialCharacter() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("NoSpecial123");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
assertTrue(exception.getMessage().contains("special character"));
}
@ParameterizedTest
@ValueSource(strings = {
"Valid@123",
"Another@456",
"Test@789",
"Complex@Pass123",
"Simple@Pass456"
})
void testMultipleValidPasswords(String password) {
Password pwd = Password.of(password);
assertNotNull(pwd);
assertEquals(password, pwd.getValue());
}
@ParameterizedTest
@ValueSource(strings = {
"lowercase@123",
"UPPERCASE@123",
"MixedCase@abc",
"MixedCase123"
})
void testMultipleInvalidPasswords(String password) {
assertThrows(ValidationException.class, () -> {
Password.of(password);
});
}
@Test
void testPasswordWithOnlyUppercaseAndDigit() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("UPPERCASE123");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithOnlyLowercaseAndDigit() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("lowercase123");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithOnlyUppercaseAndSpecial() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("UPPERCASE@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithOnlyLowercaseAndSpecial() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("lowercase@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithOnlyDigitAndSpecial() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("123456@");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithMultipleSpecialCharacters() {
Password password = Password.of("Valid@#$123");
assertNotNull(password);
assertEquals("Valid@#$123", password.getValue());
}
@Test
void testPasswordWithSpaces() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("Valid @123");
});
assertEquals(ErrorCode.VALIDATION_REQUIRED, exception.getErrorCode());
}
@Test
void testVeryLongPassword() {
Password password = Password.of("VeryLongPassword@1234567890");
assertNotNull(password);
assertEquals("VeryLongPassword@1234567890", password.getValue());
}
@Test
void testPasswordEquals() {
Password password1 = Password.of("Valid@123");
Password password2 = Password.of("Valid@123");
assertEquals(password1, password2);
}
@Test
void testPasswordNotEquals() {
Password password1 = Password.of("Valid@123");
Password password2 = Password.of("Different@456");
assertNotEquals(password1, password2);
}
@Test
void testPasswordEqualsNull() {
Password password = Password.of("Valid@123");
assertNotEquals(password, null);
}
@Test
void testPasswordEqualsDifferentClass() {
Password password = Password.of("Valid@123");
assertNotEquals(password, "Valid@123");
}
@Test
void testPasswordEqualsSameInstance() {
Password password = Password.of("Valid@123");
assertEquals(password, password);
}
@Test
void testPasswordHashCode() {
Password password1 = Password.of("Valid@123");
Password password2 = Password.of("Valid@123");
assertEquals(password1.hashCode(), password2.hashCode());
}
@Test
void testPasswordHashCodeDifferent() {
Password password1 = Password.of("Valid@123");
Password password2 = Password.of("Different@456");
assertNotEquals(password1.hashCode(), password2.hashCode());
}
@Test
void testPasswordToString() {
Password password = Password.of("Valid@123");
String toString = password.toString();
assertEquals("********", toString);
assertFalse(toString.contains("Valid"));
assertFalse(toString.contains("123"));
}
@Test
void testPasswordWithUnicodeCharacters() {
Password password = Password.of("密码@123");
assertNotNull(password);
assertEquals("密码@123", password.getValue());
}
@Test
void testPasswordWithNumbersOnly() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("12345678");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithLettersOnly() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("abcdefgh");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithSpecialOnly() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("@#$%^&*()");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithUppercaseLowercaseOnly() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("AbCdEfGh");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithUppercaseDigitOnly() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("ABCDEFGH12345678");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
@Test
void testPasswordWithLowercaseDigitOnly() {
ValidationException exception = assertThrows(ValidationException.class, () -> {
Password.of("abcdefgh12345678");
});
assertEquals(ErrorCode.VALIDATION_INVALID_VALUE, exception.getErrorCode());
}
}