refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage

This commit is contained in:
张翔
2026-04-17 18:35:50 +08:00
parent 666189b676
commit deb961c427
916 changed files with 108360 additions and 38328 deletions
+117
View File
@@ -0,0 +1,117 @@
/*
* Copyright 2007-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "3.1.0";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL =
"https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if(mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if(mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore
}
}
}
System.out.println("- Downloading from: " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if(!outputFile.getParentFile().exists()) {
if(!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}
+2
View File
@@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
+49
View File
@@ -0,0 +1,49 @@
# 多阶段构建优化Dockerfile
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# 复制Maven配置文件和源码
COPY pom.xml .
COPY mvnw .
COPY mvnw.cmd .
COPY .mvn .mvn
# 下载依赖(利用Docker缓存层)
RUN ./mvnw dependency:go-offline -B
# 复制源码并构建
COPY src ./src
RUN ./mvnw clean package -DskipTests
# 运行时镜像
FROM eclipse-temurin:21-jre-jammy
# 设置时区和语言环境
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 创建非root用户运行应用
RUN groupadd -r novalon && useradd -r -g novalon novalon
WORKDIR /app
# 复制构建产物
COPY --from=builder --chown=novalon:novalon /app/target/*.jar app.jar
# 设置JVM参数优化
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom"
# 暴露端口
EXPOSE 8084
# 切换用户
USER novalon
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8084/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
+1
View File
@@ -0,0 +1 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class Test { public static void main(String[] args) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String hash = "$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C"; System.out.println("Match Test@123: " + encoder.matches("Test@123", hash)); } }
+14
View File
@@ -0,0 +1,14 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class TestBCrypt {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String password = "admin123";
String hash = "$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy";
System.out.println("测试密码验证:");
System.out.println("密码: " + password);
System.out.println("哈希: " + hash);
System.out.println("验证结果: " + encoder.matches(password, hash));
}
}
@@ -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. **测试覆盖**:重构后确保所有测试仍然通过
+9
View File
@@ -0,0 +1,9 @@
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY manage-app/target/manage-app-1.0.0.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-manage-api</artifactId>
<version>1.0.0</version>
</parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-app</artifactId>
<packaging>jar</packaging>
<name>Manage App</name>
<description>Application module for Novalon Manage API</description>
<dependencies>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-sys</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-notify</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-file</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.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.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>runtime</scope>
</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>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>cn.novalon.manage.app.ManageApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,26 @@
package cn.novalon.gym.manage.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
@EnableR2dbcRepositories(basePackages = {"cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository"})
public class ManageApplication {
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
public static void main(String[] args) {
logger.info("应用程序启动中...");
logger.info("包扫描路径: cn.novalon.gym.manage");
// 使用简单的启动方式,避免自动配置问题
SpringApplication.run(ManageApplication.class, args);
logger.info("应用程序启动完成");
}
}
@@ -0,0 +1,42 @@
package cn.novalon.gym.manage.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 最小化应用程序启动类
* 避免复杂的自动配置问题,专注于核心功能
*/
@SpringBootApplication(
scanBasePackages = {
"cn.novalon.gym.manage.app.config",
"cn.novalon.gym.manage.app.controller",
"cn.novalon.gym.manage.app.service"
}
)
public class MinimalApplication {
private static final Logger logger = LoggerFactory.getLogger(MinimalApplication.class);
public static void main(String[] args) {
logger.info("最小化应用程序启动中...");
// 设置系统属性,避免自动配置问题
System.setProperty("spring.autoconfigure.exclude",
"org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration");
// 禁用复杂的自动配置
System.setProperty("spring.main.lazy-initialization", "true");
System.setProperty("spring.main.banner-mode", "off");
try {
SpringApplication.run(MinimalApplication.class, args);
logger.info("最小化应用程序启动完成");
} catch (Exception e) {
logger.error("应用程序启动失败: {}", e.getMessage());
e.printStackTrace();
}
}
}
@@ -0,0 +1,32 @@
package cn.novalon.gym.manage.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
/**
* 简化的应用程序启动类
* 避免复杂的自动配置问题
*/
@SpringBootApplication(
scanBasePackages = "cn.novalon.gym.manage.app",
exclude = {ReactiveUserDetailsServiceAutoConfiguration.class}
)
public class SimpleManageApplication {
private static final Logger logger = LoggerFactory.getLogger(SimpleManageApplication.class);
public static void main(String[] args) {
logger.info("简化版应用程序启动中...");
logger.info("包扫描路径: cn.novalon.gym.manage.app");
// 设置系统属性,避免自动配置问题
System.setProperty("spring.autoconfigure.exclude",
"org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration");
SpringApplication.run(SimpleManageApplication.class, args);
logger.info("简化版应用程序启动完成");
}
}
@@ -0,0 +1,57 @@
package cn.novalon.gym.manage.app.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Jackson配置类
*
* 用于统一时间格式化配置
*
* @author 张翔
* @date 2026-03-26
*/
@Configuration
public class JacksonConfig {
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
JavaTimeModule javaTimeModule = new JavaTimeModule();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
objectMapper.registerModule(javaTimeModule);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return objectMapper;
}
@Bean
public Jackson2JsonEncoder jackson2JsonEncoder(ObjectMapper objectMapper) {
return new Jackson2JsonEncoder(objectMapper);
}
@Bean
public Jackson2JsonDecoder jackson2JsonDecoder(ObjectMapper objectMapper) {
return new Jackson2JsonDecoder(objectMapper);
}
}
@@ -0,0 +1,19 @@
package cn.novalon.gym.manage.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
@Configuration
public class MultipartConfig {
@Bean
public MultipartHttpMessageReader multipartHttpMessageReader() {
DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader();
partReader.setMaxHeadersSize(8192);
partReader.setMaxDiskUsagePerPart(10 * 1024 * 1024);
partReader.setEnableLoggingRequestDetails(true);
return new MultipartHttpMessageReader(partReader);
}
}
@@ -0,0 +1,60 @@
package cn.novalon.gym.manage.app.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
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;
import java.util.Arrays;
import java.util.List;
/**
* OpenAPI配置类
*
* @author 张翔
* @date 2026-03-14
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Novalon Manage System API")
.version("1.0.0")
.description("Novalon 管理系统 RESTful API 文档")
.contact(new Contact()
.name("Novalon Team")
.email("support@novalon.cn"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.servers(List.of(
new Server().url("http://localhost:8084").description("开发环境"),
new Server().url("https://api.novalon.cn").description("生产环境")))
.tags(Arrays.asList(
new Tag().name("用户管理").description("用户相关操作"),
new Tag().name("角色管理").description("角色相关操作"),
new Tag().name("配置管理").description("系统配置相关操作"),
new Tag().name("字典管理").description("字典数据相关操作"),
new Tag().name("通知管理").description("系统通知相关操作"),
new Tag().name("文件管理").description("文件上传下载相关操作"),
new Tag().name("日志管理").description("操作日志相关操作"),
new Tag().name("认证管理").description("登录认证相关操作"),
new Tag().name("统计信息").description("系统统计相关操作")));
}
@Bean
public GroupedOpenApi allApi() {
return GroupedOpenApi.builder()
.group("all")
.pathsToMatch("/api/**")
.build();
}
}
@@ -0,0 +1,41 @@
package cn.novalon.gym.manage.app.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class RateLimitConfig {
@Value("${rate.limit.limit-for-period:100}")
private int limitForPeriod;
@Value("${rate.limit.limit-refresh-period:1s}")
private Duration limitRefreshPeriod;
@Value("${rate.limit.timeout-duration:0}")
private Duration timeoutDuration;
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(limitForPeriod)
.limitRefreshPeriod(limitRefreshPeriod)
.timeoutDuration(timeoutDuration)
.build();
return RateLimiterRegistry.of(config);
}
@Bean
@Qualifier("apiRateLimiter")
public RateLimiter apiRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("apiRateLimiter");
}
}
@@ -0,0 +1,198 @@
package cn.novalon.gym.manage.app.config;
import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler;
import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler;
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler;
import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler;
import cn.novalon.gym.manage.sys.handler.log.SysLogHandler;
import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler;
import cn.novalon.gym.manage.sys.handler.menu.MenuHandler;
import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler;
import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler;
import cn.novalon.gym.manage.sys.handler.stats.StatsHandler;
import cn.novalon.gym.manage.sys.handler.user.SysUserHandler;
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
import cn.novalon.gym.manage.file.handler.SysFileHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* 系统路由配置类
*
* 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法
* 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由
* 算法:使用RouterFunctions.route()构建函数式路由规则
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
public class SystemRouter {
@Bean
public RouterFunction<ServerResponse> systemRoutes(
DictionaryHandler dictionaryHandler,
SysUserHandler userHandler,
MenuHandler menuHandler,
SysRoleHandler roleHandler,
SysConfigHandler configHandler,
SysLogHandler logHandler,
OperationLogHandler operationLogHandler,
SysAuthHandler authHandler,
StatsHandler statsHandler,
SysDictHandler dictHandler,
SysNoticeHandler noticeHandler,
SysUserMessageHandler messageHandler,
SysFileHandler fileHandler,
SysPermissionHandler permissionHandler,
PasswordDiagnosticHandler passwordDiagnosticHandler) {
return route()
// ========== 诊断路由 ==========
.GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose)
// ========== 字典路由 ==========
.GET("/api/dictionaries", dictionaryHandler::getAllDictionaries)
.GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById)
.GET("/api/dictionaries/type/{type}", dictionaryHandler::getDictionariesByType)
.GET("/api/dictionaries/check/exists", dictionaryHandler::checkTypeAndCodeExists)
.POST("/api/dictionaries", dictionaryHandler::createDictionary)
.PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary)
.DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary)
// ========== 用户路由 ==========
.GET("/api/users", userHandler::getAllUsers)
.GET("/api/users/page", userHandler::getUsersByPage)
.GET("/api/users/count", userHandler::getUserCount)
.GET("/api/users/username/{username}", userHandler::getUserByUsername)
.GET("/api/users/check/username", userHandler::checkUsernameExists)
.GET("/api/users/check/email", userHandler::checkEmailExists)
.POST("/api/users", userHandler::createUser)
.GET("/api/users/{id}", userHandler::getUserById)
.PUT("/api/users/{id}", userHandler::updateUser)
.DELETE("/api/users/{id}", userHandler::deleteUser)
.POST("/api/users/{id}/action/change-password", userHandler::changePassword)
.POST("/api/users/{id}/action/logical-delete", userHandler::logicalDeleteUser)
.POST("/api/users/logical-delete", userHandler::logicalDeleteUsers)
.POST("/api/users/action/restore", userHandler::restoreUsers)
.POST("/api/users/{id}/action/restore", userHandler::restoreUser)
.GET("/api/users/{id}/roles", userHandler::getUserRoles)
.POST("/api/users/{id}/roles", userHandler::assignRoles)
// ========== 菜单路由 ==========
.GET("/api/menus", menuHandler::getAllMenus)
.GET("/api/menus/tree", menuHandler::getMenuTree)
.GET("/api/menus/{id}", menuHandler::getMenuById)
.POST("/api/menus", menuHandler::createMenu)
.PUT("/api/menus/{id}", menuHandler::updateMenu)
.DELETE("/api/menus/{id}", menuHandler::deleteMenu)
// ========== 角色路由 ==========
.GET("/api/roles", roleHandler::getAllRoles)
.GET("/api/roles/page", roleHandler::getRolesByPage)
.GET("/api/roles/count", roleHandler::getRoleCount)
.GET("/api/roles/name/{roleName}", roleHandler::getRoleByName)
.GET("/api/roles/check-name", roleHandler::checkNameExists)
.GET("/api/roles/{id}", roleHandler::getRoleById)
.POST("/api/roles", roleHandler::createRole)
.PUT("/api/roles/{id}", roleHandler::updateRole)
.DELETE("/api/roles/{id}", roleHandler::deleteRole)
.POST("/api/roles/{id}/restore", roleHandler::restoreRole)
.GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
.POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
// ========== 配置路由 ==========
.GET("/api/config", configHandler::getAllConfigs)
.GET("/api/config/{id}", configHandler::getConfigById)
.GET("/api/config/key/{configKey}", configHandler::getConfigByKey)
.POST("/api/config", configHandler::createConfig)
.PUT("/api/config/{id}", configHandler::updateConfig)
.DELETE("/api/config/{id}", configHandler::deleteConfig)
// ========== 日志路由 ==========
.GET("/api/logs/login", logHandler::getAllLoginLogs)
.GET("/api/logs/login/page", logHandler::getLoginLogsByPage)
.GET("/api/logs/login/count", logHandler::getLoginLogCount)
.GET("/api/logs/login/today/count", logHandler::getTodayLoginCount)
.GET("/api/logs/login/recent", logHandler::getRecentLoginLogs)
.GET("/api/logs/login/{id}", logHandler::getLoginLogById)
.POST("/api/logs/login", logHandler::createLoginLog)
.GET("/api/logs/exception", logHandler::getAllExceptionLogs)
.GET("/api/logs/exception/page", logHandler::getExceptionLogsByPage)
.GET("/api/logs/exception/count", logHandler::getExceptionLogCount)
.GET("/api/logs/exception/{id}", logHandler::getExceptionLogById)
.POST("/api/logs/exception", logHandler::createExceptionLog)
.GET("/api/logs/operation", operationLogHandler::getAllOperationLogs)
.GET("/api/logs/operation/export", operationLogHandler::exportOperationLogs)
.GET("/api/logs/operation/page", operationLogHandler::getOperationLogsByPage)
.GET("/api/logs/operation/count", operationLogHandler::getOperationLogCount)
.GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById)
.POST("/api/logs/operation", operationLogHandler::createOperationLog)
// ========== 认证路由 ==========
.POST("/api/auth/login", authHandler::login)
.POST("/api/auth/register", authHandler::register)
.POST("/api/auth/logout", authHandler::logout)
// ========== 统计路由 ==========
.GET("/api/stats/overview", statsHandler::getOverview)
// ========== 数据字典路由 ==========
.GET("/api/dict/types", dictHandler::getAllDictTypes)
.GET("/api/dict/types/{id}", dictHandler::getDictTypeById)
.GET("/api/dict/types/type/{dictType}", dictHandler::getDictTypeByType)
.POST("/api/dict/types", dictHandler::createDictType)
.PUT("/api/dict/types/{id}", dictHandler::updateDictType)
.DELETE("/api/dict/types/{id}", dictHandler::deleteDictType)
.GET("/api/dict/data", dictHandler::getAllDictData)
.GET("/api/dict/data/type/{dictType}", dictHandler::getDictDataByType)
.GET("/api/dict/data/{id}", dictHandler::getDictDataById)
.POST("/api/dict/data", dictHandler::createDictData)
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
// ========== 公告路由 ==========
.GET("/api/notices", noticeHandler::getAllNotices)
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
.GET("/api/notices/status/{status}", noticeHandler::getNoticesByStatus)
.POST("/api/notices", noticeHandler::createNotice)
.PUT("/api/notices/{id}", noticeHandler::updateNotice)
.DELETE("/api/notices/{id}", noticeHandler::deleteNotice)
// ========== 消息路由 ==========
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
.GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
.POST("/api/messages", messageHandler::createMessage)
.PUT("/api/messages/{id}/read", messageHandler::markAsRead)
.DELETE("/api/messages/{id}", messageHandler::deleteMessage)
// ========== 文件路由 ==========
.GET("/api/files", fileHandler::getAllFiles)
.GET("/api/files/{id}", fileHandler::getFileById)
.POST("/api/files/upload", fileHandler::uploadFile)
.GET("/api/files/{id}/download", fileHandler::downloadFile)
.GET("/api/files/download/{fileName}", fileHandler::downloadFileByName)
.GET("/api/files/{id}/preview", fileHandler::previewFile)
.GET("/api/files/preview/{fileName}", fileHandler::previewFileByName)
.DELETE("/api/files/{id}", fileHandler::deleteFile)
// ========== 权限路由 ==========
.GET("/api/permissions", permissionHandler::getAllPermissions)
.GET("/api/permissions/{id}", permissionHandler::getPermissionById)
.GET("/api/permissions/code/{code}", permissionHandler::getPermissionByCode)
.GET("/api/permissions/check-code", permissionHandler::checkCodeExists)
.GET("/api/permissions/count", permissionHandler::getPermissionCount)
.POST("/api/permissions", permissionHandler::createPermission)
.PUT("/api/permissions/{id}", permissionHandler::updatePermission)
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
.build();
}
}
@@ -0,0 +1,20 @@
package cn.novalon.gym.manage.app.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;
/**
* WebFlux配置类
*
* @author 张翔
* @date 2026-03-14
*/
@Configuration
public class WebFluxConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024);
}
}
@@ -0,0 +1,5 @@
cn.novalon.manage.app.config.OpenApiConfig
cn.novalon.manage.app.config.WebFluxConfig
cn.novalon.manage.app.config.SystemRouter
cn.novalon.manage.app.config.MultipartConfig
cn.novalon.manage.app.config.RateLimitConfig
@@ -0,0 +1,22 @@
spring:
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
rate:
limit:
limit-for-period: 10000
limit-refresh-period: 1s
timeout-duration: 0
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
org.springframework.web: TRACE
@@ -0,0 +1,36 @@
# 本地开发环境配置
spring:
config:
activate:
on-profile: local
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
initial-size: 5
max-size: 20
max-idle-time: 10m
max-life-time: 30m
acquire-timeout: 3s
datasource:
url: jdbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
validate-on-migrate: true
sql:
init:
mode: always
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
org.flywaydb: DEBUG
@@ -0,0 +1,17 @@
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
metrics:
enabled: true
info:
env:
enabled: true
metrics:
export:
simple:
enabled: true
@@ -0,0 +1,12 @@
spring:
r2dbc:
url: r2dbc:postgresql://postgres:5432/novalon_manage
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
flyway:
enabled: true
logging:
level:
cn.novalon.manage: INFO
org.springframework.r2dbc: INFO
@@ -0,0 +1,62 @@
server:
port: 8084
spring:
application:
name: manage-app
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
initial-size: 5
max-size: 20
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: never
security:
user:
name: disabled
password: disabled
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
base-path: /actuator
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
org.flywaydb: INFO
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,68 @@
server:
port: 8084
spring:
application:
name: gym-manage-api
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
pool:
initial-size: 10
max-size: 50
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
validate-on-migrate: true
security:
user:
name: disabled
password: disabled
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers
base-path: /actuator
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
jwt:
secret: ${JWT_SECRET:U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4}
expiration: ${JWT_EXPIRATION:86400000}
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,30 @@
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ ███╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ██╗ ██████╗ ███╗ ██╗ ║
║ ████╗ ██║██╔═══██╗██║ ██║██╔══██╗██║ ██╔═══██╗████╗ ██║ ║
║ ██╔██╗ ██║██║ ██║██║ ██║███████║██║ ██║ ██║██╔██╗ ██║ ║
║ ██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══██║██║ ██║ ██║██║╚██╗██║ ║
║ ██║ ╚████║╚██████╔╝ ╚████╔╝ ██║ ██║███████╗╚██████╔╝██║ ╚████║ ║
║ ╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ║
║ ║
║ ███╗ ███╗ █████╗ ███╗ ██╗ █████╗ ██████╗ ███████╗ ║
║ ████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝ ██╔════╝ ║
║ ██╔████╔██║███████║██╔██╗ ██║███████║██║ ███╗█████╗ ║
║ ██║╚██╔╝██║██╔══██║██║╚██╗██║██╔══██║██║ ██║██╔══╝ ║
║ ██║ ╚═╝ ██║██║ ██║██║ ╚████║██║ ██║╚██████╔╝███████╗ ║
║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ║
║ ║
║ ███████╗██╗ ██╗███████╗████████╗███████╗███╗ ███╗ ║
║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══██╔══╝██╔════╝████╗ ████║ ║
║ ███████╗ ╚████╔╝ ███████╗ ██║ █████╗ ██╔████╔██║ ║
║ ╚════██║ ╚██╔╝ ╚════██║ ██║ ██╔══╝ ██║╚██╔╝██║ ║
║ ███████║ ██║ ███████║ ██║ ███████╗██║ ╚═╝ ██║ ║
║ ╚══════╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
:: Novalon Manage System ::
Version: ${application.version:Unknown}
Spring Boot: ${spring-boot.version}
Java: ${java.version}
PID: ${PID}
@@ -0,0 +1,84 @@
-- H2数据库测试数据
-- 用于测试环境
-- 插入测试角色
MERGE INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
KEY(id)
VALUES
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system'),
(4, '访客', 'guest', 4, 1, 'system', 'system');
-- 插入测试用户
-- BCrypt哈希值对应明文密码: Test@123
MERGE INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
KEY(id)
VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
(3, 'normaluser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'),
(4, 'guestuser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'),
(5, 'disableduser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system'),
(10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system');
-- 为用户分配角色
INSERT INTO user_role (user_id, role_id, created_by)
VALUES
(1, 1, 'system'),
(2, 2, 'system'),
(3, 3, 'system'),
(4, 4, 'system'),
(10, 1, 'system');
-- 插入测试菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, update_by)
VALUES
(1, '系统管理', 0, 1, '/system', 'Layout', 'M', '1', '1', '', 'system', 'system', 'system'),
(2, '用户管理', 1, 1, 'user', 'system/user/index', 'C', '1', '1', 'system:user:list', 'user', 'system', 'system'),
(3, '角色管理', 1, 2, 'role', 'system/role/index', 'C', '1', '1', 'system:role:list', 'role', 'system', 'system'),
(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '1', '1', 'system:menu:list', 'menu', 'system', 'system'),
(5, '测试菜单', 0, 99, '/test', 'Layout', 'M', '1', '1', '', 'test', 'system', 'system'),
(6, '用户测试', 5, 1, 'user-test', 'system/user-test/index', 'C', '1', '1', 'system:user:test', 'user', 'system', 'system');
-- 插入测试权限
INSERT INTO sys_permission (id, permission_name, permission_code, resource, action, description, status, create_by, update_by)
VALUES
(1, '系统管理', 'system:manage', '/api/system', 'GET', '系统管理权限', 1, 'system', 'system'),
(2, '用户管理', 'system:user:manage', '/api/users', 'GET', '用户管理权限', 1, 'system', 'system'),
(3, '用户查询', 'system:user:list', '/api/users', 'GET', '用户查询权限', 1, 'system', 'system'),
(4, '用户新增', 'system:user:add', '/api/users', 'POST', '用户新增权限', 1, 'system', 'system'),
(5, '用户编辑', 'system:user:edit', '/api/users', 'PUT', '用户编辑权限', 1, 'system', 'system'),
(6, '用户删除', 'system:user:delete', '/api/users', 'DELETE', '用户删除权限', 1, 'system', 'system'),
(7, '测试权限', 'test:permission', '/api/test', 'GET', '测试权限', 1, 'system', 'system'),
(8, '用户测试权限', 'system:user:test', '/api/users/test', 'GET', '用户测试权限', 1, 'system', 'system');
-- 为角色分配权限
INSERT INTO sys_role_permission (role_id, permission_id, created_by, updated_by)
SELECT 1, id, 'system', 'system' FROM sys_permission
UNION ALL
SELECT 2, id, 'system', 'system' FROM sys_permission WHERE id IN (7, 8);
-- 插入字典类型
INSERT INTO sys_dict_type (id, dict_name, dict_type, status, remark, create_by, update_by)
VALUES
(1, '用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
(2, '菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
(3, '角色状态', 'role_status', '0', '角色状态列表', 'system', 'system');
-- 插入字典数据
INSERT INTO sys_dict_data (id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by)
VALUES
(1, 1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, 2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
(3, 1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(4, 2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
(5, 1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(6, 2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system');
-- 插入系统配置
INSERT INTO sys_config (id, config_name, config_key, config_value, config_type, create_by, update_by)
VALUES
(1, '用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system'),
(2, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system'),
(3, '用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system');
@@ -0,0 +1,253 @@
-- H2数据库Schema for Integration Testing
-- Create用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create用户角色关联表
CREATE TABLE IF NOT EXISTS user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- Create菜单表
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
path VARCHAR(200),
component VARCHAR(200),
menu_type VARCHAR(1) DEFAULT 'C',
visible VARCHAR(1) DEFAULT '1',
status VARCHAR(1) DEFAULT '1',
perms VARCHAR(100),
icon VARCHAR(100),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create权限表
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200),
action VARCHAR(20),
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create角色权限关联表
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
updated_by VARCHAR(50),
CONSTRAINT fk_role_permission_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT fk_role_permission_permission FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id)
);
-- Create字典类型表
CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create字典数据表
CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL,
css_class VARCHAR(100),
list_class VARCHAR(100),
is_default VARCHAR(1) DEFAULT 'N',
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
dict_value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
config_type VARCHAR(1) DEFAULT 'N',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create登录日志表
CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
browser VARCHAR(50),
os VARCHAR(50),
status VARCHAR(1),
message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
method_name VARCHAR(255),
method_params TEXT,
exception_msg TEXT,
exception_stack TEXT,
ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create系统公告表
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
message_content TEXT,
is_read VARCHAR(1) DEFAULT '0',
read_time TIMESTAMP,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create文件管理表
CREATE TABLE IF NOT EXISTS sys_file (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
file_type VARCHAR(100),
file_extension VARCHAR(10),
storage_type VARCHAR(50),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type ON sys_dict_data(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
@@ -0,0 +1,32 @@
package cn.novalon.gym.manage.app.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class MultipartConfigTest {
private MultipartConfig multipartConfig;
@BeforeEach
void setUp() {
multipartConfig = new MultipartConfig();
}
@Test
void testMultipartConfig() {
assertThat(multipartConfig).isNotNull();
}
@Test
void testMultipartHttpMessageReader() {
MultipartHttpMessageReader reader = multipartConfig.multipartHttpMessageReader();
assertThat(reader).isNotNull();
}
}
@@ -0,0 +1,50 @@
package cn.novalon.gym.manage.app.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class RateLimitConfigTest {
@Test
void testRateLimiterRegistry() throws Exception {
RateLimitConfig rateLimitConfig = new RateLimitConfig();
setField(rateLimitConfig, "limitForPeriod", 100);
setField(rateLimitConfig, "limitRefreshPeriod", Duration.ofSeconds(1));
setField(rateLimitConfig, "timeoutDuration", Duration.ZERO);
RateLimiterRegistry registry = rateLimitConfig.rateLimiterRegistry();
assertThat(registry).isNotNull();
}
@Test
void testApiRateLimiter() throws Exception {
RateLimitConfig rateLimitConfig = new RateLimitConfig();
setField(rateLimitConfig, "limitForPeriod", 100);
setField(rateLimitConfig, "limitRefreshPeriod", Duration.ofSeconds(1));
setField(rateLimitConfig, "timeoutDuration", Duration.ZERO);
RateLimiterRegistry registry = rateLimitConfig.rateLimiterRegistry();
RateLimiter rateLimiter = rateLimitConfig.apiRateLimiter(registry);
assertThat(rateLimiter).isNotNull();
assertThat(rateLimiter.getName()).isEqualTo("apiRateLimiter");
}
private void setField(Object target, String fieldName, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}
@@ -0,0 +1,68 @@
package cn.novalon.gym.manage.app.integration;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
import java.time.Duration;
/**
* 数据库初始化验证测试
*
* 注意:此测试需要完整的数据库初始化,暂时禁用。
* TODO: 修复数据库初始化问题
*
* @author 张翔
* @date 2026-04-03
*/
@Disabled("暂时禁用:数据库初始化问题需要修复")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class DatabaseInitTest {
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@Test
void testSysUserTableExists() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT COUNT(*) FROM sys_user")
.fetch()
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
void testOperationLogTableExists() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT COUNT(*) FROM operation_log")
.fetch()
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
void testAllTablesCreated() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC'")
.fetch()
.all()
.map(row -> row.get("TABLE_NAME"))
.collectList()
.as(StepVerifier::create)
.assertNext(tables -> {
System.out.println("Created tables: " + tables);
assert tables.contains("SYS_USER") : "SYS_USER table not found";
assert tables.contains("OPERATION_LOG") : "OPERATION_LOG table not found";
})
.verifyComplete();
}
}
@@ -0,0 +1,58 @@
package cn.novalon.gym.manage.app.integration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
/**
* 手动创建表测试
*
* @author 张翔
* @date 2026-04-03
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class ManualTableCreationTest {
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@BeforeEach
void setUp() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("CREATE TABLE IF NOT EXISTS operation_log (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"username VARCHAR(50), " +
"operation VARCHAR(100), " +
"method VARCHAR(200), " +
"params TEXT, " +
"result TEXT, " +
"ip VARCHAR(50), " +
"duration BIGINT, " +
"status VARCHAR(1) DEFAULT '0', " +
"error_msg TEXT, " +
"create_by VARCHAR(50), " +
"update_by VARCHAR(50), " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"deleted_at TIMESTAMP)")
.then()
.as(StepVerifier::create)
.verifyComplete();
}
@Test
void testOperationLogTableExists() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("SELECT COUNT(*) FROM operation_log")
.fetch()
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}
@@ -0,0 +1,70 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.app.ManageApplication;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* 操作日志导出功能集成测试
*
* 注意:此测试存在超时问题,暂时禁用。
* TODO: 修复Excel导出的超时问题
*
* @author 张翔
* @date 2026-04-03
*/
@Disabled("暂时禁用:Excel导出功能存在超时问题,需要优化")
@SpringBootTest(
classes = ManageApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@ActiveProfiles("test")
class OperationLogExportIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testExportOperationLogs_ShouldReturnExcelFile() {
webTestClient.get()
.uri("/api/logs/operation/export")
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_OCTET_STREAM)
.expectHeader().valueMatches("Content-Disposition", "attachment; filename=\"operation_logs_.*\\.xlsx\"")
.expectBody(byte[].class)
.value(bytes -> {
assert bytes != null;
assert bytes.length > 0;
assert bytes[0] == 0x50;
assert bytes[1] == 0x4B;
});
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testExportOperationLogsWithKeyword_ShouldReturnFilteredExcel() {
webTestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/api/logs/operation/export")
.queryParam("keyword", "test")
.build())
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_OCTET_STREAM)
.expectBody(byte[].class)
.value(bytes -> {
assert bytes != null;
assert bytes.length > 0;
});
}
}
@@ -0,0 +1,161 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.sys.core.domain.OperationLog;
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
/**
* 操作日志集成测试
*
* 注意:此测试需要完整的Spring上下文,暂时禁用。
* TODO: 优化集成测试配置
*
* @author 张翔
* @date 2026-04-03
*/
@Disabled("暂时禁用:集成测试配置需要优化")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class OperationLogIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private IOperationLogService logService;
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@BeforeEach
void setUp() {
webTestClient = webTestClient.mutate()
.responseTimeout(Duration.ofSeconds(10))
.build();
r2dbcEntityTemplate.getDatabaseClient()
.sql("CREATE TABLE IF NOT EXISTS operation_log (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"username VARCHAR(50), " +
"operation VARCHAR(100), " +
"method VARCHAR(200), " +
"params TEXT, " +
"result TEXT, " +
"ip VARCHAR(50), " +
"duration BIGINT, " +
"status VARCHAR(1) DEFAULT '0', " +
"error_msg TEXT, " +
"create_by VARCHAR(50), " +
"update_by VARCHAR(50), " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"deleted_at TIMESTAMP)")
.then()
.as(StepVerifier::create)
.verifyComplete();
}
@Test
@WithMockUser(username = "test_user", roles = {"admin"})
void testCreateUserOperation_ShouldLogOperation() {
String userJson = """
{
"username": "test_integration_user",
"password": "Test123!@#",
"email": "test@example.com",
"phone": "13900139000",
"nickname": "集成测试用户"
}
""";
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(userJson)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.id").exists()
.jsonPath("$.username").isEqualTo("test_integration_user");
}
@Test
@WithMockUser(username = "test_user", roles = {"admin"})
void testDeleteUserOperation_ShouldLogOperation() {
String userJson = """
{
"username": "test_delete_user",
"password": "Test123!@#",
"email": "delete@example.com",
"phone": "13900139001",
"nickname": "待删除用户"
}
""";
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(userJson)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.id").value(id -> {
Long userId = Long.valueOf(id.toString());
webTestClient.delete()
.uri("/api/users/{id}", userId)
.exchange()
.expectStatus().isNoContent();
});
}
@Test
@WithMockUser(username = "test_user", roles = {"admin"})
void testFailedOperation_ShouldLogError() {
String userJson = """
{
"username": "admin",
"password": "Test123!@#",
"email": "duplicate@example.com",
"phone": "13900139002",
"nickname": "重复用户"
}
""";
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(userJson)
.exchange()
.expectStatus().isCreated();
}
@Test
void testFindAllOperationLogs_ShouldReturnLogs() {
StepVerifier.create(logService.findAll().take(5))
.expectNextCount(0)
.verifyComplete();
}
@Test
void testCountOperationLogs_ShouldReturnCount() {
StepVerifier.create(logService.count())
.expectNextCount(1)
.verifyComplete();
}
}
@@ -0,0 +1,224 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.domain.UserRole;
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository;
import cn.novalon.gym.manage.sys.core.repository.IUserRoleRepository;
import cn.novalon.gym.manage.sys.core.service.impl.SysUserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import reactor.test.StepVerifier;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
/**
* 用户服务集成测试
*
* 使用PostgreSQL数据库进行集成测试
*
* 注意:此测试需要完整的Spring上下文,暂时禁用。
* TODO: 优化集成测试配置
*
* @author 张翔
* @date 2026-04-02
*/
@Disabled("暂时禁用:集成测试配置需要优化")
@SpringBootTest
@ActiveProfiles("test")
class SysUserServiceIntegrationTest {
@Autowired
private ISysUserRepository userRepository;
@Autowired
private ISysRoleRepository roleRepository;
@Autowired
private IUserRoleRepository userRoleRepository;
@Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate;
@Autowired
private SysUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@BeforeEach
void setUp() {
r2dbcEntityTemplate.delete(SysUser.class).all().block();
r2dbcEntityTemplate.delete(SysRole.class).all().block();
r2dbcEntityTemplate.delete(UserRole.class).all().block();
}
@Test
void testCreateAndFindUser() {
SysUser user = new SysUser();
user.setUsername("testuser");
user.setPassword("password123");
user.setEmail("test@example.com");
user.setNickname("Test User");
user.setPhone("13800138000");
StepVerifier.create(userService.createUser(user))
.expectNextMatches(createdUser -> {
assertNotNull(createdUser.getId());
assertEquals("testuser", createdUser.getUsername());
assertEquals("test@example.com", createdUser.getEmail());
assertTrue(createdUser.getPassword().startsWith("$2"));
assertEquals(StatusConstants.ENABLED, createdUser.getStatus());
return true;
})
.verifyComplete();
StepVerifier.create(userService.findByUsername("testuser"))
.expectNextMatches(foundUser -> {
assertEquals("testuser", foundUser.getUsername());
assertEquals("test@example.com", foundUser.getEmail());
return true;
})
.verifyComplete();
}
@Test
void testUpdateUser() {
SysUser user = new SysUser();
user.setUsername("updateuser");
user.setPassword("password123");
user.setEmail("update@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
createdUser.setEmail("updated@example.com");
createdUser.setNickname("Updated User");
StepVerifier.create(userService.updateUser(createdUser))
.expectNextMatches(updatedUser -> {
assertEquals("updated@example.com", updatedUser.getEmail());
assertEquals("Updated User", updatedUser.getNickname());
return true;
})
.verifyComplete();
}
@Test
void testDeleteUser() {
SysUser user = new SysUser();
user.setUsername("deleteuser");
user.setPassword("password123");
user.setEmail("delete@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
StepVerifier.create(userService.deleteUser(createdUser.getId()))
.verifyComplete();
StepVerifier.create(userService.findById(createdUser.getId()))
.verifyComplete();
}
@Test
void testChangePassword() {
SysUser user = new SysUser();
user.setUsername("pwduser");
user.setPassword("oldPassword");
user.setEmail("pwd@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
StepVerifier.create(userService.changePassword(createdUser.getId(), "oldPassword", "newPassword"))
.expectNextMatches(updatedUser -> {
assertNotEquals(createdUser.getPassword(), updatedUser.getPassword());
assertTrue(passwordEncoder.matches("newPassword", updatedUser.getPassword()));
return true;
})
.verifyComplete();
}
@Test
void testAssignRolesToUser() {
SysRole role1 = new SysRole();
role1.setRoleName("Test Role 1");
role1.setRoleKey("test_role_1");
role1.setStatus(1);
SysRole role2 = new SysRole();
role2.setRoleName("Test Role 2");
role2.setRoleKey("test_role_2");
role2.setStatus(1);
SysRole createdRole1 = roleRepository.save(role1).block();
SysRole createdRole2 = roleRepository.save(role2).block();
assertNotNull(createdRole1);
assertNotNull(createdRole2);
SysUser user = new SysUser();
user.setUsername("roleuser");
user.setPassword("password123");
user.setEmail("role@example.com");
SysUser createdUser = userService.createUser(user).block();
assertNotNull(createdUser);
StepVerifier.create(userService.assignRolesToUser(createdUser.getId(),
Arrays.asList(createdRole1.getId(), createdRole2.getId())))
.verifyComplete();
StepVerifier.create(userRoleRepository.findByUserId(createdUser.getId()).collectList())
.expectNextMatches(userRoles -> {
assertEquals(2, userRoles.size());
return true;
})
.verifyComplete();
}
@Test
void testFindAllUsers() {
for (int i = 1; i <= 3; i++) {
SysUser user = new SysUser();
user.setUsername("user" + i);
user.setPassword("password" + i);
user.setEmail("user" + i + "@example.com");
userService.createUser(user).block();
}
StepVerifier.create(userService.findAll(false).collectList())
.expectNextMatches(users -> {
assertEquals(3, users.size());
return true;
})
.verifyComplete();
}
@Test
void testExistsByUsername() {
SysUser user = new SysUser();
user.setUsername("existinguser");
user.setPassword("password123");
user.setEmail("existing@example.com");
userService.createUser(user).block();
StepVerifier.create(userService.existsByUsername("existinguser"))
.expectNext(true)
.verifyComplete();
StepVerifier.create(userService.existsByUsername("nonexistinguser"))
.expectNext(false)
.verifyComplete();
}
}
@@ -0,0 +1,31 @@
spring:
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
enabled: true
initial-size: 2
max-size: 10
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: never
security:
enabled: false
jwt:
secret: test-secret-key-for-integration-testing
expiration: 86400000
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
@@ -0,0 +1,80 @@
-- H2数据库测试数据
-- 用于测试环境
-- 插入测试角色
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
VALUES
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system'),
(4, '访客', 'guest', 4, 1, 'system', 'system');
-- 插入测试用户
-- BCrypt哈希值对应明文密码: Test@123
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
(3, 'normaluser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'),
(4, 'guestuser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'),
(5, 'disableduser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system');
-- 为用户分配角色
INSERT INTO user_role (user_id, role_id, created_by)
VALUES
(1, 1, 'system'),
(2, 2, 'system'),
(3, 3, 'system'),
(4, 4, 'system');
-- 插入测试菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, created_by, updated_by)
VALUES
(1, '系统管理', 0, 1, '/system', 'Layout', 'M', '1', '1', '', 'system', 'system', 'system'),
(2, '用户管理', 1, 1, 'user', 'system/user/index', 'C', '1', '1', 'system:user:list', 'user', 'system', 'system'),
(3, '角色管理', 1, 2, 'role', 'system/role/index', 'C', '1', '1', 'system:role:list', 'role', 'system', 'system'),
(4, '菜单管理', 1, 3, 'menu', 'system/menu/index', 'C', '1', '1', 'system:menu:list', 'menu', 'system', 'system'),
(5, '测试菜单', 0, 99, '/test', 'Layout', 'M', '1', '1', '', 'test', 'system', 'system'),
(6, '用户测试', 5, 1, 'user-test', 'system/user-test/index', 'C', '1', '1', 'system:user:test', 'user', 'system', 'system');
-- 插入测试权限
INSERT INTO sys_permission (id, permission_name, permission_key, permission_type, parent_id, status, created_by, updated_by)
VALUES
(1, '系统管理', 'system:manage', 'menu', 0, 1, 'system', 'system'),
(2, '用户管理', 'system:user:manage', 'menu', 1, 1, 'system', 'system'),
(3, '用户查询', 'system:user:list', 'button', 2, 1, 'system', 'system'),
(4, '用户新增', 'system:user:add', 'button', 2, 1, 'system', 'system'),
(5, '用户编辑', 'system:user:edit', 'button', 2, 1, 'system', 'system'),
(6, '用户删除', 'system:user:delete', 'button', 2, 1, 'system', 'system'),
(7, '测试权限', 'test:permission', 'menu', 0, 1, 'system', 'system'),
(8, '用户测试权限', 'system:user:test', 'button', 7, 1, 'system', 'system');
-- 为角色分配权限
INSERT INTO sys_role_permission (role_id, permission_id, created_by, updated_by)
SELECT 1, id, 'system', 'system' FROM sys_permission
UNION ALL
SELECT 2, id, 'system', 'system' FROM sys_permission WHERE id IN (7, 8);
-- 插入字典类型
INSERT INTO sys_dict_type (id, dict_name, dict_type, status, remark, created_by, updated_by)
VALUES
(1, '用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
(2, '菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
(3, '角色状态', 'role_status', '0', '角色状态列表', 'system', 'system');
-- 插入字典数据
INSERT INTO sys_dict_data (id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, created_by, updated_by)
VALUES
(1, 1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, 2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
(3, 1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(4, 2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
(5, 1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(6, 2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system');
-- 插入系统配置
INSERT INTO sys_config (id, config_name, config_key, config_value, config_type, remark, created_by, updated_by)
VALUES
(1, '用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', '初始化用户密码', 'system', 'system'),
(2, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', '默认皮肤', 'system', 'system'),
(3, '用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', '是否开启验证码功能', 'system', 'system');
@@ -0,0 +1,76 @@
-- H2数据库Schema for Integration Testing
-- 创建用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建用户角色关联表
CREATE TABLE IF NOT EXISTS user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
-- 创建审计日志表
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data CLOB,
after_data CLOB,
changed_fields CLOB,
ip_address VARCHAR(50),
user_agent CLOB,
description CLOB,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建审计日志索引
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-manage-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>manage-audit</artifactId>
<packaging>jar</packaging>
<name>Manage Audit</name>
<description>Audit module for Novalon Manage API</description>
<dependencies>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.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-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-manage-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>manage-common</artifactId>
<packaging>jar</packaging>
<name>Manage Common</name>
<description>Common module for Novalon Manage API</description>
<dependencies>
<dependency>
<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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,36 @@
package cn.novalon.gym.manage.common.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置类
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
private Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats();
}
}
@@ -0,0 +1,36 @@
package cn.novalon.gym.manage.common.config;
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
public class JwtProperties {
private String secret = "default-secret-key-change-in-production";
private long expiration = 86400000;
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public long getExpiration() {
return expiration;
}
public void setExpiration(long expiration) {
this.expiration = expiration;
}
}
@@ -0,0 +1,42 @@
package cn.novalon.gym.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,164 @@
package cn.novalon.gym.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) {
log.info("Query object is null, returning empty criteria");
return Query.query(criteria);
}
System.out.println("=== QueryUtil.getQuery START ===");
System.out.println("Query object class: " + query.getClass().getName());
log.info("=== QueryUtil.getQuery START ===");
log.info("Query object class: {}", query.getClass().getName());
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
log.info("Found {} fields to process", fields.size());
System.out.println("Found " + fields.size() + " fields to process");
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);
log.info("Processing field: {}, value: {}, blurry: {}", attributeName, val, blurry);
System.out.println("Processing field: " + attributeName + ", value: " + val + ", blurry: " + blurry);
if (val == null || "".equals(val)) {
log.info("Field {} has null or empty value, skipping", attributeName);
System.out.println("Field " + attributeName + " has null or empty value, skipping");
continue;
}
if (StringUtils.isNotBlank(blurry)) {
log.info("Field {} has blurry search configuration: {}", attributeName, blurry);
System.out.println("Field " + attributeName + " has blurry search configuration: " + blurry);
String[] blurrys = blurry.split(",");
Criteria orCriteria = Criteria.empty();
for (String s : blurrys) {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
criteria = criteria.and(orCriteria);
log.info("Added OR criteria for blurry search: {} with value: {}", blurry, val);
System.out.println("Added OR criteria for blurry search: " + blurry + " with value: " + val);
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;
}
}
@@ -0,0 +1,38 @@
package cn.novalon.gym.manage.common.domain.query;
/**
* 菜单查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysMenuQuery {
private String menuName;
private String menuType;
private String status;
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public String getMenuType() {
return menuType;
}
public void setMenuType(String menuType) {
this.menuType = menuType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
@@ -0,0 +1,38 @@
package cn.novalon.gym.manage.common.domain.query;
/**
* 角色查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysRoleQuery {
private String roleName;
private String roleKey;
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 getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
@@ -0,0 +1,56 @@
package cn.novalon.gym.manage.common.domain.query;
/**
* 用户查询条件对象
*
* @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;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -0,0 +1,55 @@
package cn.novalon.gym.manage.common.dto;
/**
* 分页请求参数封装类
*
* @author 张翔
* @date 2026-03-13
*/
public class PageRequest {
private int page = 0;
private int size = 10;
private String sort = "id";
private String order = "asc";
private String keyword;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String getSort() {
return sort;
}
public void setSort(String sort) {
this.sort = sort;
}
public String getOrder() {
return order;
}
public void setOrder(String order) {
this.order = order;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -0,0 +1,88 @@
package cn.novalon.gym.manage.common.dto;
import java.util.List;
/**
* 分页响应结果封装类
*
* @author 张翔
* @date 2026-03-13
*/
public class PageResponse<T> {
private List<T> content;
private int totalPages;
private long totalElements;
private int currentPage;
private int pageSize;
private boolean first;
private boolean last;
public PageResponse() {
}
public PageResponse(List<T> content, int totalPages, long totalElements, int currentPage, int pageSize) {
this.content = content;
this.totalPages = totalPages;
this.totalElements = totalElements;
this.currentPage = currentPage;
this.pageSize = pageSize;
this.first = currentPage == 0;
this.last = currentPage >= totalPages - 1;
}
public List<T> getContent() {
return content;
}
public void setContent(List<T> content) {
this.content = content;
}
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
public long getTotalElements() {
return totalElements;
}
public void setTotalElements(long totalElements) {
this.totalElements = totalElements;
}
public int getCurrentPage() {
return currentPage;
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public boolean isFirst() {
return first;
}
public void setFirst(boolean first) {
this.first = first;
}
public boolean isLast() {
return last;
}
public void setLast(boolean last) {
this.last = last;
}
}
@@ -0,0 +1,39 @@
package cn.novalon.gym.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();
}
@@ -0,0 +1,19 @@
package cn.novalon.gym.manage.common.exception;
import org.springframework.http.HttpStatus;
public class BusinessException extends BaseException {
public BusinessException(String errorCode, String message) {
super(errorCode, message);
}
public BusinessException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.BAD_REQUEST;
}
}
@@ -0,0 +1,19 @@
package cn.novalon.gym.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.gym.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.gym.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.gym.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.gym.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.gym.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;
}
}
@@ -0,0 +1,33 @@
package cn.novalon.gym.manage.common.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
* 默认异常日志服务实现
* 临时实现,用于解决启动时的依赖注入问题
*
* @author 张翔
* @date 2026-04-15
*/
@Service
public class DefaultExceptionLogService implements IExceptionLogService {
private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionLogService.class);
@Override
public Mono<Void> logException(String title, String exceptionName, String exceptionMsg,
String methodName, String ip, String stackTrace) {
logger.warn("异常日志记录 (临时实现): title={}, exceptionName={}, methodName={}, ip={}",
title, exceptionName, methodName, ip);
logger.warn("异常信息: {}", exceptionMsg);
if (stackTrace != null && stackTrace.length() > 500) {
logger.warn("堆栈跟踪 (截断): {}", stackTrace.substring(0, 500) + "...");
} else if (stackTrace != null) {
logger.warn("堆栈跟踪: {}", stackTrace);
}
return Mono.empty();
}
}
@@ -0,0 +1,198 @@
package cn.novalon.gym.manage.common.handler;
import cn.novalon.gym.manage.common.exception.BaseException;
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 IExceptionLogService exceptionLogService;
public GlobalExceptionHandler(IExceptionLogService exceptionLogService) {
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);
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,18 @@
package cn.novalon.gym.manage.common.handler;
import reactor.core.publisher.Mono;
/**
* 异常日志服务接口
*
* 文件定义:定义异常日志记录的抽象接口
* 涉及业务:异常日志记录、错误追踪
* 算法:使用响应式编程实现异步日志记录
*
* @author 张翔
* @date 2026-04-14
*/
public interface IExceptionLogService {
Mono<Void> logException(String title, String exceptionName, String exceptionMsg,
String methodName, String ip, String stackTrace);
}
@@ -0,0 +1,25 @@
package cn.novalon.gym.manage.common.util;
/**
* 数据库字段名常量定义
*
* @author 张翔
* @date 2026-03-13
*/
public class FieldConstants {
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String EMAIL = "email";
public static final String PHONE = "phone";
public static final String STATUS = "status";
public static final String ROLE_NAME = "roleName";
public static final String ROLE_KEY = "roleKey";
public static final String MENU_NAME = "menuName";
public static final String MENU_TYPE = "menuType";
public static final String ROLE_ID = "roleId";
public static final String PARENT_ID = "parentId";
private FieldConstants() {
}
}
@@ -0,0 +1,17 @@
package cn.novalon.gym.manage.common.util;
/**
* 菜单类型常量定义
*
* @author 张翔
* @date 2026-03-13
*/
public class MenuTypeConstants {
public static final String DIRECTORY = "M";
public static final String MENU = "C";
public static final String BUTTON = "F";
private MenuTypeConstants() {
}
}
@@ -0,0 +1,224 @@
package cn.novalon.gym.manage.common.util;
import cn.novalon.gym.manage.common.exception.ErrorCode;
import cn.novalon.gym.manage.common.exception.SystemException;
import cn.novalon.gym.manage.common.exception.ValidationException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
/**
* 雪花算法ID生成器
*
* 文件定义:基于Twitter Snowflake算法的分布式唯一ID生成器
* 涉及业务:为系统所有实体生成唯一ID,支持分布式环境下的ID生成
* 算法:使用雪花算法,结合时间戳、机器ID和序列号生成唯一ID,支持高并发场景
*
* @author 张翔
* @date 2026-03-13
*/
public final class SnowflakeId {
private static final int DEFAULT_WORKER_BITS = 10;
private static final int DEFAULT_SEQ_BITS = 12;
private static final long DEFAULT_EPOCH = 1582136402000L;
private static final int MAX_RETRIES = 10;
private static final long MAX_BACKWARD_MS = 50;
private static final int SPIN_THRESHOLD = 5;
private static final long TIME_CACHE_DURATION_MS = 16;
private static final AtomicLong lastTimestamp = new AtomicLong(-1L);
private static final AtomicLong sequence = new AtomicLong(0);
private static volatile SnowflakeConfig config;
private static volatile long workerId;
private static volatile long lastTimeCacheMs;
private static volatile int timeCacheHits;
static {
configure(DEFAULT_WORKER_BITS, DEFAULT_SEQ_BITS, DEFAULT_EPOCH);
}
private static void configure(int workerBits, int seqBits, long epoch) {
validateBits(workerBits, seqBits);
config = new SnowflakeConfig(epoch, workerBits, seqBits);
workerId = resolveWorkerId(config.maxWorkerId);
lastTimeCacheMs = 0;
timeCacheHits = 0;
}
public static long nextId() {
for (int i = 0; i < MAX_RETRIES; i++) {
try {
return nextIdInternal();
} catch (ClockBackwardException e) {
long backwardMs = e.getBackwardMs();
if (backwardMs > MAX_BACKWARD_MS) {
throw e;
}
if (i < SPIN_THRESHOLD) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
} else {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
}
}
}
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR,
"Failed to generate ID after " + MAX_RETRIES + " retries");
}
private static long nextIdInternal() {
long currentTs = timeGen();
long lastTs;
long seq;
do {
lastTs = lastTimestamp.get();
if (currentTs < lastTs) {
long backwardMs = lastTs - currentTs;
if (backwardMs <= MAX_BACKWARD_MS) {
lastTimestamp.set(currentTs);
lastTs = currentTs;
} else {
throw new ClockBackwardException(backwardMs);
}
}
if (currentTs == lastTs) {
seq = sequence.incrementAndGet() & config.sequenceMask;
if (seq == 0) {
currentTs = waitNextMillis(currentTs);
}
} else {
seq = 0;
}
} while (!lastTimestamp.compareAndSet(lastTs, currentTs));
return ((currentTs - config.epoch) << config.timestampShift)
| (workerId << config.workerShift)
| seq;
}
private static long waitNextMillis(long currentTs) {
long deadline = currentTs + 2;
int spinCount = 0;
while (currentTs <= lastTimestamp.get()) {
if (currentTs >= deadline) {
return currentTs;
}
if (spinCount < 10) {
spinCount++;
} else if (spinCount < 50) {
LockSupport.parkNanos(100_000);
spinCount++;
} else {
LockSupport.parkNanos(500_000);
}
currentTs = timeGen();
}
return currentTs;
}
private static long timeGen() {
long now = System.currentTimeMillis();
long cached = lastTimeCacheMs;
if (now - cached < TIME_CACHE_DURATION_MS) {
timeCacheHits++;
return cached;
}
synchronized (SnowflakeId.class) {
cached = lastTimeCacheMs;
if (now - cached < TIME_CACHE_DURATION_MS) {
timeCacheHits++;
return cached;
}
lastTimeCacheMs = now;
return now;
}
}
public static int getTimeCacheHits() {
return timeCacheHits;
}
public static void resetTimeCache() {
synchronized (SnowflakeId.class) {
lastTimeCacheMs = 0;
timeCacheHits = 0;
}
}
private static void validateBits(int workerBits, int seqBits) {
if (workerBits < 0 || workerBits > 22) {
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE, "WorkerID位数必须在0-22之间");
}
if (seqBits < 0 || seqBits > 22) {
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE, "序列号位数必须在0-22之间");
}
if (workerBits + seqBits > 22) {
throw new ValidationException(ErrorCode.VALIDATION_INVALID_VALUE,
"WorkerID和序列号位数总和不能超过22位,当前为: " + (workerBits + seqBits));
}
if (workerBits + seqBits == 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 SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR,
"WorkerID超出有效范围: " + id + " (有效范围: 0-" + maxWorkerId + ")");
}
return id;
}
private static long generateNewId() {
long newId = ThreadLocalRandom.current().nextLong(config.maxWorkerId + 1);
return newId;
}
public static void config(int workerBits, int seqBits, long epoch) {
configure(workerBits, seqBits, epoch);
}
public static long getWorkerId() {
return workerId;
}
private static class SnowflakeConfig {
final long epoch;
final int timestampShift;
final int workerShift;
final long sequenceMask;
final long maxWorkerId;
SnowflakeConfig(long epoch, int workerBits, int seqBits) {
this.epoch = epoch;
this.timestampShift = workerBits + seqBits;
this.workerShift = seqBits;
this.sequenceMask = ~(-1L << seqBits);
this.maxWorkerId = ~(-1L << workerBits);
}
}
public static class ClockBackwardException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final long backwardMs;
ClockBackwardException(long backwardMs) {
super("Clock moved backwards by " + backwardMs + "ms");
this.backwardMs = backwardMs;
}
public long getBackwardMs() {
return backwardMs;
}
}
}
@@ -0,0 +1,21 @@
package cn.novalon.gym.manage.common.util;
/**
* 状态常量定义
*
* 文件定义:系统通用的状态常量定义类
* 涉及业务:为系统提供统一的状态码定义,包括启用、禁用、删除等状态
* 算法:无复杂算法,主要为常量定义
*
* @author 张翔
* @date 2026-03-13
*/
public class StatusConstants {
public static final Integer DISABLED = 0;
public static final Integer ENABLED = 1;
public static final Integer DELETED = 2;
private StatusConstants() {
}
}
@@ -0,0 +1,2 @@
cn.novalon.manage.common.config.CacheConfig
cn.novalon.manage.common.config.JwtProperties
+115
View File
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-manage-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>manage-db</artifactId>
<packaging>jar</packaging>
<name>Manage DB</name>
<description>Database module for Novalon Manage API</description>
<dependencies>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-sys</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-notify</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-file</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-common</artifactId>
<version>${project.version}</version>
</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-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>runtime</scope>
</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.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,9 @@
package cn.novalon.gym.manage.db.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "cn.novalon.gym.manage.db.repository")
public class RepositoryScanConfig {
}
@@ -0,0 +1,87 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
import cn.novalon.gym.manage.db.entity.AuditLogEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 审计日志实体转换器
*
* @author 张翔
* @date 2026-04-08
*/
@Component
public class AuditLogConverter {
public AuditLog toDomain(AuditLogEntity entity) {
if (entity == null) {
return null;
}
AuditLog domain = new AuditLog();
domain.setId(entity.getId());
domain.setEntityType(entity.getEntityType());
domain.setEntityId(entity.getEntityId());
domain.setOperationType(entity.getOperationType());
domain.setOperator(entity.getOperator());
domain.setOperationTime(entity.getOperationTime());
domain.setBeforeData(entity.getBeforeData());
domain.setAfterData(entity.getAfterData());
domain.setChangedFields(entity.getChangedFields());
domain.setIpAddress(entity.getIpAddress());
domain.setUserAgent(entity.getUserAgent());
domain.setDescription(entity.getDescription());
domain.setCreateBy(entity.getCreateBy());
domain.setUpdateBy(entity.getUpdateBy());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public AuditLogEntity toEntity(AuditLog domain) {
if (domain == null) {
return null;
}
AuditLogEntity entity = new AuditLogEntity();
entity.setId(domain.getId());
entity.setEntityType(domain.getEntityType());
entity.setEntityId(domain.getEntityId());
entity.setOperationType(domain.getOperationType());
entity.setOperator(domain.getOperator());
entity.setOperationTime(domain.getOperationTime());
entity.setBeforeData(domain.getBeforeData());
entity.setAfterData(domain.getAfterData());
entity.setChangedFields(domain.getChangedFields());
entity.setIpAddress(domain.getIpAddress());
entity.setUserAgent(domain.getUserAgent());
entity.setDescription(domain.getDescription());
entity.setCreateBy(domain.getCreateBy());
entity.setUpdateBy(domain.getUpdateBy());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<AuditLog> toDomainList(List<AuditLogEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<AuditLogEntity> toEntityList(List<AuditLog> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,72 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.Dictionary;
import cn.novalon.gym.manage.db.entity.DictionaryEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字典实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class DictionaryConverter {
public DictionaryEntity toEntity(Dictionary domain) {
if (domain == null) {
return null;
}
DictionaryEntity entity = new DictionaryEntity();
entity.setId(domain.getId());
entity.setType(domain.getType());
entity.setCode(domain.getCode());
entity.setName(domain.getName());
entity.setValue(domain.getValue());
entity.setRemark(domain.getRemark());
entity.setSort(domain.getSort());
entity.setCreateBy(domain.getCreateBy());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
return entity;
}
public Dictionary toDomain(DictionaryEntity entity) {
if (entity == null) {
return null;
}
Dictionary domain = new Dictionary();
domain.setId(entity.getId());
domain.setType(entity.getType());
domain.setCode(entity.getCode());
domain.setName(entity.getName());
domain.setValue(entity.getValue());
domain.setRemark(entity.getRemark());
domain.setSort(entity.getSort());
domain.setCreateBy(entity.getCreateBy());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
return domain;
}
public List<DictionaryEntity> toEntityList(List<Dictionary> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
public List<Dictionary> toDomainList(List<DictionaryEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,79 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.OperationLog;
import cn.novalon.gym.manage.db.entity.OperationLogEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 操作日志实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class OperationLogConverter {
public OperationLog toDomain(OperationLogEntity entity) {
if (entity == null) {
return null;
}
OperationLog domain = new OperationLog();
domain.setId(entity.getId());
domain.setUsername(entity.getUsername());
domain.setOperation(entity.getOperation());
domain.setMethod(entity.getMethod());
domain.setParams(entity.getParams());
domain.setResult(entity.getResult());
domain.setIp(entity.getIp());
domain.setDuration(entity.getDuration());
domain.setStatus(entity.getStatus());
domain.setErrorMsg(entity.getErrorMsg());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public OperationLogEntity toEntity(OperationLog domain) {
if (domain == null) {
return null;
}
OperationLogEntity entity = new OperationLogEntity();
entity.setId(domain.getId());
entity.setUsername(domain.getUsername());
entity.setOperation(domain.getOperation());
entity.setMethod(domain.getMethod());
entity.setParams(domain.getParams());
entity.setResult(domain.getResult());
entity.setIp(domain.getIp());
entity.setDuration(domain.getDuration());
entity.setStatus(domain.getStatus());
entity.setErrorMsg(domain.getErrorMsg());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<OperationLog> toDomainList(List<OperationLogEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<OperationLogEntity> toEntityList(List<OperationLog> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,70 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysConfig;
import cn.novalon.gym.manage.db.entity.SysConfigEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 系统配置实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysConfigConverter {
public SysConfig toDomain(SysConfigEntity entity) {
if (entity == null) {
return null;
}
SysConfig domain = new SysConfig();
domain.setId(entity.getId());
domain.setConfigName(entity.getConfigName());
domain.setConfigKey(entity.getConfigKey());
domain.setConfigValue(entity.getConfigValue());
domain.setConfigType(entity.getConfigType());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
return domain;
}
public SysConfigEntity toEntity(SysConfig domain) {
if (domain == null) {
return null;
}
SysConfigEntity entity = new SysConfigEntity();
entity.setId(domain.getId());
entity.setConfigName(domain.getConfigName());
entity.setConfigKey(domain.getConfigKey());
entity.setConfigValue(domain.getConfigValue());
entity.setConfigType(domain.getConfigType());
entity.setCreateBy(domain.getCreateBy());
entity.setUpdateBy(domain.getUpdateBy());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysConfig> toDomainList(List<SysConfigEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysConfigEntity> toEntityList(List<SysConfig> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,75 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysDictData;
import cn.novalon.gym.manage.db.entity.SysDictDataEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字典数据实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysDictDataConverter {
public SysDictData toDomain(SysDictDataEntity entity) {
if (entity == null) {
return null;
}
SysDictData domain = new SysDictData();
domain.setId(entity.getId());
domain.setDictSort(entity.getDictSort());
domain.setDictLabel(entity.getDictLabel());
domain.setDictValue(entity.getDictValue());
domain.setDictType(entity.getDictType());
domain.setCssClass(entity.getCssClass());
domain.setListClass(entity.getListClass());
domain.setIsDefault(entity.getIsDefault());
domain.setStatus(entity.getStatus());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
return domain;
}
public SysDictDataEntity toEntity(SysDictData domain) {
if (domain == null) {
return null;
}
SysDictDataEntity entity = new SysDictDataEntity();
entity.setId(domain.getId());
entity.setDictSort(domain.getDictSort());
entity.setDictLabel(domain.getDictLabel());
entity.setDictValue(domain.getDictValue());
entity.setDictType(domain.getDictType());
entity.setCssClass(domain.getCssClass());
entity.setListClass(domain.getListClass());
entity.setIsDefault(domain.getIsDefault());
entity.setStatus(domain.getStatus());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
return entity;
}
public List<SysDictData> toDomainList(List<SysDictDataEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysDictDataEntity> toEntityList(List<SysDictData> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,67 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysDictType;
import cn.novalon.gym.manage.db.entity.SysDictTypeEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字典类型实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysDictTypeConverter {
public SysDictType toDomain(SysDictTypeEntity entity) {
if (entity == null) {
return null;
}
SysDictType domain = new SysDictType();
domain.setId(entity.getId());
domain.setDictName(entity.getDictName());
domain.setDictType(entity.getDictType());
domain.setStatus(entity.getStatus());
domain.setRemark(entity.getRemark());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
return domain;
}
public SysDictTypeEntity toEntity(SysDictType domain) {
if (domain == null) {
return null;
}
SysDictTypeEntity entity = new SysDictTypeEntity();
entity.setId(domain.getId());
entity.setDictName(domain.getDictName());
entity.setDictType(domain.getDictType());
entity.setStatus(domain.getStatus());
entity.setRemark(domain.getRemark());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
return entity;
}
public List<SysDictType> toDomainList(List<SysDictTypeEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysDictTypeEntity> toEntityList(List<SysDictType> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,73 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysExceptionLog;
import cn.novalon.gym.manage.db.entity.SysExceptionLogEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 异常日志实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysExceptionLogConverter {
public SysExceptionLog toDomain(SysExceptionLogEntity entity) {
if (entity == null) {
return null;
}
SysExceptionLog domain = new SysExceptionLog();
domain.setId(entity.getId());
domain.setUsername(entity.getUsername());
domain.setTitle(entity.getTitle());
domain.setExceptionName(entity.getExceptionName());
domain.setMethodName(entity.getMethodName());
domain.setMethodParams(entity.getMethodParams());
domain.setExceptionMsg(entity.getExceptionMsg());
domain.setExceptionStack(entity.getExceptionStack());
domain.setIp(entity.getIp());
domain.setCreateTime(entity.getCreateTime());
return domain;
}
public SysExceptionLogEntity toEntity(SysExceptionLog domain) {
if (domain == null) {
return null;
}
SysExceptionLogEntity entity = new SysExceptionLogEntity();
entity.setId(domain.getId());
entity.setUsername(domain.getUsername());
entity.setTitle(domain.getTitle());
entity.setExceptionName(domain.getExceptionName());
entity.setMethodName(domain.getMethodName());
entity.setMethodParams(domain.getMethodParams());
entity.setExceptionMsg(domain.getExceptionMsg());
entity.setExceptionStack(domain.getExceptionStack());
entity.setIp(domain.getIp());
entity.setCreateTime(domain.getCreateTime());
return entity;
}
public List<SysExceptionLog> toDomainList(List<SysExceptionLogEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysExceptionLogEntity> toEntityList(List<SysExceptionLog> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,69 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.db.entity.SysFileEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 文件实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysFileConverter {
public SysFile toDomain(SysFileEntity entity) {
if (entity == null) {
return null;
}
SysFile domain = new SysFile();
domain.setId(entity.getId());
domain.setFileName(entity.getFileName());
domain.setFilePath(entity.getFilePath());
domain.setFileSize(entity.getFileSize());
domain.setFileType(entity.getFileType());
domain.setStorageType(entity.getStorageType());
domain.setCreateBy(entity.getCreateBy());
domain.setCreatedAt(entity.getCreatedAt());
return domain;
}
public SysFileEntity toEntity(SysFile domain) {
if (domain == null) {
return null;
}
SysFileEntity entity = new SysFileEntity();
entity.setId(domain.getId());
entity.setFileName(domain.getFileName());
entity.setFilePath(domain.getFilePath());
entity.setFileSize(domain.getFileSize());
entity.setFileType(domain.getFileType());
entity.setStorageType(domain.getStorageType());
entity.setCreateBy(domain.getCreateBy());
entity.setCreatedAt(domain.getCreatedAt());
return entity;
}
public List<SysFile> toDomainList(List<SysFileEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysFileEntity> toEntityList(List<SysFile> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,71 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysLoginLog;
import cn.novalon.gym.manage.db.entity.SysLoginLogEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 登录日志实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysLoginLogConverter {
public SysLoginLog toDomain(SysLoginLogEntity entity) {
if (entity == null) {
return null;
}
SysLoginLog domain = new SysLoginLog();
domain.setId(entity.getId());
domain.setUsername(entity.getUsername());
domain.setIp(entity.getIp());
domain.setLocation(entity.getLocation());
domain.setBrowser(entity.getBrowser());
domain.setOs(entity.getOs());
domain.setStatus(entity.getStatus());
domain.setMessage(entity.getMessage());
domain.setLoginTime(entity.getLoginTime());
return domain;
}
public SysLoginLogEntity toEntity(SysLoginLog domain) {
if (domain == null) {
return null;
}
SysLoginLogEntity entity = new SysLoginLogEntity();
entity.setId(domain.getId());
entity.setUsername(domain.getUsername());
entity.setIp(domain.getIp());
entity.setLocation(domain.getLocation());
entity.setBrowser(domain.getBrowser());
entity.setOs(domain.getOs());
entity.setStatus(domain.getStatus());
entity.setMessage(domain.getMessage());
entity.setLoginTime(domain.getLoginTime());
return entity;
}
public List<SysLoginLog> toDomainList(List<SysLoginLogEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysLoginLogEntity> toEntityList(List<SysLoginLog> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,79 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysMenu;
import cn.novalon.gym.manage.db.entity.SysMenuEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 菜单实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysMenuConverter {
public SysMenu toDomain(SysMenuEntity entity) {
if (entity == null) {
return null;
}
SysMenu domain = new SysMenu();
domain.setId(entity.getId());
domain.setMenuName(entity.getMenuName());
domain.setParentId(entity.getParentId());
domain.setOrderNum(entity.getOrderNum());
domain.setMenuType(entity.getMenuType());
domain.setPerms(entity.getPerms());
domain.setComponent(entity.getComponent());
domain.setStatus(entity.getStatus());
domain.setCreateBy(entity.getCreateBy());
domain.setUpdateBy(entity.getUpdateBy());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public SysMenuEntity toEntity(SysMenu domain) {
if (domain == null) {
return null;
}
SysMenuEntity entity = new SysMenuEntity();
entity.setId(domain.getId());
entity.setMenuName(domain.getMenuName());
entity.setParentId(domain.getParentId());
entity.setOrderNum(domain.getOrderNum());
entity.setMenuType(domain.getMenuType());
entity.setPerms(domain.getPerms());
entity.setComponent(domain.getComponent());
entity.setStatus(domain.getStatus());
entity.setCreateBy(domain.getCreateBy());
entity.setUpdateBy(domain.getUpdateBy());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysMenu> toDomainList(List<SysMenuEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysMenuEntity> toEntityList(List<SysMenu> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,69 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.notify.core.domain.SysNotice;
import cn.novalon.gym.manage.db.entity.SysNoticeEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 通知公告实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysNoticeConverter {
public SysNotice toDomain(SysNoticeEntity entity) {
if (entity == null) {
return null;
}
SysNotice domain = new SysNotice();
domain.setId(entity.getId());
domain.setNoticeTitle(entity.getNoticeTitle());
domain.setNoticeType(entity.getNoticeType());
domain.setNoticeContent(entity.getNoticeContent());
domain.setStatus(entity.getStatus());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public SysNoticeEntity toEntity(SysNotice domain) {
if (domain == null) {
return null;
}
SysNoticeEntity entity = new SysNoticeEntity();
entity.setId(domain.getId());
entity.setNoticeTitle(domain.getNoticeTitle());
entity.setNoticeType(domain.getNoticeType());
entity.setNoticeContent(domain.getNoticeContent());
entity.setStatus(domain.getStatus());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysNotice> toDomainList(List<SysNoticeEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysNoticeEntity> toEntityList(List<SysNotice> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,73 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysPermission;
import cn.novalon.gym.manage.db.entity.SysPermissionEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 权限实体转换器
*
* @author 张翔
* @date 2026-03-25
*/
@Component
public class SysPermissionConverter {
public SysPermission toDomain(SysPermissionEntity entity) {
if (entity == null) {
return null;
}
SysPermission domain = new SysPermission();
domain.setId(entity.getId());
domain.setPermissionName(entity.getPermissionName());
domain.setPermissionCode(entity.getPermissionCode());
domain.setResource(entity.getResource());
domain.setAction(entity.getAction());
domain.setDescription(entity.getDescription());
domain.setStatus(entity.getStatus());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public SysPermissionEntity toEntity(SysPermission domain) {
if (domain == null) {
return null;
}
SysPermissionEntity entity = new SysPermissionEntity();
entity.setId(domain.getId());
entity.setPermissionName(domain.getPermissionName());
entity.setPermissionCode(domain.getPermissionCode());
entity.setResource(domain.getResource());
entity.setAction(domain.getAction());
entity.setDescription(domain.getDescription());
entity.setStatus(domain.getStatus());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysPermission> toDomainList(List<SysPermissionEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysPermissionEntity> toEntityList(List<SysPermission> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,69 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.db.entity.SysRoleEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 角色实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysRoleConverter {
public SysRole toDomain(SysRoleEntity entity) {
if (entity == null) {
return null;
}
SysRole domain = new SysRole();
domain.setId(entity.getId());
domain.setRoleName(entity.getRoleName());
domain.setRoleKey(entity.getRoleKey());
domain.setRoleSort(entity.getRoleSort());
domain.setStatus(entity.getStatus());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public SysRoleEntity toEntity(SysRole domain) {
if (domain == null) {
return null;
}
SysRoleEntity entity = new SysRoleEntity();
entity.setId(domain.getId());
entity.setRoleName(domain.getRoleName());
entity.setRoleKey(domain.getRoleKey());
entity.setRoleSort(domain.getRoleSort());
entity.setStatus(domain.getStatus());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysRole> toDomainList(List<SysRoleEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysRoleEntity> toEntityList(List<SysRole> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,63 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysRolePermission;
import cn.novalon.gym.manage.db.entity.SysRolePermissionEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 角色权限关联实体转换器
*
* @author 张翔
* @date 2026-03-25
*/
@Component
public class SysRolePermissionConverter {
public SysRolePermission toDomain(SysRolePermissionEntity entity) {
if (entity == null) {
return null;
}
SysRolePermission domain = new SysRolePermission();
domain.setId(entity.getId());
domain.setRoleId(entity.getRoleId());
domain.setPermissionId(entity.getPermissionId());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
return domain;
}
public SysRolePermissionEntity toEntity(SysRolePermission domain) {
if (domain == null) {
return null;
}
SysRolePermissionEntity entity = new SysRolePermissionEntity();
entity.setId(domain.getId());
entity.setRoleId(domain.getRoleId());
entity.setPermissionId(domain.getPermissionId());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
return entity;
}
public List<SysRolePermission> toDomainList(List<SysRolePermissionEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysRolePermissionEntity> toEntityList(List<SysRolePermission> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,75 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.db.entity.SysUserEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysUserConverter {
public SysUser toDomain(SysUserEntity entity) {
if (entity == null) {
return null;
}
SysUser domain = new SysUser();
domain.setId(entity.getId());
domain.setUsername(entity.getUsername());
domain.setPassword(entity.getPassword());
domain.setEmail(entity.getEmail());
domain.setPhone(entity.getPhone());
domain.setNickname(entity.getNickname());
domain.setRoleId(entity.getRoleId());
domain.setStatus(entity.getStatus());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public SysUserEntity toEntity(SysUser domain) {
if (domain == null) {
return null;
}
SysUserEntity entity = new SysUserEntity();
entity.setId(domain.getId());
entity.setUsername(domain.getUsername());
entity.setPassword(domain.getPassword());
entity.setEmail(domain.getEmail());
entity.setPhone(domain.getPhone());
entity.setNickname(domain.getNickname());
entity.setRoleId(domain.getRoleId());
entity.setStatus(domain.getStatus());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<SysUser> toDomainList(List<SysUserEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysUserEntity> toEntityList(List<SysUser> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,67 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.notify.core.domain.SysUserMessage;
import cn.novalon.gym.manage.db.entity.SysUserMessageEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户消息实体转换器
*
* @author 张翔
* @date 2026-03-13
*/
@Component
public class SysUserMessageConverter {
public SysUserMessage toDomain(SysUserMessageEntity entity) {
if (entity == null) {
return null;
}
SysUserMessage domain = new SysUserMessage();
domain.setId(entity.getId());
domain.setUserId(entity.getUserId());
domain.setTitle(entity.getTitle());
domain.setContent(entity.getContent());
domain.setMessageType(entity.getMessageType());
domain.setIsRead(entity.getIsRead());
domain.setCreateTime(entity.getCreateTime());
return domain;
}
public SysUserMessageEntity toEntity(SysUserMessage domain) {
if (domain == null) {
return null;
}
SysUserMessageEntity entity = new SysUserMessageEntity();
entity.setId(domain.getId());
entity.setUserId(domain.getUserId());
entity.setTitle(domain.getTitle());
entity.setContent(domain.getContent());
entity.setMessageType(domain.getMessageType());
entity.setIsRead(domain.getIsRead());
entity.setCreateTime(domain.getCreateTime());
return entity;
}
public List<SysUserMessage> toDomainList(List<SysUserMessageEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysUserMessageEntity> toEntityList(List<SysUserMessage> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,37 @@
package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.db.entity.UserRoleEntity;
import cn.novalon.gym.manage.sys.core.domain.UserRole;
import org.springframework.stereotype.Component;
@Component
public class UserRoleConverter {
public UserRole toDomain(UserRoleEntity entity) {
if (entity == null) {
return null;
}
UserRole domain = new UserRole();
domain.setId(entity.getId());
domain.setUserId(entity.getUserId());
domain.setRoleId(entity.getRoleId());
domain.setCreatedAt(entity.getCreatedAt());
domain.setCreatedBy(entity.getCreatedBy());
return domain;
}
public UserRoleEntity toEntity(UserRole domain) {
if (domain == null) {
return null;
}
UserRoleEntity entity = new UserRoleEntity();
entity.setId(domain.getId());
entity.setUserId(domain.getUserId());
entity.setRoleId(domain.getRoleId());
entity.setCreatedAt(domain.getCreatedAt());
entity.setCreatedBy(domain.getCreatedBy());
return entity;
}
}
@@ -0,0 +1,53 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.AuditLogEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
/**
* 审计日志数据访问接口
*
* @author 张翔
* @date 2026-04-08
*/
@Repository
public interface AuditLogDao extends R2dbcRepository<AuditLogEntity, Long> {
Flux<AuditLogEntity> findByEntityTypeAndDeletedAtIsNull(String entityType);
Flux<AuditLogEntity> findByEntityIdAndDeletedAtIsNull(Long entityId);
Flux<AuditLogEntity> findByEntityTypeAndEntityIdAndDeletedAtIsNull(String entityType, Long entityId);
Flux<AuditLogEntity> findByOperatorAndDeletedAtIsNull(String operator);
Flux<AuditLogEntity> findByOperationTypeAndDeletedAtIsNull(String operationType);
Flux<AuditLogEntity> findByOperationTimeBetweenAndDeletedAtIsNull(LocalDateTime startTime, LocalDateTime endTime);
Flux<AuditLogEntity> findByEntityTypeAndOperationTimeBetweenAndDeletedAtIsNull(
String entityType,
LocalDateTime startTime,
LocalDateTime endTime
);
Flux<AuditLogEntity> findByOperatorAndOperationTimeBetweenAndDeletedAtIsNull(
String operator,
LocalDateTime startTime,
LocalDateTime endTime
);
Mono<Long> countByEntityTypeAndDeletedAtIsNull(String entityType);
Mono<Long> countByOperationTypeAndDeletedAtIsNull(String operationType);
Mono<Long> countByOperatorAndDeletedAtIsNull(String operator);
Mono<Long> countByOperationTimeBetweenAndDeletedAtIsNull(LocalDateTime startTime, LocalDateTime endTime);
Flux<AuditLogEntity> findByDeletedAtIsNull();
}
@@ -0,0 +1,29 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.DictionaryEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 字典数据访问接口
*
* @author 张翔
* @date 2026-03-13
*/
@Repository
public interface DictionaryDao extends R2dbcRepository<DictionaryEntity, Long> {
Flux<DictionaryEntity> findByType(String type);
Mono<DictionaryEntity> findByTypeAndCode(String type, String code);
Mono<DictionaryEntity> findByTypeAndCodeAndDeletedAtIsNull(String type, String code);
Flux<DictionaryEntity> findByDeletedAtIsNull();
Flux<DictionaryEntity> findByDeletedAtIsNullOrderBySortAsc();
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,21 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.OperationLogEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public interface OperationLogDao extends R2dbcRepository<OperationLogEntity, Long> {
Flux<OperationLogEntity> findByUsernameAndDeletedAtIsNull(String username);
Flux<OperationLogEntity> findByDeletedAtIsNull();
Mono<Long> countByDeletedAtIsNull();
Mono<Long> countByCreatedAtAfterAndDeletedAtIsNull(LocalDateTime dateTime);
}
@@ -0,0 +1,42 @@
package cn.novalon.gym.manage.db.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,171 @@
package cn.novalon.gym.manage.db.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) {
log.info("Query object is null, returning empty criteria");
return Query.query(criteria);
}
System.out.println("=== QueryUtil.getQuery START ===");
System.out.println("Query object class: " + query.getClass().getName());
log.info("=== QueryUtil.getQuery START ===");
log.info("Query object class: {}", query.getClass().getName());
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
log.info("Found {} fields to process", fields.size());
System.out.println("Found " + fields.size() + " fields to process");
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);
log.info("Processing field: {}, value: {}, blurry: {}", attributeName, val, blurry);
System.out.println("Processing field: " + attributeName + ", value: " + val + ", blurry: " + blurry);
if (val == null || "".equals(val)) {
log.info("Field {} has null or empty value, skipping", attributeName);
System.out.println("Field " + attributeName + " has null or empty value, skipping");
continue;
}
if (StringUtils.isNotBlank(blurry)) {
log.info("Field {} has blurry search configuration: {}", attributeName, blurry);
System.out.println("Field " + attributeName + " has blurry search configuration: " + blurry);
String[] blurrys = blurry.split(",");
Criteria orCriteria = null;
for (int i = 0; i < blurrys.length; i++) {
String s = blurrys[i];
if (i == 0) {
orCriteria = Criteria.where(s).like("%" + val + "%");
} else {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
}
if (orCriteria != null) {
criteria = criteria.and(orCriteria);
log.info("Added OR criteria for blurry search: {} with value: {}", blurry, val);
System.out.println("Added OR criteria for blurry search: " + blurry + " with value: " + val);
}
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 true;
}
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;
}
}
@@ -0,0 +1,22 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysConfigEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysConfigDao extends R2dbcRepository<SysConfigEntity, Long> {
Mono<SysConfigEntity> findByConfigKeyAndDeletedAtIsNull(String configKey);
Flux<SysConfigEntity> findByDeletedAtIsNull();
Flux<SysConfigEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,26 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysDictDataEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysDictDataDao extends R2dbcRepository<SysDictDataEntity, Long> {
Flux<SysDictDataEntity> findByDictTypeAndStatusAndDeletedAtIsNull(String dictType, String status);
Flux<SysDictDataEntity> findByDictTypeAndDeletedAtIsNull(String dictType);
Flux<SysDictDataEntity> findByDictTypeAndDeletedAtIsNull(String dictType, Sort sort);
Flux<SysDictDataEntity> findByDeletedAtIsNull();
Flux<SysDictDataEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,22 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysDictTypeEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysDictTypeDao extends R2dbcRepository<SysDictTypeEntity, Long> {
Mono<SysDictTypeEntity> findByDictTypeAndDeletedAtIsNull(String dictType);
Flux<SysDictTypeEntity> findByDeletedAtIsNull();
Flux<SysDictTypeEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,25 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysExceptionLogEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public interface SysExceptionLogDao extends R2dbcRepository<SysExceptionLogEntity, Long> {
Flux<SysExceptionLogEntity> findByUsername(String username);
Flux<SysExceptionLogEntity> findByUsernameOrderByCreateTimeDesc(String username);
Flux<SysExceptionLogEntity> findByCreateTimeBetweenOrderByCreateTimeDesc(LocalDateTime startTime, LocalDateTime endTime);
Flux<SysExceptionLogEntity> findAllByOrderByCreateTimeDesc();
Mono<Long> count();
Mono<Long> countByUsername(String username);
}
@@ -0,0 +1,30 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysFileEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysFileDao extends R2dbcRepository<SysFileEntity, Long> {
Flux<SysFileEntity> findByCreateBy(String createBy);
Flux<SysFileEntity> findByCreateBy(String createBy, Sort sort);
Flux<SysFileEntity> findByCreateByOrderByCreatedAtDesc(String createBy);
Flux<SysFileEntity> findByDeletedAtIsNull();
Flux<SysFileEntity> findByDeletedAtIsNull(Sort sort);
Flux<SysFileEntity> findByDeletedAtIsNullOrderByCreatedAtDesc();
Mono<Long> countByDeletedAtIsNull();
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
Flux<SysFileEntity> findByFilePathContaining(String fileName);
}
@@ -0,0 +1,27 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysLoginLogEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Repository
public interface SysLoginLogDao extends R2dbcRepository<SysLoginLogEntity, Long> {
Flux<SysLoginLogEntity> findByUsername(String username);
Flux<SysLoginLogEntity> findByUsernameOrderByLoginTimeDesc(String username);
Flux<SysLoginLogEntity> findByLoginTimeBetweenOrderByLoginTimeDesc(LocalDateTime startTime, LocalDateTime endTime);
Flux<SysLoginLogEntity> findAllByOrderByLoginTimeDesc();
Mono<Long> count();
Mono<Long> countByUsername(String username);
Mono<Long> countByLoginTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
}
@@ -0,0 +1,17 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysMenuEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysMenuDao extends R2dbcRepository<SysMenuEntity, Long> {
Mono<SysMenuEntity> findByIdAndDeletedAtIsNull(Long id);
Flux<SysMenuEntity> findByParentIdAndDeletedAtIsNull(Long parentId);
Flux<SysMenuEntity> findByDeletedAtIsNull();
}
@@ -0,0 +1,24 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysNoticeEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysNoticeDao extends R2dbcRepository<SysNoticeEntity, Long> {
Flux<SysNoticeEntity> findByStatusAndDeletedAtIsNull(String status);
Flux<SysNoticeEntity> findByStatusAndDeletedAtIsNull(String status, Sort sort);
Flux<SysNoticeEntity> findByDeletedAtIsNull();
Flux<SysNoticeEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,38 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysPermissionEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysPermissionDao extends R2dbcRepository<SysPermissionEntity, Long> {
Mono<SysPermissionEntity> findByIdAndDeletedAtIsNull(Long id);
Mono<SysPermissionEntity> findByPermissionCodeAndDeletedAtIsNull(String permissionCode);
Flux<SysPermissionEntity> findByDeletedAtIsNull();
Flux<SysPermissionEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Boolean> existsByPermissionCodeAndDeletedAtIsNull(String permissionCode);
@org.springframework.data.r2dbc.repository.Query("""
SELECT p.* FROM sys_permission p
INNER JOIN sys_role_permission rp ON p.id = rp.permission_id
WHERE rp.role_id = :roleId AND p.deleted_at IS NULL
""")
Flux<SysPermissionEntity> findByRoleId(Long roleId);
@org.springframework.data.r2dbc.repository.Query("""
SELECT DISTINCT p.* FROM sys_permission p
INNER JOIN sys_role_permission rp ON p.id = rp.permission_id
WHERE rp.role_id IN (:roleIds) AND p.deleted_at IS NULL
""")
Flux<SysPermissionEntity> findByRoleIds(java.util.List<Long> roleIds);
}
@@ -0,0 +1,30 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysRoleEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysRoleDao extends R2dbcRepository<SysRoleEntity, Long> {
Mono<SysRoleEntity> findByIdAndDeletedAtIsNull(Long id);
Mono<SysRoleEntity> findByRoleKeyAndDeletedAtIsNull(String roleKey);
Flux<SysRoleEntity> findByDeletedAtIsNull();
Flux<SysRoleEntity> findByDeletedAtIsNull(Sort sort);
Flux<SysRoleEntity> findByRoleNameLikeAndRoleKeyLikeAndDeletedAtIsNull(String roleName, String roleKey, Sort sort);
Mono<Long> countByDeletedAtIsNull();
Mono<Long> countByRoleNameLikeAndRoleKeyLikeAndDeletedAtIsNull(String roleName, String roleKey);
Mono<SysRoleEntity> findByRoleNameAndDeletedAtIsNull(String roleName);
Mono<Boolean> existsByRoleNameAndDeletedAtIsNull(String roleName);
}
@@ -0,0 +1,46 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysRolePermissionEntity;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface SysRolePermissionDao extends R2dbcRepository<SysRolePermissionEntity, Long> {
Flux<SysRolePermissionEntity> findByRoleId(Long roleId);
Flux<SysRolePermissionEntity> findByPermissionId(Long permissionId);
Flux<Long> findPermissionIdsByRoleId(Long roleId);
Flux<Long> findRoleIdsByPermissionId(Long permissionId);
@Modifying
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission
WHERE role_id = :roleId AND permission_id IN (:permissionIds)
""")
Mono<Void> deleteByRoleIdAndPermissionIds(Long roleId, java.util.List<Long> permissionIds);
@Modifying
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission
WHERE permission_id = :permissionId AND role_id IN (:roleIds)
""")
Mono<Void> deleteByPermissionIdAndRoleIds(Long permissionId, java.util.List<Long> roleIds);
@Modifying
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission WHERE role_id = :roleId
""")
Mono<Void> deleteByRoleId(Long roleId);
@Modifying
@org.springframework.data.r2dbc.repository.Query("""
DELETE FROM sys_role_permission WHERE permission_id = :permissionId
""")
Mono<Void> deleteByPermissionId(Long permissionId);
}
@@ -0,0 +1,36 @@
package cn.novalon.gym.manage.db.dao;
import cn.novalon.gym.manage.db.entity.SysUserEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 用户数据访问接口
*
* @author 张翔
* @date 2026-03-13
*/
@Repository
public interface SysUserDao extends R2dbcRepository<SysUserEntity, Long> {
Mono<SysUserEntity> findByUsernameAndDeletedAtIsNull(String username);
Mono<SysUserEntity> findByEmailAndDeletedAtIsNull(String email);
Mono<SysUserEntity> findByIdAndDeletedAtIsNull(Long id);
Flux<SysUserEntity> findAll();
Flux<SysUserEntity> findAll(Sort sort);
Flux<SysUserEntity> findByDeletedAtIsNull();
Flux<SysUserEntity> findByDeletedAtIsNull(Sort sort);
Mono<Long> countByDeletedAtIsNull();
Flux<SysUserEntity> findByRoleId(Long roleId);
}

Some files were not shown because too many files have changed in this diff Show More