diff --git a/pom.xml b/pom.xml index 100bc70f..910d0e6b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.youlai youlai-boot - 4.3.1 + 4.3.2 基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。 @@ -26,9 +26,9 @@ 1.2.24 3.5.15 - 4.3.1 4.5.0 + 2.8.9 1.6.3 0.2.0 @@ -54,8 +54,8 @@ 2.7.0 - 4.7.6 - 2.2.1 + 4.7.6 + 2.2.1 2.9.3 @@ -64,7 +64,6 @@ 4.8.1.B - @@ -81,7 +80,7 @@ ${hutool.version} - + com.alibaba transmittable-thread-local @@ -178,7 +177,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.8.9 + ${springdoc.version} @@ -201,7 +200,7 @@ ${xxl-job.version} - + cn.idev.excel fastexcel @@ -253,13 +252,13 @@ com.aliyun aliyun-java-sdk-core - ${aliyun.java.sdk.core.version} + ${aliyun-sdk-core.version} com.aliyun aliyun-java-sdk-dysmsapi - ${aliyun.java.sdk.dysmsapi.version} + ${aliyun-sdk-dysmsapi.version} @@ -276,13 +275,6 @@ ${weixin-java-miniapp.version} - - - diff --git a/src/main/java/com/youlai/boot/YouLaiBootApplication.java b/src/main/java/com/youlai/boot/YouLaiBootApplication.java index a40fcaaa..1b2761fd 100644 --- a/src/main/java/com/youlai/boot/YouLaiBootApplication.java +++ b/src/main/java/com/youlai/boot/YouLaiBootApplication.java @@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; * 应用启动类 * * @author Ray.Hao - * @since 0.0.1 + * @since 3.0.0 */ @EnableScheduling @SpringBootApplication diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index c159f577..305938e5 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -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; /** * 认证控制层 diff --git a/src/main/java/com/youlai/boot/common/annotation/DataPermission.java b/src/main/java/com/youlai/boot/common/annotation/DataPermission.java index 10f0d00a..f63832c8 100644 --- a/src/main/java/com/youlai/boot/common/annotation/DataPermission.java +++ b/src/main/java/com/youlai/boot/common/annotation/DataPermission.java @@ -5,7 +5,7 @@ import java.lang.annotation.*; /** * 数据权限注解 * - * @author zc + * @author Ray.Hao * @since 2.0.0 */ @Documented diff --git a/src/main/java/com/youlai/boot/common/annotation/Log.java b/src/main/java/com/youlai/boot/common/annotation/Log.java index a9caa742..c02d5608 100644 --- a/src/main/java/com/youlai/boot/common/annotation/Log.java +++ b/src/main/java/com/youlai/boot/common/annotation/Log.java @@ -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) diff --git a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java index d8e9dfc7..f53893bc 100644 --- a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java +++ b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java @@ -6,6 +6,9 @@ import java.util.concurrent.TimeUnit; /** * 接口限流 *

标注在 Controller 方法上,基于 Redis 计数窗口实现

+ * + * @author Ray.Hao + * @since 4.3.1 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java deleted file mode 100644 index 97011be1..00000000 --- a/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java +++ /dev/null @@ -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; - } - - -} - diff --git a/src/main/java/com/youlai/boot/common/base/BaseEntity.java b/src/main/java/com/youlai/boot/common/base/BaseEntity.java index 9c6a2812..70cd1a92 100644 --- a/src/main/java/com/youlai/boot/common/base/BaseEntity.java +++ b/src/main/java/com/youlai/boot/common/base/BaseEntity.java @@ -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; * *

实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等

* - * @author Ray - * @since 2024/6/23 + * @author Ray.Hao + * @since 4.3.1 */ @Data public class BaseEntity implements Serializable { diff --git a/src/main/java/com/youlai/boot/common/base/IBaseEnum.java b/src/main/java/com/youlai/boot/common/base/IBaseEnum.java index 0a44cd81..facc8635 100644 --- a/src/main/java/com/youlai/boot/common/base/IBaseEnum.java +++ b/src/main/java/com/youlai/boot/common/base/IBaseEnum.java @@ -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 { @@ -37,12 +36,12 @@ public interface IBaseEnum { } /** - * 根据文本标签获取值 + * 根据值获取标签 * - * @param value - * @param clazz - * @param - * @return + * @param value 枚举值 + * @param clazz 枚举类型 + * @param 枚举 + * @return 标签文本 */ static & IBaseEnum> String getLabelByValue(Object value, Class clazz) { Objects.requireNonNull(value); @@ -59,14 +58,13 @@ public interface IBaseEnum { return label; } - /** - * 根据文本标签获取值 + * 根据标签获取值 * - * @param label - * @param clazz - * @param - * @return + * @param label 标签文本 + * @param clazz 枚举类型 + * @param 枚举 + * @return 枚举值 */ static & IBaseEnum> Object getValueByLabel(String label, Class clazz) { Objects.requireNonNull(label); @@ -84,5 +82,4 @@ public interface IBaseEnum { return value; } - } diff --git a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java index 410ff1ed..93e24ccd 100644 --- a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java @@ -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:{}"; diff --git a/src/main/java/com/youlai/boot/common/enums/EnvEnum.java b/src/main/java/com/youlai/boot/common/enums/EnvEnum.java index 9f502fab..bfc1b8a5 100644 --- a/src/main/java/com/youlai/boot/common/enums/EnvEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/EnvEnum.java @@ -6,7 +6,7 @@ import lombok.Getter; /** * 环境枚举 * - * @author Ray + * @author Ray.Hao * @since 4.0.0 */ @Getter diff --git a/src/main/java/com/youlai/boot/common/enums/StatusEnum.java b/src/main/java/com/youlai/boot/common/enums/StatusEnum.java index 229086fa..7650dd02 100644 --- a/src/main/java/com/youlai/boot/common/enums/StatusEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/StatusEnum.java @@ -6,18 +6,17 @@ import lombok.Getter; /** * 状态枚举 * - * @author haoxr + * @author Ray.Hao * @since 2022/10/14 */ @Getter public enum StatusEnum implements IBaseEnum { ENABLE(1, "启用"), - DISABLE (0, "禁用"); + DISABLE(0, "禁用"); private final Integer value; - private final String label; StatusEnum(Integer value, String label) { diff --git a/src/main/java/com/youlai/boot/common/exception/BusinessException.java b/src/main/java/com/youlai/boot/common/exception/BusinessException.java index cd93205f..35286cea 100644 --- a/src/main/java/com/youlai/boot/common/exception/BusinessException.java +++ b/src/main/java/com/youlai/boot/common/exception/BusinessException.java @@ -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); } diff --git a/src/main/java/com/youlai/boot/common/result/Result.java b/src/main/java/com/youlai/boot/common/result/Result.java index 94153792..154b07f0 100644 --- a/src/main/java/com/youlai/boot/common/result/Result.java +++ b/src/main/java/com/youlai/boot/common/result/Result.java @@ -9,7 +9,7 @@ import java.io.Serializable; * 统一响应结构体 * * @author Ray.Hao - * @since 2022/1/30 + * @since 4.3.1 **/ @Data public class Result implements Serializable { @@ -64,10 +64,6 @@ public class Result implements Serializable { return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data); } - private static Result result(IResultCode resultCode, T data) { - return result(resultCode.getCode(), resultCode.getMsg(), data); - } - private static Result result(String code, String msg, T data) { Result result = new Result<>(); result.setCode(code); diff --git a/src/main/java/com/youlai/boot/common/result/ResultCode.java b/src/main/java/com/youlai/boot/common/result/ResultCode.java index dd7fa3d2..60b8668f 100644 --- a/src/main/java/com/youlai/boot/common/result/ResultCode.java +++ b/src/main/java/com/youlai/boot/common/result/ResultCode.java @@ -48,7 +48,7 @@ import java.io.Serializable; * * * @author Ray.Hao - * @since 2020/6/23 + * @since 4.3.1 **/ public enum ResultCode implements IResultCode, Serializable { diff --git a/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java b/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java index 2da50bd3..b06bb4cb 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java @@ -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() { diff --git a/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java index 8cb13732..f5e9d893 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java @@ -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 diff --git a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java index d2bd122e..7dd4e003 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java @@ -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; *

* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。 * - * @author zc - * @since 2021-12-10 13:28 + * @author Ray.Hao + * @since 2021/12/10 */ @Slf4j public class MyDataPermissionHandler implements DataPermissionHandler { diff --git a/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java b/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java index b64b6de0..fa541460 100644 --- a/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java +++ b/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java @@ -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 ); } diff --git a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java index 9b74c532..0692e0de 100644 --- a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java +++ b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java @@ -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; /** * 全局系统异常处理器 - *

- * 调整异常处理的HTTP状态码,丰富异常处理类型 + * + * @author Ray.Hao + * @since 4.3.1 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { - /** - * 处理绑定异常 - *

- * 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。 - */ @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.OK) public Result processException(BindException e) { @@ -54,12 +50,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); } - /** - * 处理 @RequestParam 参数校验异常 - *

- * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), - * 会捕获到 ConstraintViolationException 异常。 - */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.OK) public Result processException(ConstraintViolationException e) { @@ -68,12 +58,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INVALID_USER_INPUT, msg); } - /** - * 处理方法参数校验异常 - *

- * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, - * 会抛出 MethodArgumentNotValidException 异常。 - */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.OK) public Result processException(MethodArgumentNotValidException e) { @@ -82,11 +66,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INVALID_USER_INPUT, msg); } - /** - * 处理接口不存在的异常 - *

- * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 - */ @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Result processException(NoHandlerFoundException e) { @@ -94,11 +73,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INTERFACE_NOT_EXIST); } - /** - * 处理缺少请求参数的异常 - *

- * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 - */ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(HttpStatus.OK) public Result processException(MissingServletRequestParameterException e) { @@ -106,11 +80,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); } - /** - * 处理方法参数类型不匹配的异常 - *

- * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 - */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) @ResponseStatus(HttpStatus.OK) public Result processException(MethodArgumentTypeMismatchException e) { @@ -118,11 +87,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); } - /** - * 处理 Servlet 异常 - *

- * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 - */ @ExceptionHandler(ServletException.class) @ResponseStatus(HttpStatus.OK) public Result processException(ServletException e) { @@ -130,11 +94,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理非法参数异常 - *

- * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 - */ @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.OK) public Result handleIllegalArgumentException(IllegalArgumentException e) { @@ -142,11 +101,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理 JSON 处理异常 - *

- * 当处理 JSON 数据时发生错误,会抛出 JacksonException 异常。 - */ @ExceptionHandler(JacksonException.class) @ResponseStatus(HttpStatus.OK) public Result handleJacksonException(JacksonException e) { @@ -154,11 +108,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理请求体不可读的异常 - *

- * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 - */ @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseStatus(HttpStatus.OK) public Result processException(HttpMessageNotReadableException e) { @@ -171,11 +120,6 @@ public class GlobalExceptionHandler { return Result.failed(errorMessage); } - /** - * 处理类型不匹配异常 - *

- * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 - */ @ExceptionHandler(TypeMismatchException.class) @ResponseStatus(HttpStatus.OK) public Result processException(TypeMismatchException e) { @@ -183,11 +127,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理 SQL 语法错误异常 - *

- * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 - */ @ExceptionHandler(BadSqlGrammarException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result handleBadSqlGrammarException(BadSqlGrammarException e) { @@ -198,11 +137,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR); } - /** - * 处理 SQL 语法错误异常 - *

- * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 - */ @ExceptionHandler(SQLSyntaxErrorException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { @@ -210,12 +144,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR); } - - /** - * 处理 SQL 违反了完整性约束 - *

- * 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。 - */ @ExceptionHandler(SQLIntegrityConstraintViolationException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) { @@ -223,22 +151,12 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION); } - /** - * 处理 Token 无效异常 - *

- * 当 access_token 或 refresh_token 过期/无效时,返回 401。 - */ @ExceptionHandler(TokenInvalidException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public Result handleTokenInvalidException(TokenInvalidException e) { return Result.failed(e.getResultCode()); } - /** - * 处理业务异常 - *

- * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 - */ @ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.OK) public Result handleBizException(BusinessException e) { @@ -249,15 +167,9 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理所有未捕获的异常 - *

- * 当发生未捕获的异常时,会抛出 Exception 异常。 - */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result 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 = "\\[\"(.*?)\"]+"; diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java index 4276a604..bcbc377c 100644 --- a/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java +++ b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java @@ -26,6 +26,9 @@ import java.util.concurrent.TimeUnit; /** * 接口限流切面 *

对 @RateLimit 标注的方法做 Redis 计数,超阈值则拒绝

+ * + * @author Ray.Hao + * @since 4.3.1 */ @Aspect @Component diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java index e69de29b..63e2456c 100644 --- a/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java +++ b/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java @@ -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; + } + +} diff --git a/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java b/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java index 88354e30..c50edf6d 100644 --- a/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java +++ b/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java @@ -9,11 +9,9 @@ import java.util.concurrent.ThreadPoolExecutor; /** * 操作日志线程池 - *

core=1 确保日志按序写入,max=2 峰值扩容,queue=1000 缓冲突发流量, - * CallerRunsPolicy 保证满载时由调用线程兜底执行不丢数据。

* * @author Ray.Hao - * @since 0.0.1 + * @since 3.0.0 */ @Configuration public class OperationLogExecutorConfig { diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index f90e3f25..ce9a926c 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -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() - .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); diff --git a/src/main/java/com/youlai/boot/system/model/entity/Menu.java b/src/main/java/com/youlai/boot/system/model/entity/Menu.java index 180435f0..4839f40b 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Menu.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Menu.java @@ -48,6 +48,11 @@ public class Menu extends BaseEntity { */ private String component; + /** + * 外链地址 + */ + private String externalUrl; + /** * 权限标识 */ diff --git a/src/main/java/com/youlai/boot/system/model/form/MenuForm.java b/src/main/java/com/youlai/boot/system/model/form/MenuForm.java index dabc73ce..41cda185 100644 --- a/src/main/java/com/youlai/boot/system/model/form/MenuForm.java +++ b/src/main/java/com/youlai/boot/system/model/form/MenuForm.java @@ -38,6 +38,9 @@ public class MenuForm { @Schema(description = "组件路径(vue页面完整路径,省略.vue后缀)") private String component; + @Schema(description = "外链地址") + private String externalUrl; + @Schema(description = "权限标识") private String perm; diff --git a/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java b/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java index 81010597..2353a18e 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java @@ -31,6 +31,9 @@ public class MenuVO { @Schema(description = "组件路径") private String component; + @Schema(description = "外链地址") + private String externalUrl; + @Schema(description = "菜单排序(数字越小排名越靠前)") private Integer sort; diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index d0abf23f..48c5f58f 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -55,6 +55,15 @@ public interface UserService extends IService { */ boolean updateUser(Long userId, UserForm userForm); + /** + * 修改用户状态 + * + * @param userId 用户ID + * @param status 状态值 + * @return {@link Boolean} 是否修改成功 + */ + boolean updateUserStatus(Long userId, Integer status); + /** * 删除用户 diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index 8251829c..8ffef8d4 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -664,6 +664,14 @@ public class UserServiceImpl extends ServiceImpl implements ); } + @Override + public boolean updateUserStatus(Long userId, Integer status) { + return this.update(new LambdaUpdateWrapper() + .eq(SysUser::getId, userId) + .set(SysUser::getStatus, status) + ); + } + /** * 获取用户选项列表 * diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 12d1d96c..f3758e1b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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