feat: complete all post-refactor tasks (performance, monitoring, docs, CI/CD)

This commit is contained in:
张翔
2026-03-14 11:48:47 +08:00
parent 6453c8c6fb
commit c3d7ad8a40
36 changed files with 1588 additions and 867 deletions
@@ -0,0 +1,884 @@
# 模块架构重构执行计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 重构项目模块架构,实现清晰的职责划分和依赖倒置
**Architecture:**
- app模块:只包含启动类、应用级配置和flyway脚本
- sys模块:包含所有业务代码(domain、service、handler等)和业务级配置
- gateway模块:包含路由和限流配置
- db模块:依赖sys模块,实现repository接口
- common模块:提供通用工具类和基础配置
**Tech Stack:** Maven, Spring Boot, Spring WebFlux, Spring Security, R2DBC
---
## 重构目标
### 模块职责划分
| 模块 | 职责 | 内容 |
|-------|--------|------|
| manage-app | 应用启动和配置 | ManageApplication.java、application.yml、flyway脚本、应用级配置(WebFluxConfig、MultipartConfig、OpenApiConfig |
| manage-sys | 业务逻辑 | domain、repository接口、service接口和实现、handler、业务级配置(SecurityConfig、WebSocketConfig |
| manage-gateway | 网关路由和限流 | GatewayApplication.java、路由配置(SystemRouter)、限流配置(RateLimitConfig |
| manage-db | 数据访问实现 | entity、dao、repository实现、converter |
| manage-common | 通用工具和配置 | 工具类、通用DTO、基础配置、全局异常处理(GlobalExceptionHandler |
### 依赖关系
```
manage-gateway → 无依赖(独立模块)
manage-app → manage-sys + manage-db
manage-sys → manage-common
manage-db → manage-sys
manage-common → 无依赖
```
---
## Task 1: 将RateLimitConfig从app模块移到gateway模块
**Files:**
- Create: `manage-gateway/src/main/java/cn/novalon/manage/gateway/config/RateLimitConfig.java`
- Delete: `manage-app/src/main/java/cn/novalon/manage/app/config/RateLimitConfig.java`
**Step 1: 创建gateway模块的config目录**
```bash
mkdir -p manage-gateway/src/main/java/cn/novalon/manage/gateway/config
```
**Step 2: 移动RateLimitConfig.java**
```bash
mv manage-app/src/main/java/cn/novalon/manage/app/config/RateLimitConfig.java \
manage-gateway/src/main/java/cn/novalon/manage/gateway/config/
```
**Step 3: 更新RateLimitConfig.java的包声明**
```java
// 将
package cn.novalon.manage.sys.config;
// 改为
package cn.novalon.manage.gateway.config;
```
**Step 4: 更新gateway模块的pom.xml,添加Resilience4j依赖**
```xml
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>2.2.0</version>
</dependency>
```
**Step 5: 更新gateway模块的application.yml,添加限流配置**
```yaml
rate:
limit:
limit-for-period: 100
limit-refresh-period: 1s
timeout-duration: 0
```
**Step 6: 提交更改**
```bash
git add manage-gateway/src/main/java/cn/novalon/manage/gateway/config/RateLimitConfig.java
git add manage-gateway/pom.xml
git add manage-gateway/src/main/resources/application.yml
git rm manage-app/src/main/java/cn/novalon/manage/app/config/RateLimitConfig.java
git commit -m "refactor: move RateLimitConfig to gateway module"
```
---
## Task 2: 将SystemRouter从app模块移到gateway模块
**Files:**
- Create: `manage-gateway/src/main/java/cn/novalon/manage/gateway/config/SystemRouter.java`
- Delete: `manage-app/src/main/java/cn/novalon/manage/app/config/SystemRouter.java`
**Step 1: 移动SystemRouter.java**
```bash
mv manage-app/src/main/java/cn/novalon/manage/app/config/SystemRouter.java \
manage-gateway/src/main/java/cn/novalon/manage/gateway/config/
```
**Step 2: 更新SystemRouter.java的包声明**
```java
// 将
package cn.novalon.manage.sys.config;
// 改为
package cn.novalon.manage.gateway.config;
```
**Step 3: 更新GatewayApplication.java,集成SystemRouter**
```java
package cn.novalon.manage.gateway;
import cn.novalon.manage.gateway.config.SystemRouter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder, SystemRouter systemRouter) {
return systemRouter.buildRoutes(builder);
}
}
```
**Step 4: 更新SystemRouter.java,使用RouteLocatorBuilder**
```java
package cn.novalon.manage.gateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.stereotype.Component;
/**
* 系统路由配置
*
* 文件定义:配置Spring Cloud Gateway的路由规则
* 涉及业务:API路由、负载均衡、服务发现
* 算法:使用Spring Cloud Gateway的路由匹配和转发
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SystemRouter {
public RouteLocator buildRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("manage-app", r -> r
.path("/api/**")
.uri("http://manage-app:8081"))
.build();
}
}
```
**Step 5: 提交更改**
```bash
git add manage-gateway/src/main/java/cn/novalon/manage/gateway/config/SystemRouter.java
git add manage-gateway/src/main/java/cn/novalon/manage/gateway/GatewayApplication.java
git rm manage-app/src/main/java/cn/novalon/manage/app/config/SystemRouter.java
git commit -m "refactor: move SystemRouter to gateway module"
```
---
## Task 3: 将SecurityConfig从app模块移到sys模块
**Files:**
- Create: `manage-sys/src/main/java/cn/novalon/manage/sys/config/SecurityConfig.java`
- Delete: `manage-app/src/main/java/cn/novalon/manage/app/config/SecurityConfig.java`
**Step 1: 创建sys模块的config目录**
```bash
mkdir -p manage-sys/src/main/java/cn/novalon/manage/sys/config
```
**Step 2: 移动SecurityConfig.java**
```bash
mv manage-app/src/main/java/cn/novalon/manage/app/config/SecurityConfig.java \
manage-sys/src/main/java/cn/novalon/manage/sys/config/
```
**Step 3: 更新SecurityConfig.java的包声明**
```java
// 将
package cn.novalon.manage.sys.config;
// 改为
package cn.novalon.manage.sys.config;
```
**Step 4: 提交更改**
```bash
git add manage-sys/src/main/java/cn/novalon/manage/sys/config/SecurityConfig.java
git rm manage-app/src/main/java/cn/novalon/manage/app/config/SecurityConfig.java
git commit -m "refactor: move SecurityConfig to sys module"
```
---
## Task 4: 将WebSocketConfig从app模块移到sys模块
**Files:**
- Create: `manage-sys/src/main/java/cn/novalon/manage/sys/config/WebSocketConfig.java`
- Delete: `manage-app/src/main/java/cn/novalon/manage/app/config/WebSocketConfig.java`
**Step 1: 移动WebSocketConfig.java**
```bash
mv manage-app/src/main/java/cn/novalon/manage/app/config/WebSocketConfig.java \
manage-sys/src/main/java/cn/novalon/manage/sys/config/
```
**Step 2: 更新WebSocketConfig.java的包声明**
```java
// 将
package cn.novalon.manage.sys.config;
// 改为
package cn.novalon.manage.sys.config;
```
**Step 3: 提交更改**
```bash
git add manage-sys/src/main/java/cn/novalon/manage/sys/config/WebSocketConfig.java
git rm manage-app/src/main/java/cn/novalon/manage/app/config/WebSocketConfig.java
git commit -m "refactor: move WebSocketConfig to sys module"
```
---
## Task 5: 将GlobalExceptionHandler移到common模块并重构
**Files:**
- Create: `manage-common/src/main/java/cn/novalon/manage/common/handler/GlobalExceptionHandler.java`
- Create: `manage-common/src/main/java/cn/novalon/manage/common/handler/ExceptionLogService.java`
- Delete: `manage-app/src/main/java/cn/novalon/manage/app/handler/GlobalExceptionHandler.java`
**Step 1: 创建common模块的handler目录**
```bash
mkdir -p manage-common/src/main/java/cn/novalon/manage/common/handler
```
**Step 2: 创建异常日志服务接口**
```java
package cn.novalon.manage.common.handler;
import reactor.core.publisher.Mono;
/**
* 异常日志服务接口
*
* 文件定义:定义异常日志记录的抽象接口
* 涉及业务:异常日志记录、错误追踪
* 算法:使用响应式编程实现异步日志记录
*
* @author 张翔
* @date 2026-03-13
*/
public interface ExceptionLogService {
Mono<Void> logException(String title, String exceptionName, String exceptionMsg,
String methodName, String ip, String stackTrace);
}
```
**Step 3: 重构GlobalExceptionHandler,移除对sys模块的依赖**
```java
package cn.novalon.manage.common.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
* 文件定义:统一处理系统中抛出的各种异常,返回标准化的错误响应
* 涉及业务:异常捕获、错误日志记录、错误响应格式化
* 算法:使用@RestControllerAdvice注解实现全局异常拦截
*
* @author 张翔
* @date 2026-03-13
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final ExceptionLogService exceptionLogService;
public GlobalExceptionHandler(ExceptionLogService exceptionLogService) {
this.exceptionLogService = exceptionLogService;
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex, ServerWebExchange exchange) {
logger.warn("Runtime exception: ", ex);
Map<String, Object> response = new HashMap<>();
if (ex.getMessage() != null && ex.getMessage().contains("not found")) {
response.put("code", HttpStatus.NOT_FOUND.value());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception ex, ServerWebExchange exchange) {
logger.error("Exception occurred: ", ex);
exceptionLogService.logException(
"System Exception",
ex.getClass().getSimpleName(),
ex.getMessage(),
exchange.getRequest().getPath().value(),
getClientIp(exchange),
getStackTrace(ex)
).subscribe();
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
response.put("message", "Internal server error");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex, ServerWebExchange exchange) {
logger.warn("Illegal argument: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, ServerWebExchange exchange) {
logger.warn("Validation failed: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", "Validation failed");
response.put("timestamp", LocalDateTime.now());
Map<String, String> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (e1, e2) -> e1));
response.put("errors", fieldErrors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(ServerWebInputException.class)
public ResponseEntity<Map<String, Object>> handleServerWebInputException(ServerWebInputException ex, ServerWebExchange exchange) {
logger.warn("Invalid input: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", "Invalid input");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatusException(ResponseStatusException ex, ServerWebExchange exchange) {
logger.warn("Response status exception: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", ex.getStatusCode().value());
response.put("message", ex.getReason());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(ex.getStatusCode()).body(response);
}
@ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<Map<String, Object>> handleDuplicateKeyException(DuplicateKeyException ex, ServerWebExchange exchange) {
logger.warn("Duplicate key: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.CONFLICT.value());
response.put("message", "Duplicate key violation");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Map<String, Object>> handleDataIntegrityViolationException(DataIntegrityViolationException ex, ServerWebExchange exchange) {
logger.warn("Data integrity violation: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.CONFLICT.value());
response.put("message", "Data integrity violation");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
private String getClientIp(ServerWebExchange exchange) {
return exchange.getRequest().getHeaders().getFirst("X-Forwarded-For",
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
private String getStackTrace(Exception ex) {
StringBuilder stackTrace = new StringBuilder();
for (StackTraceElement element : ex.getStackTrace()) {
stackTrace.append(element.toString()).append("\n");
}
return stackTrace.toString();
}
}
```
**Step 4: 移动GlobalExceptionHandler.java**
```bash
mv manage-app/src/main/java/cn/novalon/manage/app/handler/GlobalExceptionHandler.java \
manage-common/src/main/java/cn/novalon/manage/common/handler/
```
**Step 5: 更新GlobalExceptionHandler.java的包声明**
```java
// 将
package cn.novalon.manage.sys.handler;
// 改为
package cn.novalon.manage.common.handler;
```
**Step 6: 在sys模块实现ExceptionLogService接口**
```java
package cn.novalon.manage.sys.handler;
import cn.novalon.manage.common.handler.ExceptionLogService;
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
import cn.novalon.manage.sys.core.service.ISysExceptionLogService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
* 异常日志服务实现
*
* 文件定义:实现异常日志记录接口,使用sys模块的异常日志服务
* 涉及业务:异常日志记录、错误追踪
* 算法:使用响应式编程实现异步日志记录
*
* @author 张翔
* @date 2026-03-13
*/
@Service
public class ExceptionLogServiceImpl implements ExceptionLogService {
private final ISysExceptionLogService exceptionLogService;
public ExceptionLogServiceImpl(ISysExceptionLogService exceptionLogService) {
this.exceptionLogService = exceptionLogService;
}
@Override
public Mono<Void> logException(String title, String exceptionName, String exceptionMsg,
String methodName, String ip, String stackTrace) {
SysExceptionLog exceptionLog = new SysExceptionLog();
exceptionLog.setTitle(title);
exceptionLog.setExceptionName(exceptionName);
exceptionLog.setExceptionMsg(exceptionMsg);
exceptionLog.setMethodName(methodName);
exceptionLog.setIp(ip);
exceptionLog.setCreateTime(LocalDateTime.now());
exceptionLog.setStackTrace(stackTrace);
return exceptionLogService.save(exceptionLog).then();
}
}
```
**Step 7: 在sys模块的配置中注册ExceptionLogServiceImpl**
```java
package cn.novalon.manage.sys.config;
import cn.novalon.manage.common.handler.ExceptionLogService;
import cn.novalon.manage.sys.handler.ExceptionLogServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 异常日志配置类
*
* 文件定义:配置异常日志服务的实现
* 涉及业务:异常日志记录、错误追踪
* 算法:使用Spring的依赖注入实现接口和实现的绑定
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
public class ExceptionLogConfig {
@Bean
public ExceptionLogService exceptionLogService(ExceptionLogServiceImpl exceptionLogServiceImpl) {
return exceptionLogServiceImpl;
}
}
```
**Step 8: 提交更改**
```bash
git add manage-common/src/main/java/cn/novalon/manage/common/handler/GlobalExceptionHandler.java
git add manage-common/src/main/java/cn/novalon/manage/common/handler/ExceptionLogService.java
git add manage-sys/src/main/java/cn/novalon/manage/sys/handler/ExceptionLogServiceImpl.java
git add manage-sys/src/main/java/cn/novalon/manage/sys/config/ExceptionLogConfig.java
git rm manage-app/src/main/java/cn/novalon/manage/app/handler/GlobalExceptionHandler.java
git commit -m "refactor: move GlobalExceptionHandler to common module with dependency inversion"
```
---
## Task 6: 更新app模块的ManageApplication.java
**Files:**
- Modify: `manage-app/src/main/java/cn/novalon/manage/app/ManageApplication.java`
**Step 1: 更新ManageApplication.java的组件扫描配置**
```java
package cn.novalon.manage.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
/**
* 管理应用主类
*
* 文件定义:Spring Boot应用启动类,配置组件扫描和功能启用
* 涉及业务:应用启动、组件扫描、功能配置
* 算法:使用Spring Boot自动配置和注解驱动
*
* @author 张翔
* @date 2026-03-13
*/
@SpringBootApplication
@ConfigurationPropertiesScan(basePackages = "cn.novalon.manage")
@ComponentScan(basePackages = {"cn.novalon.manage.sys", "cn.novalon.manage.db"})
@EnableR2dbcRepositories(basePackages = "cn.novalon.manage.db.repository")
public class ManageApplication {
public static void main(String[] args) {
SpringApplication.run(ManageApplication.class, args);
}
}
```
**Step 2: 提交更改**
```bash
git add manage-app/src/main/java/cn/novalon/manage/app/ManageApplication.java
git commit -m "refactor: update ManageApplication component scan configuration"
```
---
## Task 7: 更新app模块的pom.xml
**Files:**
- Modify: `manage-app/pom.xml`
**Step 1: 确保app模块依赖sys和db模块**
```xml
<dependencies>
<dependency>
<groupId>cn.novalon.manage</groupId>
<artifactId>manage-sys</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.manage</groupId>
<artifactId>manage-db</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
**Step 2: 移除不需要的依赖**
```xml
<!-- 移除以下依赖 -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>2.2.0</version>
</dependency>
```
**Step 3: 提交更改**
```bash
git add manage-app/pom.xml
git commit -m "refactor: update app module dependencies"
```
---
## Task 8: 编译和测试验证
**Files:**
- Test: `manage-app`, `manage-sys`, `manage-db`, `manage-gateway`
**Step 1: 清理并编译所有模块**
```bash
mvn clean compile -DskipTests
```
**Expected:** 所有模块编译成功,无错误
**Step 2: 运行单元测试**
```bash
mvn test
```
**Expected:** 所有测试通过
**Step 3: 启动应用验证**
```bash
cd manage-app
mvn spring-boot:run
```
**Expected:** 应用成功启动,无错误日志
**Step 4: 提交更改**
```bash
git add .
git commit -m "refactor: complete module architecture refactoring"
```
---
## Task 9: 更新文档
**Files:**
- Create: `docs/architecture/module-architecture.md`
**Step 1: 创建模块架构文档**
```markdown
# 模块架构设计
## 模块职责划分
### manage-app
应用启动和配置模块,包含:
- ManageApplication.java:应用启动类
- application.yml:应用配置文件
- flyway脚本:数据库迁移脚本
- 应用级配置:WebFluxConfig、MultipartConfig、OpenApiConfig、GlobalExceptionHandler
### manage-sys
业务逻辑模块,包含:
- domain:领域对象(SysUser、SysRole、SysMenu等)
- repository:数据访问接口
- service:业务逻辑接口和实现
- handler:业务处理器(用户、角色、菜单等)
- 业务级配置:SecurityConfig、WebSocketConfig
- 其他:filter、security、websocket、primitive、command、dto
### manage-gateway
网关模块,包含:
- GatewayApplication.java:网关启动类
- 路由配置:SystemRouter
- 限流配置:RateLimitConfig
### manage-db
数据访问实现模块,包含:
- entity:数据库实体
- dao:数据访问对象
- repositoryrepository实现
- converter:实体和领域对象转换器
### manage-common
通用工具和配置模块,包含:
- 工具类:SnowflakeId等
- 通用DTOPageRequest、PageResponse
- 基础配置:JwtProperties、CacheConfig
## 依赖关系
```
manage-gateway → 无依赖(独立模块)
manage-app → manage-sys + manage-db
manage-sys → manage-common
manage-db → manage-sys
manage-common → 无依赖
```
## 依赖倒置实现
通过manage-app模块的依赖注入,实现依赖倒置:
- sys模块定义repository接口
- db模块实现repository接口
- app模块通过@ComponentScan扫描db模块的repository实现
- app模块通过@EnableR2dbcRepositories启用R2DBC repository
- common模块定义ExceptionLogService接口
- sys模块实现ExceptionLogService接口
- app模块通过配置注册ExceptionLogService实现
```
**Step 2: 提交文档**
```bash
git add docs/architecture/module-architecture.md
git commit -m "docs: add module architecture documentation"
```
---
## 验证清单
### 编译验证
- [ ] manage-common编译成功
- [ ] manage-sys编译成功
- [ ] manage-db编译成功
- [ ] manage-app编译成功
- [ ] manage-gateway编译成功
### 功能验证
- [ ] 应用启动成功
- [ ] 数据库连接正常
- [ ] API访问正常
- [ ] WebSocket连接正常
- [ ] 安全认证正常
- [ ] 限流功能正常
### 依赖验证
- [ ] manage-sys不依赖manage-db
- [ ] manage-db依赖manage-sys
- [ ] manage-app依赖manage-sys和manage-db
- [ ] manage-gateway无依赖
### 测试验证
- [ ] 单元测试全部通过
- [ ] 集成测试全部通过
- [ ] E2E测试全部通过
---
## 回滚计划
如果重构过程中出现问题,可以使用以下命令回滚:
```bash
# 回滚到重构前的状态
git reset --hard <commit-hash-before-refactoring>
# 或者使用git reflog查找之前的提交
git reflog
git reset --hard HEAD@{n}
```
---
## 注意事项
1. **循环依赖**:确保manage-sys不依赖manage-db
2. **包声明**:移动文件后记得更新包声明
3. **import语句**:更新所有import语句以匹配新的包结构
4. **配置文件**:确保application.yml中的配置正确
5. **组件扫描**:确保ManageApplication.java中的@ComponentScan配置正确
6. **测试覆盖**:重构后确保所有测试仍然通过
@@ -0,0 +1,23 @@
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active:default}
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5,0.95,0.99
sla:
http.server.requests: 100ms,200ms,500ms,1s,2s
enable:
jvm: true
process: true
system: true
+20
View File
@@ -21,6 +21,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
@@ -37,6 +41,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
@@ -51,6 +59,18 @@
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -9,6 +9,12 @@ import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置类
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
@EnableCaching
public class CacheConfig {
@@ -4,6 +4,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
/**
* JWT配置属性类
*
* @author 张翔
* @date 2026-03-13
*/
@Component
@ConfigurationProperties(prefix = "jwt")
@Validated
@@ -0,0 +1,42 @@
package cn.novalon.manage.common.dao;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 查询字段注解
*
* @author 张翔
* @date 2026-03-13
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QueryField {
String propName() default "";
String blurry() default "";
Type type() default Type.EQUAL;
Type orPropVal() default Type.EQUAL;
String[] orPropNames() default {};
enum Type {
EQUAL,
GREATER_THAN,
LESS_THAN,
LESS_THAN_NQ,
INNER_LIKE,
LEFT_LIKE,
NOT_LEFT_LIKE,
RIGHT_LIKE,
IN,
OR,
IS_NULL,
IS_NOT_NULL
}
}
@@ -0,0 +1,149 @@
package cn.novalon.manage.common.dao;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* 查询工具类
*
* @author 张翔
* @date 2026-03-13
*/
public class QueryUtil {
private static final Logger log = LoggerFactory.getLogger(QueryUtil.class);
public static <Q> Query getQuery(Q query) {
return getQuery(query, true);
}
public static <Q> Query getQueryAll(Q query) {
return getQuery(query, false);
}
public static <Q> Query getQuery(Q query, Boolean enabled) {
Criteria criteria = Criteria.empty();
if (enabled) {
criteria = criteria.and("deletedAt").isNull();
}
if (query == null) {
return Query.query(criteria);
}
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
for (Field field : fields) {
boolean accessible = Modifier.isStatic(field.getModifiers()) ? field.canAccess(null)
: field.canAccess(query);
field.setAccessible(true);
QueryField q = field.getAnnotation(QueryField.class);
if (q != null) {
String propName = q.propName();
String blurry = q.blurry();
String attributeName = isBlank(propName) ? field.getName() : propName;
Object val = field.get(query);
if (val == null || "".equals(val)) {
continue;
}
if (StringUtils.isNotBlank(blurry)) {
String[] blurrys = blurry.split(",");
Criteria orCriteria = Criteria.empty();
for (String s : blurrys) {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
criteria = criteria.and(orCriteria);
continue;
}
switch (q.type()) {
case EQUAL:
criteria = criteria.and(attributeName).is(val);
break;
case GREATER_THAN:
criteria = criteria.and(attributeName).greaterThanOrEquals(val);
break;
case LESS_THAN:
criteria = criteria.and(attributeName).lessThanOrEquals(val);
break;
case LESS_THAN_NQ:
criteria = criteria.and(attributeName).lessThan(val);
break;
case INNER_LIKE:
criteria = criteria.and(attributeName).like("%" + val + "%");
break;
case LEFT_LIKE:
criteria = criteria.and(attributeName).like("%" + val);
break;
case NOT_LEFT_LIKE:
criteria = criteria.and(attributeName).notLike("%" + val);
break;
case RIGHT_LIKE:
criteria = criteria.and(attributeName).like(val + "%");
break;
case IN:
if (val instanceof Collection && CollectionUtils.isNotEmpty((Collection<?>) val)) {
criteria = criteria.and(attributeName).in((Collection<?>) val);
}
break;
case OR:
QueryField.Type orValue = q.orPropVal();
String[] orPropNames = q.orPropNames();
Criteria orPredicate = Criteria.empty();
if (QueryField.Type.IS_NULL.equals(orValue)) {
for (String prop : orPropNames) {
orPredicate = orPredicate.or(prop).isNull();
}
}
if (QueryField.Type.IS_NOT_NULL.equals(orValue)) {
for (String prop : orPropNames) {
orPredicate = orPredicate.or(prop).isNotNull();
}
}
criteria = criteria.and(orPredicate);
break;
case IS_NULL:
criteria = criteria.and(attributeName).isNull();
break;
case IS_NOT_NULL:
criteria = criteria.and(attributeName).isNotNull();
break;
}
}
field.setAccessible(accessible);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return Query.query(criteria);
}
public static boolean isBlank(final CharSequence cs) {
int strLen;
if (cs == null || (strLen = cs.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return false;
}
private static List<Field> getAllFields(Class<?> clazz, List<Field> fields) {
if (clazz != null) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
}
@@ -1,67 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
/**
* @author zhangxiang
* @version 1.0
* @description 基础领域对象
* @date 2026/03/11
**/
public abstract class BaseDomain {
protected Long id;
protected String createBy;
protected String updateBy;
protected LocalDateTime createdAt;
protected LocalDateTime updatedAt;
protected LocalDateTime deletedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public String getUpdateBy() {
return updateBy;
}
public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}
@@ -1,117 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class Dictionary {
private Long id;
private String type;
private String code;
private String name;
private String value;
private String remark;
private Integer sort;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Dictionary() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public String getUpdateBy() {
return updateBy;
}
public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}
@@ -1,86 +0,0 @@
package cn.novalon.manage.common.domain;
public class OperationLog extends BaseDomain {
private String username;
private String operation;
private String method;
private String params;
private String result;
private String ip;
private Long duration;
private String status;
private String errorMsg;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getParams() {
return params;
}
public void setParams(String params) {
this.params = params;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Long getDuration() {
return duration;
}
public void setDuration(Long duration) {
this.duration = duration;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
}
@@ -1,38 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysConfig {
private Long id;
private String configName;
private String configKey;
private String configValue;
private String configType;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getConfigName() { return configName; }
public void setConfigName(String configName) { this.configName = configName; }
public String getConfigKey() { return configKey; }
public void setConfigKey(String configKey) { this.configKey = configKey; }
public String getConfigValue() { return configValue; }
public void setConfigValue(String configValue) { this.configValue = configValue; }
public String getConfigType() { return configType; }
public void setConfigType(String configType) { this.configType = configType; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}
@@ -1,53 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysDictData {
private Long id;
private Long dictTypeId;
private String dictLabel;
private String dictValue;
private Integer dictSort;
private String dictType;
private String cssClass;
private String listClass;
private String isDefault;
private String status;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getDictTypeId() { return dictTypeId; }
public void setDictTypeId(Long dictTypeId) { this.dictTypeId = dictTypeId; }
public String getDictLabel() { return dictLabel; }
public void setDictLabel(String dictLabel) { this.dictLabel = dictLabel; }
public String getDictValue() { return dictValue; }
public void setDictValue(String dictValue) { this.dictValue = dictValue; }
public Integer getDictSort() { return dictSort; }
public void setDictSort(Integer dictSort) { this.dictSort = dictSort; }
public String getDictType() { return dictType; }
public void setDictType(String dictType) { this.dictType = dictType; }
public String getCssClass() { return cssClass; }
public void setCssClass(String cssClass) { this.cssClass = cssClass; }
public String getListClass() { return listClass; }
public void setListClass(String listClass) { this.listClass = listClass; }
public String getIsDefault() { return isDefault; }
public void setIsDefault(String isDefault) { this.isDefault = isDefault; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}
@@ -1,38 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysDictType {
private Long id;
private String dictName;
private String dictType;
private String status;
private String remark;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDictName() { return dictName; }
public void setDictName(String dictName) { this.dictName = dictName; }
public String getDictType() { return dictType; }
public void setDictType(String dictType) { this.dictType = dictType; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}
@@ -1,38 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysExceptionLog {
private Long id;
private String username;
private String title;
private String exceptionName;
private String methodName;
private String methodParams;
private String exceptionMsg;
private String exceptionStack;
private String ip;
private LocalDateTime createTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getExceptionName() { return exceptionName; }
public void setExceptionName(String exceptionName) { this.exceptionName = exceptionName; }
public String getMethodName() { return methodName; }
public void setMethodName(String methodName) { this.methodName = methodName; }
public String getMethodParams() { return methodParams; }
public void setMethodParams(String methodParams) { this.methodParams = methodParams; }
public String getExceptionMsg() { return exceptionMsg; }
public void setExceptionMsg(String exceptionMsg) { this.exceptionMsg = exceptionMsg; }
public String getExceptionStack() { return exceptionStack; }
public void setExceptionStack(String exceptionStack) { this.exceptionStack = exceptionStack; }
public String getIp() { return ip; }
public void setIp(String ip) { this.ip = ip; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
@@ -1,38 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysFile {
private Long id;
private String fileName;
private String filePath;
private String fileSize;
private String fileType;
private String storageType;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getFileSize() { return fileSize; }
public void setFileSize(String fileSize) { this.fileSize = fileSize; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getStorageType() { return storageType; }
public void setStorageType(String storageType) { this.storageType = storageType; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}
@@ -1,35 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysLoginLog {
private Long id;
private String username;
private String ip;
private String location;
private String browser;
private String os;
private String status;
private String message;
private LocalDateTime loginTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getIp() { return ip; }
public void setIp(String ip) { this.ip = ip; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getBrowser() { return browser; }
public void setBrowser(String browser) { this.browser = browser; }
public String getOs() { return os; }
public void setOs(String os) { this.os = os; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public LocalDateTime getLoginTime() { return loginTime; }
public void setLoginTime(LocalDateTime loginTime) { this.loginTime = loginTime; }
}
@@ -1,97 +0,0 @@
package cn.novalon.manage.common.domain;
import java.util.List;
public class SysMenu extends BaseDomain {
private String menuName;
private Long parentId;
private Integer orderNum;
private String menuType;
private String perms;
private String component;
private String status;
private String createBy;
private String updateBy;
private List<SysMenu> children;
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public Integer getOrderNum() {
return orderNum;
}
public void setOrderNum(Integer orderNum) {
this.orderNum = orderNum;
}
public String getMenuType() {
return menuType;
}
public void setMenuType(String menuType) {
this.menuType = menuType;
}
public String getPerms() {
return perms;
}
public void setPerms(String perms) {
this.perms = perms;
}
public String getComponent() {
return component;
}
public void setComponent(String component) {
this.component = component;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public String getUpdateBy() {
return updateBy;
}
public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}
public List<SysMenu> getChildren() {
return children;
}
public void setChildren(List<SysMenu> children) {
this.children = children;
}
}
@@ -1,38 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysNotice {
private Long id;
private String noticeTitle;
private String noticeType;
private String noticeContent;
private String status;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNoticeTitle() { return noticeTitle; }
public void setNoticeTitle(String noticeTitle) { this.noticeTitle = noticeTitle; }
public String getNoticeType() { return noticeType; }
public void setNoticeType(String noticeType) { this.noticeType = noticeType; }
public String getNoticeContent() { return noticeContent; }
public void setNoticeContent(String noticeContent) { this.noticeContent = noticeContent; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}
@@ -1,75 +0,0 @@
package cn.novalon.manage.common.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import java.time.LocalDateTime;
/**
* @author zhangxiang
* @version 1.0
* @description 角色领域对象
* @date 2026/03/11
**/
public class SysRole extends BaseDomain {
private String roleName;
private String roleKey;
private Integer roleSort;
private Integer status;
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getRoleKey() {
return roleKey;
}
public void setRoleKey(String roleKey) {
this.roleKey = roleKey;
}
public Integer getRoleSort() {
return roleSort;
}
public void setRoleSort(Integer roleSort) {
this.roleSort = roleSort;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除角色
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
/**
* 恢复角色
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -1,77 +0,0 @@
package cn.novalon.manage.common.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import java.time.LocalDateTime;
/**
* @author zhangxiang
* @version 1.0
* @description 用户领域对象
* @date 2026/03/11
**/
public class SysUser extends BaseDomain {
private String username;
private String password;
private String email;
private Long roleId;
private Integer status;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除用户
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
@@ -1,29 +0,0 @@
package cn.novalon.manage.common.domain;
import java.time.LocalDateTime;
public class SysUserMessage {
private Long id;
private Long userId;
private String title;
private String content;
private String messageType;
private String isRead;
private LocalDateTime createTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getMessageType() { return messageType; }
public void setMessageType(String messageType) { this.messageType = messageType; }
public String getIsRead() { return isRead; }
public void setIsRead(String isRead) { this.isRead = isRead; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
}
@@ -1,11 +1,11 @@
package cn.novalon.manage.common.domain.query;
/**
* @author zhangxiang
* @version 1.0
* @description 菜单查询对象
* @date 2026/03/11
**/
* 菜单查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysMenuQuery {
private String menuName;
@@ -1,11 +1,11 @@
package cn.novalon.manage.common.domain.query;
/**
* @author zhangxiang
* @version 1.0
* @description 角色查询对象
* @date 2026/03/11
**/
* 角色查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysRoleQuery {
private String roleName;
@@ -1,17 +1,18 @@
package cn.novalon.manage.common.domain.query;
/**
* @author zhangxiang
* @version 1.0
* @description 用户查询对象
* @date 2026/03/11
**/
* 用户查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysUserQuery {
private String username;
private String email;
private Integer status;
private Long roleId;
private String keyword;
public String getUsername() {
return username;
@@ -44,4 +45,12 @@ public class SysUserQuery {
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -1,5 +1,11 @@
package cn.novalon.manage.common.dto;
/**
* 分页请求参数封装类
*
* @author 张翔
* @date 2026-03-13
*/
public class PageRequest {
private int page = 0;
private int size = 10;
@@ -2,6 +2,12 @@ package cn.novalon.manage.common.dto;
import java.util.List;
/**
* 分页响应结果封装类
*
* @author 张翔
* @date 2026-03-13
*/
public class PageResponse<T> {
private List<T> content;
private int totalPages;
@@ -1,5 +1,11 @@
package cn.novalon.manage.common.exception;
/**
* 业务异常类
*
* @author 张翔
* @date 2026-03-13
*/
public class BusinessException extends RuntimeException {
private final String code;
@@ -0,0 +1,18 @@
package cn.novalon.manage.common.handler;
import reactor.core.publisher.Mono;
/**
* 异常日志服务接口
*
* 文件定义:定义异常日志记录的抽象接口
* 涉及业务:异常日志记录、错误追踪
* 算法:使用响应式编程实现异步日志记录
*
* @author 张翔
* @date 2026-03-13
*/
public interface ExceptionLogService {
Mono<Void> logException(String title, String exceptionName, String exceptionMsg,
String methodName, String ip, String stackTrace);
}
@@ -0,0 +1,181 @@
package cn.novalon.manage.common.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
* 文件定义:统一处理系统中抛出的各种异常,返回标准化的错误响应
* 涉及业务:异常捕获、错误日志记录、错误响应格式化
* 算法:使用@RestControllerAdvice注解实现全局异常拦截
*
* @author 张翔
* @date 2026-03-13
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final ExceptionLogService exceptionLogService;
public GlobalExceptionHandler(ExceptionLogService exceptionLogService) {
this.exceptionLogService = exceptionLogService;
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex, ServerWebExchange exchange) {
logger.warn("Runtime exception: ", ex);
Map<String, Object> response = new HashMap<>();
if (ex.getMessage() != null && ex.getMessage().contains("not found")) {
response.put("code", HttpStatus.NOT_FOUND.value());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception ex, ServerWebExchange exchange) {
logger.error("Exception occurred: ", ex);
exceptionLogService.logException(
"System Exception",
ex.getClass().getSimpleName(),
ex.getMessage(),
exchange.getRequest().getPath().value(),
getClientIp(exchange),
getStackTrace(ex)
).subscribe();
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
response.put("message", "Internal server error");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex, ServerWebExchange exchange) {
logger.warn("Illegal argument: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", ex.getMessage());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, ServerWebExchange exchange) {
logger.warn("Validation failed: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", "Validation failed");
response.put("timestamp", LocalDateTime.now());
Map<String, String> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (e1, e2) -> e1));
response.put("errors", fieldErrors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(ServerWebInputException.class)
public ResponseEntity<Map<String, Object>> handleServerWebInputException(ServerWebInputException ex, ServerWebExchange exchange) {
logger.warn("Invalid input: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.BAD_REQUEST.value());
response.put("message", "Invalid input");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatusException(ResponseStatusException ex, ServerWebExchange exchange) {
logger.warn("Response status exception: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", ex.getStatusCode().value());
response.put("message", ex.getReason());
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(ex.getStatusCode()).body(response);
}
@ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<Map<String, Object>> handleDuplicateKeyException(DuplicateKeyException ex, ServerWebExchange exchange) {
logger.warn("Duplicate key: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.CONFLICT.value());
response.put("message", "Duplicate key violation");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Map<String, Object>> handleDataIntegrityViolationException(DataIntegrityViolationException ex, ServerWebExchange exchange) {
logger.warn("Data integrity violation: ", ex);
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.CONFLICT.value());
response.put("message", "Data integrity violation");
response.put("timestamp", LocalDateTime.now());
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
private String getClientIp(ServerWebExchange exchange) {
String ip = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = exchange.getRequest().getHeaders().getFirst("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "127.0.0.1";
}
return ip;
}
private String getStackTrace(Exception ex) {
StringBuilder stackTrace = new StringBuilder();
for (StackTraceElement element : ex.getStackTrace()) {
stackTrace.append(element.toString()).append("\n");
}
return stackTrace.toString();
}
}
@@ -0,0 +1,79 @@
package cn.novalon.manage.common.monitoring;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Arrays;
@Aspect
@Component
public class MetricsAspect {
private final MetricsCollector metricsCollector;
public MetricsAspect(MetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
@Pointcut("within(cn.novalon.manage..*) && " +
"@annotation(org.springframework.web.bind.annotation.GetMapping || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void apiMethods() {}
@Around("apiMethods()")
public Object monitorApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
String module = extractModule(className);
String endpoint = className.replace("Handler", "").toLowerCase() + "/" + methodName;
long startTime = System.currentTimeMillis();
boolean success = true;
String errorType = null;
try {
Object result = joinPoint.proceed();
if (result instanceof Mono) {
return ((Mono<?>) result)
.doOnError(error -> {
success = false;
errorType = error.getClass().getSimpleName();
metricsCollector.recordError(module, errorType, error.getMessage());
})
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
metricsCollector.recordApiCall(module, endpoint, "GET", duration, true);
});
}
long duration = System.currentTimeMillis() - startTime;
metricsCollector.recordApiCall(module, endpoint, "GET", duration, true);
return result;
} catch (Exception e) {
success = false;
errorType = e.getClass().getSimpleName();
long duration = System.currentTimeMillis() - startTime;
metricsCollector.recordApiCall(module, endpoint, "GET", duration, false);
metricsCollector.recordError(module, errorType, e.getMessage());
throw e;
}
}
private String extractModule(String className) {
if (className.contains("Notify")) return "notify";
if (className.contains("File")) return "file";
if (className.contains("User")) return "user";
if (className.contains("Role")) return "role";
if (className.contains("Config")) return "config";
if (className.contains("Log")) return "log";
return "system";
}
}
@@ -0,0 +1,88 @@
package cn.novalon.manage.common.monitoring;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class MetricsCollector {
private final MeterRegistry meterRegistry;
public MetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public Counter buildCounter(String name, String description, String... tags) {
return Counter.builder(name)
.description(description)
.tags(tags)
.register(meterRegistry);
}
public Timer buildTimer(String name, String description, String... tags) {
return Timer.builder(name)
.description(description)
.tags(tags)
.register(meterRegistry);
}
public void recordApiCall(String module, String endpoint, String method, long duration, boolean success) {
Timer.builder("api.call.duration")
.description("API call duration")
.tag("module", module)
.tag("endpoint", endpoint)
.tag("method", method)
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
Counter.builder("api.call.count")
.description("API call count")
.tag("module", module)
.tag("endpoint", endpoint)
.tag("method", method)
.tag("status", success ? "success" : "failure")
.register(meterRegistry)
.increment();
}
public void recordDatabaseQuery(String module, String operation, long duration, boolean success) {
Timer.builder("db.query.duration")
.description("Database query duration")
.tag("module", module)
.tag("operation", operation)
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
Counter.builder("db.query.count")
.description("Database query count")
.tag("module", module)
.tag("operation", operation)
.tag("status", success ? "success" : "failure")
.register(meterRegistry)
.increment();
}
public void recordCacheHit(String module, String cacheName, boolean hit) {
Counter.builder("cache.access")
.description("Cache access count")
.tag("module", module)
.tag("cache", cacheName)
.tag("result", hit ? "hit" : "miss")
.register(meterRegistry)
.increment();
}
public void recordError(String module, String errorType, String message) {
Counter.builder("error.count")
.description("Error count")
.tag("module", module)
.tag("type", errorType)
.tag("message", message)
.register(meterRegistry)
.increment();
}
}
@@ -0,0 +1,23 @@
package cn.novalon.manage.common.monitoring;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "novalon-manage-api",
"environment", System.getenv().getOrDefault("ENV", "development")
);
}
@Bean
public MetricsCollector metricsCollector(MeterRegistry meterRegistry) {
return new MetricsCollector(meterRegistry);
}
}
@@ -1,11 +1,11 @@
package cn.novalon.manage.common.util;
/**
* @author zhangxiang
* @version 1.0
* @description 数据库字段名常量
* @date 2026/03/11
**/
* 数据库字段名常量定义
*
* @author 张翔
* @date 2026-03-13
*/
public class FieldConstants {
public static final String USERNAME = "username";
@@ -1,11 +1,11 @@
package cn.novalon.manage.common.util;
/**
* @author zhangxiang
* @version 1.0
* @description 菜单类型常量
* @date 2026/03/11
**/
* 菜单类型常量定义
*
* @author 张翔
* @date 2026-03-13
*/
public class MenuTypeConstants {
public static final String DIRECTORY = "M";
@@ -6,17 +6,14 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
/**
* Twitter的Snowflake算法实现(性能优化版)
*
* 优化点:
* 1. 使用自适应等待策略,减少CPU空转
* 2. 时间戳缓存机制,降低系统调用频率
* 3. 增强CAS重试策略,提升并发性能
* 4. 完善异常处理和资源管理
*
* @author zhangxiang
* @version 2.0
* @date 2026/03/11
* 雪花算法ID生成器
*
* 文件定义:基于Twitter Snowflake算法的分布式唯一ID生成器
* 涉及业务:为系统所有实体生成唯一ID,支持分布式环境下的ID生成
* 算法:使用雪花算法,结合时间戳、机器ID和序列号生成唯一ID,支持高并发场景
*
* @author 张翔
* @date 2026-03-13
*/
public final class SnowflakeId {
@@ -1,11 +1,15 @@
package cn.novalon.manage.common.util;
/**
* @author zhangxiang
* @version 1.0
* @description 状态常量
* @date 2026/03/11
**/
* 状态常量定义
*
* 文件定义:系统通用的状态常量定义类
* 涉及业务:为系统提供统一的状态码定义,包括启用、禁用、删除等状态
* 算法:无复杂算法,主要为常量定义
*
* @author 张翔
* @date 2026-03-13
*/
public class StatusConstants {
public static final Integer DISABLED = 0;