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
@@ -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