refactor: 统一注释规范与代码风格,优化分层结构

This commit is contained in:
Ray.Hao
2026-06-28 19:04:21 +08:00
parent 8fd648c2e0
commit 49e3d7a71b
30 changed files with 215 additions and 286 deletions

View File

@@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
* 应用启动类
*
* @author Ray.Hao
* @since 0.0.1
* @since 3.0.0
*/
@EnableScheduling
@SpringBootApplication

View File

@@ -15,7 +15,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证控制层

View File

@@ -5,7 +5,7 @@ import java.lang.annotation.*;
/**
* 数据权限注解
*
* @author zc
* @author Ray.Hao
* @since 2.0.0
*/
@Documented

View File

@@ -3,13 +3,17 @@ package com.youlai.boot.common.annotation;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import java.lang.annotation.*;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 日志注解
*
* @author Ray
* @since 2024/6/25
* @author Ray.Hao
* @since 3.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)

View File

@@ -6,6 +6,9 @@ import java.util.concurrent.TimeUnit;
/**
* 接口限流
* <p>标注在 Controller 方法上,基于 Redis 计数窗口实现</p>
*
* @author Ray.Hao
* @since 4.3.1
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

View File

@@ -1,102 +0,0 @@
package com.youlai.boot.common.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;
/**
* 防重复提交切面
*
* @author Ray.Hao
* @since 2.3.0
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RepeatSubmitAspect {
private final RedissonClient redissonClient;
/**
* 防重复提交切点
*/
@Pointcut("@annotation(repeatSubmit)")
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
}
/**
* 环绕通知:处理防重复提交逻辑
*/
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
String lockKey = buildLockKey();
int expire = repeatSubmit.expire();
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
}
return pjp.proceed();
}
/**
* 生成防重复提交锁的 key
* @return 锁的 key
*/
private String buildLockKey() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 用户唯一标识
String userIdentifier = getUserIdentifier(request);
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
}
/**
* 获取用户唯一标识
* 1. 从请求头中获取 Token使用 SHA-256 加密 Token 作为用户唯一标识
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
*
* @param request 请求对象
* @return 用户唯一标识
*/
private String getUserIdentifier(HttpServletRequest request) {
// 用户身份唯一标识
String userIdentifier;
// 从请求头中获取 Token
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
} else {
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
}
return userIdentifier;
}
}

View File

@@ -1,6 +1,9 @@
package com.youlai.boot.common.base;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@@ -14,8 +17,8 @@ import java.time.LocalDateTime;
*
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p>
*
* @author Ray
* @since 2024/6/23
* @author Ray.Hao
* @since 4.3.1
*/
@Data
public class BaseEntity implements Serializable {

View File

@@ -1,6 +1,5 @@
package com.youlai.boot.common.base;
import cn.hutool.core.util.ObjectUtil;
import java.util.EnumSet;
@@ -9,8 +8,8 @@ import java.util.Objects;
/**
* 枚举通用接口
*
* @author haoxr
* @since 2022/3/27 12:06
* @author Ray.Hao
* @since 4.3.1
*/
public interface IBaseEnum<T> {
@@ -37,12 +36,12 @@ public interface IBaseEnum<T> {
}
/**
* 根据文本标签获取
* 根据值获取标签
*
* @param value
* @param clazz
* @param <E>
* @return
* @param value 枚举值
* @param clazz 枚举类型
* @param <E> 枚举
* @return 标签文本
*/
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
Objects.requireNonNull(value);
@@ -59,14 +58,13 @@ public interface IBaseEnum<T> {
return label;
}
/**
* 根据文本标签获取值
* 根据标签获取值
*
* @param label
* @param clazz
* @param <E>
* @return
* @param label 标签文本
* @param clazz 枚举类型
* @param <E> 枚举
* @return 枚举值
*/
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
Objects.requireNonNull(label);
@@ -84,5 +82,4 @@ public interface IBaseEnum<T> {
return value;
}
}

View File

@@ -4,7 +4,7 @@ package com.youlai.boot.common.constant;
* Redis 常量
*
* @author Theo
* @since 2024-7-29 11:46:08
* @since 4.3.1
*/
public interface RedisConstants {
@@ -32,7 +32,7 @@ public interface RedisConstants {
String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
// 用户与访问令牌的映射userId -> accessToken
String USER_ACCESS_TOKEN = "auth:user:access:{}";
// 用户与刷新令牌的映射userId -> refreshToken
// 用户与刷新令牌的映射userId -> refreshToken
String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
// 已撤销 Token 的 JTI单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效
String BLACKLIST_TOKEN = "auth:token:blacklist:{}";

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
/**
* 环境枚举
*
* @author Ray
* @author Ray.Hao
* @since 4.0.0
*/
@Getter

View File

@@ -6,18 +6,17 @@ import lombok.Getter;
/**
* 状态枚举
*
* @author haoxr
* @author Ray.Hao
* @since 2022/10/14
*/
@Getter
public enum StatusEnum implements IBaseEnum<Integer> {
ENABLE(1, "启用"),
DISABLE (0, "禁用");
DISABLE(0, "禁用");
private final Integer value;
private final String label;
StatusEnum(Integer value, String label) {

View File

@@ -20,13 +20,11 @@ public class BusinessException extends RuntimeException {
this.resultCode = errorCode;
}
public BusinessException(IResultCode errorCode, String message) {
super(message);
this.resultCode = errorCode;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}

View File

@@ -9,7 +9,7 @@ import java.io.Serializable;
* 统一响应结构体
*
* @author Ray.Hao
* @since 2022/1/30
* @since 4.3.1
**/
@Data
public class Result<T> implements Serializable {
@@ -64,10 +64,6 @@ public class Result<T> implements Serializable {
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data);
}
private static <T> Result<T> result(IResultCode resultCode, T data) {
return result(resultCode.getCode(), resultCode.getMsg(), data);
}
private static <T> Result<T> result(String code, String msg, T data) {
Result<T> result = new Result<>();
result.setCode(code);

View File

@@ -48,7 +48,7 @@ import java.io.Serializable;
* </pre>
*
* @author Ray.Hao
* @since 2020/6/23
* @since 4.3.1
**/
public enum ResultCode implements IResultCode, Serializable {

View File

@@ -12,10 +12,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* mybatis-plus 配置类
* MyBatis-Plus 配置类
*
* @author Ray.Hao
* @since 2022/7/2
* @since 4.3.1
*/
@Configuration
@EnableTransactionManagement
@@ -38,7 +38,7 @@ public class MybatisConfig {
}
/**
* 自动填充数据库创建人、创建时间、更新人、更新时间
* 自动填充创建时间和更新时间
*/
@Bean
public GlobalConfig globalConfig() {

View File

@@ -1,17 +1,18 @@
package com.youlai.boot.framework.mybatis.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* mybatis-plus 字段自动填充
* MyBatis-Plus 字段自动填充
*
* @author Ray.Hao
* @since 3.0.0
*/
@Component
@RequiredArgsConstructor
public class AutoFillMetaObjectHandler implements MetaObjectHandler {
@Override

View File

@@ -11,7 +11,8 @@ import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.framework.security.util.SecurityUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
@@ -30,8 +31,8 @@ import java.util.List;
* <p>
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
*
* @author zc
* @since 2021-12-10 13:28
* @author Ray.Hao
* @since 2021/12/10
*/
@Slf4j
public class MyDataPermissionHandler implements DataPermissionHandler {

View File

@@ -13,10 +13,9 @@ import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
@@ -35,7 +34,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
* Spring Security 配置类
*
* @author Ray.Hao
* @since 2023/2/17
* @since 4.3.1
*/
@Configuration
@EnableWebSecurity
@@ -147,7 +146,7 @@ public class SecurityConfig {
return new ProviderManager(
daoAuthenticationProvider,
smsAuthenticationProvider,
wxMaAuthenticationProvider
wxMaAuthenticationProvider
);
}

View File

@@ -1,7 +1,6 @@
package com.youlai.boot.framework.web.advice;
import cn.hutool.core.util.StrUtil;
import tools.jackson.core.JacksonException;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.result.ResultCode;
@@ -25,6 +24,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import tools.jackson.core.JacksonException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.sql.SQLSyntaxErrorException;
@@ -34,18 +34,14 @@ import java.util.stream.Collectors;
/**
* 全局系统异常处理器
* <p>
* 调整异常处理的HTTP状态码丰富异常处理类型
*
* @author Ray.Hao
* @since 4.3.1
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理绑定异常
* <p>
* 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(BindException e) {
@@ -54,12 +50,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
}
/**
* 处理 @RequestParam 参数校验异常
* <p>
* 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过),
* 会捕获到 ConstraintViolationException 异常。
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(ConstraintViolationException e) {
@@ -68,12 +58,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
}
/**
* 处理方法参数校验异常
* <p>
* 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败,
* 会抛出 MethodArgumentNotValidException 异常。
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MethodArgumentNotValidException e) {
@@ -82,11 +66,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
}
/**
* 处理接口不存在的异常
* <p>
* 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public <T> Result<T> processException(NoHandlerFoundException e) {
@@ -94,11 +73,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INTERFACE_NOT_EXIST);
}
/**
* 处理缺少请求参数的异常
* <p>
* 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MissingServletRequestParameterException e) {
@@ -106,11 +80,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY);
}
/**
* 处理方法参数类型不匹配的异常
* <p>
* 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
@@ -118,11 +87,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
}
/**
* 处理 Servlet 异常
* <p>
* 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。
*/
@ExceptionHandler(ServletException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(ServletException e) {
@@ -130,11 +94,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage());
}
/**
* 处理非法参数异常
* <p>
* 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
@@ -142,11 +101,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage());
}
/**
* 处理 JSON 处理异常
* <p>
* 当处理 JSON 数据时发生错误,会抛出 JacksonException 异常。
*/
@ExceptionHandler(JacksonException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleJacksonException(JacksonException e) {
@@ -154,11 +108,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage());
}
/**
* 处理请求体不可读的异常
* <p>
* 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(HttpMessageNotReadableException e) {
@@ -171,11 +120,6 @@ public class GlobalExceptionHandler {
return Result.failed(errorMessage);
}
/**
* 处理类型不匹配异常
* <p>
* 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。
*/
@ExceptionHandler(TypeMismatchException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(TypeMismatchException e) {
@@ -183,11 +127,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage());
}
/**
* 处理 SQL 语法错误异常
* <p>
* 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。
*/
@ExceptionHandler(BadSqlGrammarException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
@@ -198,11 +137,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR);
}
/**
* 处理 SQL 语法错误异常
* <p>
* 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。
*/
@ExceptionHandler(SQLSyntaxErrorException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
@@ -210,12 +144,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR);
}
/**
* 处理 SQL 违反了完整性约束
* <p>
* 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
@@ -223,22 +151,12 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
}
/**
* 处理 Token 无效异常
* <p>
* 当 access_token 或 refresh_token 过期/无效时,返回 401。
*/
@ExceptionHandler(TokenInvalidException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public <T> Result<T> handleTokenInvalidException(TokenInvalidException e) {
return Result.failed(e.getResultCode());
}
/**
* 处理业务异常
* <p>
* 当业务逻辑发生错误时,会抛出 BusinessException 异常。
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleBizException(BusinessException e) {
@@ -249,15 +167,9 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage());
}
/**
* 处理所有未捕获的异常
* <p>
* 当发生未捕获的异常时,会抛出 Exception 异常。
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleException(Exception e) throws Exception {
// 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
if (e instanceof AccessDeniedException
|| e instanceof AuthenticationException) {
throw e;
@@ -266,12 +178,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getLocalizedMessage());
}
/**
* 传参类型错误时,用于消息转换
*
* @param throwable 异常
* @return 错误信息
*/
private String convertMessage(Throwable throwable) {
String error = throwable.toString();
String regulation = "\\[\"(.*?)\"]+";

View File

@@ -26,6 +26,9 @@ import java.util.concurrent.TimeUnit;
/**
* 接口限流切面
* <p>对 @RateLimit 标注的方法做 Redis 计数,超阈值则拒绝</p>
*
* @author Ray.Hao
* @since 4.3.1
*/
@Aspect
@Component

View File

@@ -0,0 +1,84 @@
package com.youlai.boot.framework.web.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;
/**
* 防重复提交切面
*
* @author Ray.Hao
* @since 4.3.1
*/
@Aspect
@Component
@RequiredArgsConstructor
public class RepeatSubmitAspect {
private final RedissonClient redissonClient;
@Pointcut("@annotation(repeatSubmit)")
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
}
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
String lockKey = buildLockKey();
int expire = repeatSubmit.expire();
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
}
return pjp.proceed();
}
/**
* 生成防重复提交锁的 key
*/
private String buildLockKey() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String userIdentifier = getUserIdentifier(request);
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
}
/**
* 获取用户唯一标识
* 1. 从请求头中获取 Token使用 SHA-256 加密 Token 作为用户唯一标识
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
*/
private String getUserIdentifier(HttpServletRequest request) {
String userIdentifier;
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
userIdentifier = DigestUtil.sha256Hex(rawToken);
} else {
userIdentifier = IPUtils.getIpAddr(request);
}
return userIdentifier;
}
}

View File

@@ -9,11 +9,9 @@ import java.util.concurrent.ThreadPoolExecutor;
/**
* 操作日志线程池
* <p>core=1 确保日志按序写入max=2 峰值扩容queue=1000 缓冲突发流量,
* CallerRunsPolicy 保证满载时由调用线程兜底执行不丢数据。</p>
*
* @author Ray.Hao
* @since 0.0.1
* @since 3.0.0
*/
@Configuration
public class OperationLogExecutorConfig {

View File

@@ -2,11 +2,11 @@ package com.youlai.boot.system.controller;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.enums.StatusEnum;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.common.result.ExcelResult;
@@ -14,9 +14,14 @@ import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.util.ExcelUtils;
import com.youlai.boot.system.listener.UserImportListener;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.form.EmailUpdateForm;
import com.youlai.boot.system.model.form.MobileUpdateForm;
import com.youlai.boot.system.model.form.PasswordUpdateForm;
import com.youlai.boot.system.model.form.PasswordVerifyForm;
import com.youlai.boot.system.model.form.UserForm;
import com.youlai.boot.system.model.form.UserImportForm;
import com.youlai.boot.system.model.form.UserProfileForm;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.entity.SysUser;
import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserPageVO;
@@ -32,7 +37,16 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@@ -46,7 +60,7 @@ import java.util.List;
* 用户控制层
*
* @author Ray.Hao
* @since 2022/10/16
* @since 4.3.1
*/
@Tag(name = "02.用户接口")
@RestController
@@ -120,12 +134,9 @@ public class UserController {
@Parameter(description = "用户ID") @PathVariable Long userId,
@Parameter(description = "用户状态(1:启用;0:禁用)") @RequestParam Integer status
) {
boolean result = userService.update(new LambdaUpdateWrapper<SysUser>()
.eq(SysUser::getId, userId)
.set(SysUser::getStatus, status)
);
boolean result = userService.updateUserStatus(userId, status);
// 用户禁用时立即失效其会话
if (result && status == 0) {
if (result && StatusEnum.DISABLE.getValue().equals(status)) {
tokenManager.invalidateUserSessions(userId);
}
return Result.judge(result);

View File

@@ -48,6 +48,11 @@ public class Menu extends BaseEntity {
*/
private String component;
/**
* 外链地址
*/
private String externalUrl;
/**
* 权限标识
*/

View File

@@ -38,6 +38,9 @@ public class MenuForm {
@Schema(description = "组件路径(vue页面完整路径省略.vue后缀)")
private String component;
@Schema(description = "外链地址")
private String externalUrl;
@Schema(description = "权限标识")
private String perm;

View File

@@ -31,6 +31,9 @@ public class MenuVO {
@Schema(description = "组件路径")
private String component;
@Schema(description = "外链地址")
private String externalUrl;
@Schema(description = "菜单排序(数字越小排名越靠前)")
private Integer sort;

View File

@@ -55,6 +55,15 @@ public interface UserService extends IService<SysUser> {
*/
boolean updateUser(Long userId, UserForm userForm);
/**
* 修改用户状态
*
* @param userId 用户ID
* @param status 状态值
* @return {@link Boolean} 是否修改成功
*/
boolean updateUserStatus(Long userId, Integer status);
/**
* 删除用户

View File

@@ -664,6 +664,14 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
);
}
@Override
public boolean updateUserStatus(Long userId, Integer status) {
return this.update(new LambdaUpdateWrapper<SysUser>()
.eq(SysUser::getId, userId)
.set(SysUser::getStatus, status)
);
}
/**
* 获取用户选项列表
*

View File

@@ -162,14 +162,16 @@ springdoc:
packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档
- com.youlai.boot.auth.controller
- com.youlai.boot.system.controller
- com.youlai.boot.module.file.controller
- com.youlai.boot.module.codegen.controller
default-flat-param-object: true # 将对象参数扁平化显示在文档中
- com.youlai.boot.file.controller
- com.youlai.boot.codegen.controller
- com.youlai.boot.message.controller
default-flat-param-object: true
# knife4j 配置
knife4j:
enable: true # 是否启用 Knife4j 增强功能
production: false # 是否启用生产环境保护true=生产环境隐藏文档false=开发环境可访问)
enable: true
# 生产环境设置为 true 关闭文档
production: false
setting:
language: zh_cn