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
+4
View File
@@ -82,6 +82,10 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
</dependencies>
<build>
@@ -6,6 +6,7 @@ import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -48,4 +49,12 @@ public class OpenApiConfig {
new Tag().name("认证管理").description("登录认证相关操作"),
new Tag().name("统计信息").description("系统统计相关操作")));
}
@Bean
public GroupedOpenApi allApi() {
return GroupedOpenApi.builder()
.group("all")
.pathsToMatch("/api/**")
.build();
}
}
@@ -37,3 +37,16 @@ logging:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
springdoc:
api-docs:
path: /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
tags-sorter: alpha
operations-sorter: alpha
show-actuator: false
default-consumes-media-type: application/json
default-produces-media-type: application/json
@@ -0,0 +1,39 @@
package cn.novalon.manage.common.exception;
import org.springframework.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
public abstract class BaseException extends RuntimeException {
private final String errorCode;
private final Map<String, Object> context;
protected BaseException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
protected BaseException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public String getErrorCode() {
return errorCode;
}
public Map<String, Object> getContext() {
return context;
}
public BaseException addContext(String key, Object value) {
context.put(key, value);
return this;
}
public abstract HttpStatus getHttpStatus();
}
@@ -1,28 +1,19 @@
package cn.novalon.manage.common.exception;
/**
* 业务异常类
*
* @author 张翔
* @date 2026-03-13
*/
public class BusinessException extends RuntimeException {
import org.springframework.http.HttpStatus;
private final String code;
private final String message;
public class BusinessException extends BaseException {
public BusinessException(String code, String message) {
super(message);
this.code = code;
this.message = message;
public BusinessException(String errorCode, String message) {
super(errorCode, message);
}
public String getCode() {
return code;
public BusinessException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public String getMessage() {
return message;
public HttpStatus getHttpStatus() {
return HttpStatus.BAD_REQUEST;
}
}
@@ -0,0 +1,19 @@
package cn.novalon.manage.common.exception;
import org.springframework.http.HttpStatus;
public class ConflictException extends BusinessException {
public ConflictException(String errorCode, String message) {
super(errorCode, message);
}
public ConflictException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.CONFLICT;
}
}
@@ -0,0 +1,32 @@
package cn.novalon.manage.common.exception;
public class ErrorCode {
public static final String VALIDATION_PREFIX = "VALIDATION_";
public static final String NOT_FOUND_PREFIX = "NOT_FOUND_";
public static final String PERMISSION_PREFIX = "PERMISSION_";
public static final String CONFLICT_PREFIX = "CONFLICT_";
public static final String SYSTEM_PREFIX = "SYSTEM_";
public static final String VALIDATION_REQUIRED = VALIDATION_PREFIX + "001";
public static final String VALIDATION_INVALID_FORMAT = VALIDATION_PREFIX + "002";
public static final String VALIDATION_INVALID_LENGTH = VALIDATION_PREFIX + "003";
public static final String VALIDATION_INVALID_VALUE = VALIDATION_PREFIX + "004";
public static final String NOT_FOUND_USER = NOT_FOUND_PREFIX + "001";
public static final String NOT_FOUND_ROLE = NOT_FOUND_PREFIX + "002";
public static final String NOT_FOUND_MENU = NOT_FOUND_PREFIX + "003";
public static final String NOT_FOUND_DICTIONARY = NOT_FOUND_PREFIX + "004";
public static final String PERMISSION_DENIED = PERMISSION_PREFIX + "001";
public static final String PERMISSION_INSUFFICIENT = PERMISSION_PREFIX + "002";
public static final String CONFLICT_DUPLICATE = CONFLICT_PREFIX + "001";
public static final String CONFLICT_DUPLICATE_USER = CONFLICT_PREFIX + "002";
public static final String CONFLICT_DUPLICATE_ROLE = CONFLICT_PREFIX + "003";
public static final String CONFLICT_DUPLICATE_DICTIONARY = CONFLICT_PREFIX + "004";
public static final String SYSTEM_INTERNAL_ERROR = SYSTEM_PREFIX + "001";
public static final String SYSTEM_DATABASE_ERROR = SYSTEM_PREFIX + "002";
public static final String SYSTEM_NETWORK_ERROR = SYSTEM_PREFIX + "003";
}
@@ -0,0 +1,19 @@
package cn.novalon.manage.common.exception;
import org.springframework.http.HttpStatus;
public class NotFoundException extends BusinessException {
public NotFoundException(String errorCode, String message) {
super(errorCode, message);
}
public NotFoundException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.NOT_FOUND;
}
}
@@ -0,0 +1,19 @@
package cn.novalon.manage.common.exception;
import org.springframework.http.HttpStatus;
public class PermissionException extends BusinessException {
public PermissionException(String errorCode, String message) {
super(errorCode, message);
}
public PermissionException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.FORBIDDEN;
}
}
@@ -0,0 +1,19 @@
package cn.novalon.manage.common.exception;
import org.springframework.http.HttpStatus;
public class SystemException extends BaseException {
public SystemException(String errorCode, String message) {
super(errorCode, message);
}
public SystemException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
@@ -0,0 +1,19 @@
package cn.novalon.manage.common.exception;
import org.springframework.http.HttpStatus;
public class ValidationException extends BusinessException {
public ValidationException(String errorCode, String message) {
super(errorCode, message);
}
public ValidationException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.BAD_REQUEST;
}
}
@@ -1,5 +1,6 @@
package cn.novalon.manage.common.handler;
import cn.novalon.manage.common.exception.BaseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
@@ -40,6 +41,22 @@ public class GlobalExceptionHandler {
this.exceptionLogService = exceptionLogService;
}
@ExceptionHandler(BaseException.class)
public ResponseEntity<Map<String, Object>> handleBaseException(BaseException ex, ServerWebExchange exchange) {
logger.warn("Business exception: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", ex.getErrorCode());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
if (!ex.getContext().isEmpty()) {
response.put("context", ex.getContext());
}
return ResponseEntity.status(ex.getHttpStatus()).body(response);
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex, ServerWebExchange exchange) {
logger.warn("Runtime exception: ", ex);
@@ -1,5 +1,9 @@
package cn.novalon.manage.common.util;
import cn.novalon.manage.common.exception.ErrorCode;
import cn.novalon.manage.common.exception.SystemException;
import cn.novalon.manage.common.exception.ValidationException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@@ -60,7 +64,8 @@ public final class SnowflakeId {
}
}
}
throw new IllegalStateException("Failed to generate ID after " + MAX_RETRIES + " retries");
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR,
"Failed to generate ID after " + MAX_RETRIES + " retries");
}
private static long nextIdInternal() {
@@ -151,23 +156,25 @@ public final class SnowflakeId {
private static void validateBits(int workerBits, int seqBits) {
if (workerBits < 0 || workerBits > 22) {
throw new IllegalArgumentException("WorkerID位数必须在0-22之间");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE, "WorkerID位数必须在0-22之间");
}
if (seqBits < 0 || seqBits > 22) {
throw new IllegalArgumentException("序列号位数必须在0-22之间");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE, "序列号位数必须在0-22之间");
}
if (workerBits + seqBits > 22) {
throw new IllegalArgumentException("WorkerID和序列号位数总和不能超过22位,当前为: " + (workerBits + seqBits));
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE,
"WorkerID和序列号位数总和不能超过22位,当前为: " + (workerBits + seqBits));
}
if (workerBits + seqBits == 0) {
throw new IllegalArgumentException("WorkerID和序列号位数总和不能为0");
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE, "WorkerID和序列号位数总和不能为0");
}
}
private static long resolveWorkerId(long maxWorkerId) {
long id = generateNewId();
if (id < 0 || id > maxWorkerId) {
throw new IllegalStateException("WorkerID超出有效范围: " + id + " (有效范围: 0-" + maxWorkerId + ")");
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR,
"WorkerID超出有效范围: " + id + " (有效范围: 0-" + maxWorkerId + ")");
}
return id;
}
@@ -1,60 +1,297 @@
package cn.novalon.manage.db.dao;
import cn.novalon.manage.db.entity.SysUserQueryCriteria;
import cn.novalon.manage.db.dao.QueryField;
import cn.novalon.manage.db.dao.QueryUtil;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* QueryUtil详细测试
* QueryUtil详细测试 - 提升分支覆盖率
*
* @author 张翔
* @date 2026-03-24
*/
class QueryUtilDetailedTest {
@Test
void testBlurrySearchCriteria() {
SysUserQueryCriteria criteria = new SysUserQueryCriteria();
criteria.setKeyword("search");
Query query = QueryUtil.getQuery(criteria);
System.out.println("生成的Query: " + query);
System.out.println("生成的Criteria: " + query.getCriteria());
assertTrue(true, "模糊搜索功能已实现");
}
@Test
void testBlurrySearchWithDeletedFilter() {
SysUserQueryCriteria criteria = new SysUserQueryCriteria();
criteria.setKeyword("search");
Query query = QueryUtil.getQuery(criteria, true);
System.out.println("带deletedAt过滤的Query: " + query);
System.out.println("带deletedAt过滤的Criteria: " + query.getCriteria());
assertTrue(true, "模糊搜索和deletedAt过滤功能已实现");
}
@Test
void testOrCriteriaLogic() {
String[] blurrys = {"username", "email"};
String val = "search";
Criteria criteria = Criteria.empty();
for (String s : blurrys) {
criteria = criteria.or(s).like("%" + val + "%");
static class TestQuery {
@QueryField(propName = "name", type = QueryField.Type.EQUAL)
private String name;
@QueryField(propName = "age", type = QueryField.Type.GREATER_THAN)
private Integer age;
@QueryField(propName = "score", type = QueryField.Type.LESS_THAN)
private Integer score;
@QueryField(propName = "status", type = QueryField.Type.INNER_LIKE)
private String status;
@QueryField(propName = "email", type = QueryField.Type.LEFT_LIKE)
private String email;
@QueryField(propName = "phone", type = QueryField.Type.RIGHT_LIKE)
private String phone;
@QueryField(propName = "roles", type = QueryField.Type.IN)
private List<String> roles;
@QueryField(propName = "keyword", blurry = "name,description,content")
private String keyword;
@QueryField(propName = "deletedAt", type = QueryField.Type.IS_NULL)
private String deletedAt;
@QueryField(propName = "updatedAt", type = QueryField.Type.IS_NOT_NULL)
private String updatedAt;
@QueryField(propName = "orField", type = QueryField.Type.OR,
orPropVal = QueryField.Type.IS_NULL,
orPropNames = {"field1", "field2"})
private String orField;
public TestQuery() {}
public TestQuery(String name, Integer age, Integer score, String status, String email,
String phone, List<String> roles, String keyword, String deletedAt,
String updatedAt, String orField) {
this.name = name;
this.age = age;
this.score = score;
this.status = status;
this.email = email;
this.phone = phone;
this.roles = roles;
this.keyword = keyword;
this.deletedAt = deletedAt;
this.updatedAt = updatedAt;
this.orField = orField;
}
System.out.println("循环构建的Criteria: " + criteria);
String criteriaStr = criteria.toString();
System.out.println("Criteria字符串: " + criteriaStr);
assertTrue(criteriaStr.contains("username"), "应该包含username");
assertTrue(criteriaStr.contains("email"), "应该包含email");
assertTrue(criteriaStr.contains("OR"), "应该包含OR");
public String getName() { return name; }
public Integer getAge() { return age; }
public Integer getScore() { return score; }
public String getStatus() { return status; }
public String getEmail() { return email; }
public String getPhone() { return phone; }
public List<String> getRoles() { return roles; }
public String getKeyword() { return keyword; }
public String getDeletedAt() { return deletedAt; }
public String getUpdatedAt() { return updatedAt; }
public String getOrField() { return orField; }
}
@Test
void testNullQuery() {
Query query = QueryUtil.getQuery(null);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testQueryWithDeletedAtFilter() {
TestQuery testQuery = new TestQuery();
Query query = QueryUtil.getQuery(testQuery, true);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testQueryWithoutDeletedAtFilter() {
TestQuery testQuery = new TestQuery();
Query query = QueryUtil.getQuery(testQuery, false);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testEqualCondition() {
TestQuery testQuery = new TestQuery("John", null, null, null, null, null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testGreaterThanCondition() {
TestQuery testQuery = new TestQuery(null, 18, null, null, null, null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testLessThanCondition() {
TestQuery testQuery = new TestQuery(null, null, 100, null, null, null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testInnerLikeCondition() {
TestQuery testQuery = new TestQuery(null, null, null, "active", null, null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testLeftLikeCondition() {
TestQuery testQuery = new TestQuery(null, null, null, null, "@example.com", null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testRightLikeCondition() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, "123", null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testInCondition() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null,
Arrays.asList("admin", "user"), null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testInConditionWithEmptyList() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null,
Collections.emptyList(), null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testBlurrySearchSingleField() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null, null, "test", null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testBlurrySearchMultipleFields() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null, null, "keyword", null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testIsNullCondition() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null, null, null, "null", null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testIsNotNullCondition() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null, null, null, null, "value", null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testOrConditionIsNull() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null, null, null, null, null, "value");
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testEmptyStringValue() {
TestQuery testQuery = new TestQuery("", null, null, null, null, null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testNullFieldValue() {
TestQuery testQuery = new TestQuery(null, null, null, null, null, null, null, null, null, null, null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testMultipleConditions() {
TestQuery testQuery = new TestQuery("John", 18, 100, "active", "@example.com",
"123", Arrays.asList("admin"), "test", null, "value", null);
Query query = QueryUtil.getQuery(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testQueryAllWithoutDeletedAtFilter() {
TestQuery testQuery = new TestQuery("John", 18, 100, "active", "@example.com",
"123", Arrays.asList("admin"), "test", null, "value", null);
Query query = QueryUtil.getQueryAll(testQuery);
assertNotNull(query);
Criteria criteria = query.getCriteria();
assertNotNull(criteria);
}
@Test
void testIsBlankWithNull() {
assertTrue(QueryUtil.isBlank(null));
}
@Test
void testIsBlankWithEmptyString() {
assertTrue(QueryUtil.isBlank(""));
}
@Test
void testIsBlankWithWhitespace() {
assertTrue(QueryUtil.isBlank(" "));
}
@Test
void testIsBlankWithValidString() {
assertFalse(QueryUtil.isBlank("test"));
}
@Test
void testIsBlankWithMixedWhitespace() {
assertFalse(QueryUtil.isBlank(" test "));
}
}
+4
View File
@@ -26,6 +26,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -2,6 +2,8 @@ package cn.novalon.manage.file.handler;
import cn.novalon.manage.file.core.domain.SysFile;
import cn.novalon.manage.file.core.service.ISysFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Component;
@@ -15,6 +17,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
@Component
@Tag(name = "文件管理", description = "文件上传下载相关操作")
public class SysFileHandler {
private final ISysFileService fileService;
@@ -23,11 +26,13 @@ public class SysFileHandler {
this.fileService = fileService;
}
@Operation(summary = "获取所有文件", description = "获取系统中所有文件列表")
public Mono<ServerResponse> getAllFiles(ServerRequest request) {
Flux<SysFile> files = fileService.getAllFiles();
return ServerResponse.ok().body(files, SysFile.class);
}
@Operation(summary = "根据ID获取文件", description = "根据文件ID获取文件详细信息")
public Mono<ServerResponse> getFileById(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.getFileById(id)
@@ -35,6 +40,7 @@ public class SysFileHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "上传文件", description = "上传文件到系统")
public Mono<ServerResponse> uploadFile(ServerRequest request) {
String username = request.headers().firstHeader("X-Username");
if (username == null) {
@@ -60,6 +66,7 @@ public class SysFileHandler {
.switchIfEmpty(ServerResponse.badRequest().bodyValue("No file data"));
}
@Operation(summary = "下载文件", description = "根据文件ID下载文件")
public Mono<ServerResponse> downloadFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.getFileById(id)
@@ -78,6 +85,7 @@ public class SysFileHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据文件名下载", description = "根据文件名下载文件")
public Mono<ServerResponse> downloadFileByName(ServerRequest request) {
String fileName = request.pathVariable("fileName");
return fileService.getAllFiles()
@@ -98,6 +106,7 @@ public class SysFileHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "预览文件", description = "根据文件ID预览文件")
public Mono<ServerResponse> previewFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.getFileById(id)
@@ -115,6 +124,7 @@ public class SysFileHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据文件名预览", description = "根据文件名预览文件")
public Mono<ServerResponse> previewFileByName(ServerRequest request) {
String fileName = request.pathVariable("fileName");
return fileService.getAllFiles()
@@ -134,6 +144,7 @@ public class SysFileHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除文件", description = "删除指定文件")
public Mono<ServerResponse> deleteFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.deleteFile(id)
+4
View File
@@ -26,6 +26,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -2,6 +2,8 @@ package cn.novalon.manage.notify.handler;
import cn.novalon.manage.notify.core.domain.SysNotice;
import cn.novalon.manage.notify.core.service.ISysNoticeService;
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 java.util.List;
import java.util.Map;
@Component
@Tag(name = "通知管理", description = "系统通知相关操作")
public class SysNoticeHandler {
private final ISysNoticeService noticeService;
@@ -25,11 +28,13 @@ public class SysNoticeHandler {
this.noticeService = noticeService;
}
@Operation(summary = "获取所有通知", description = "获取系统中所有通知列表")
public Mono<ServerResponse> getAllNotices(ServerRequest request) {
Flux<SysNotice> notices = noticeService.getAllNotices();
return ServerResponse.ok().body(notices, SysNotice.class);
}
@Operation(summary = "根据ID获取通知", description = "根据通知ID获取通知详细信息")
public Mono<ServerResponse> getNoticeById(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return noticeService.getNoticeById(id)
@@ -37,12 +42,14 @@ public class SysNoticeHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据状态获取通知", description = "根据状态获取通知列表")
public Mono<ServerResponse> getNoticesByStatus(ServerRequest request) {
String status = request.pathVariable("status");
Flux<SysNotice> notices = noticeService.getNoticesByStatus(status);
return ServerResponse.ok().body(notices, SysNotice.class);
}
@Operation(summary = "创建通知", description = "创建新通知")
public Mono<ServerResponse> createNotice(ServerRequest request) {
return request.bodyToMono(SysNotice.class)
.filter(notice -> notice.getNoticeTitle() != null && !notice.getNoticeTitle().trim().isEmpty())
@@ -64,6 +71,7 @@ public class SysNoticeHandler {
});
}
@Operation(summary = "更新通知", description = "更新通知信息")
public Mono<ServerResponse> updateNotice(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return request.bodyToMono(SysNotice.class)
@@ -72,6 +80,7 @@ public class SysNoticeHandler {
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除通知", description = "删除指定通知")
public Mono<ServerResponse> deleteNotice(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return noticeService.getNoticeById(id)
@@ -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());
}
}