标注在 Controller 方法上,基于 Redis 计数窗口实现
+ * + * @author Ray.Hao + * @since 4.3.1 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimit { + + int limit() default 0; + + int window() default 0; + + TimeUnit timeUnit() default TimeUnit.SECONDS; + + String prefix() default "rate_limit:"; +} diff --git a/src/main/java/com/youlai/boot/common/aspect/LogAspect.java b/src/main/java/com/youlai/boot/common/aspect/LogAspect.java deleted file mode 100644 index 81d31082..00000000 --- a/src/main/java/com/youlai/boot/common/aspect/LogAspect.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.youlai.boot.common.aspect; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.http.useragent.UserAgent; -import cn.hutool.http.useragent.UserAgentUtil; -import com.youlai.boot.common.annotation.Log; -import com.youlai.boot.common.enums.ActionTypeEnum; -import com.youlai.boot.common.enums.LogModuleEnum; -import com.youlai.boot.common.util.IPUtils; -import com.youlai.boot.framework.security.util.SecurityUtils; -import com.youlai.boot.system.model.entity.SysLog; -import com.youlai.boot.system.service.LogService; -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.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import java.time.LocalDateTime; - -/** - * 日志切面 - * - * @author Ray.Hao - * @since 2.10.0 - */ -@Aspect -@Component -@RequiredArgsConstructor -@Slf4j -public class LogAspect { - - private final LogService logService; - - /** - * 日志注解切点 - */ - @Pointcut("@annotation(logAnnotation)") - public void logPointCut(Log logAnnotation) { - } - - /** - * 环绕通知:记录操作日志 - */ - @Around(value = "logPointCut(logAnnotation)", argNames = "pjp,logAnnotation") - public Object around(ProceedingJoinPoint pjp, Log logAnnotation) throws Throwable { - - long startTime = System.currentTimeMillis(); - // 在方法执行前获取用户信息,避免 logout 等操作清除 SecurityContext 后无法获取 - Long userId = SecurityUtils.getUserId(); - String username = SecurityUtils.getUsername(); - Object result = null; - Exception exception = null; - - try { - result = pjp.proceed(); - return result; - } catch (Exception e) { - exception = e; - throw e; - } finally { - long executionTime = System.currentTimeMillis() - startTime; - // fallback:登录等场景在 proceed() 前未认证,需在 proceed() 后获取 - if (userId == null) { - userId = SecurityUtils.getUserId(); - username = SecurityUtils.getUsername(); - } - try { - saveLogAsync(logAnnotation, executionTime, exception, userId, username); - } catch (Exception ex) { - log.error("保存操作日志失败", ex); - } - } - } - - /** - * 异步保存日志 - */ - @Async - public void saveLogAsync(Log logAnnotation, long executionTime, Exception exception, Long userId, String username) { - try { - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - return; - } - - HttpServletRequest request = attributes.getRequest(); - - // 解析 User-Agent - String userAgentStr = request.getHeader("User-Agent"); - UserAgent userAgent = UserAgentUtil.parse(userAgentStr); - String sn = request.getHeader("X-Device-SN"); - - // 解析 IP 地区 - String ip = IPUtils.getIpAddr(request); - String region = IPUtils.getRegion(ip); - String province = null; - String city = null; - if (StrUtil.isNotBlank(region)) { - String[] parts = region.split("\\|"); - if (parts.length >= 3) { - province = StrUtil.blankToDefault(parts[2], null); - city = StrUtil.blankToDefault(parts[3], null); - } - } - - // 构建日志实体 - LogModuleEnum module = logAnnotation.module(); - ActionTypeEnum actionType = logAnnotation.value(); - String title = StrUtil.blankToDefault(logAnnotation.title(), - module.getLabel() + "-" + actionType.getLabel()); - String content = logAnnotation.content(); - - SysLog logEntity = new SysLog(); - logEntity.setModule(module); - logEntity.setActionType(actionType); - logEntity.setTitle(title); - logEntity.setContent(content); - logEntity.setOperatorId(userId != null ? userId : 99); - logEntity.setOperatorName(username != null ? username : sn); - logEntity.setRequestUri(request.getRequestURI()); - logEntity.setRequestMethod(request.getMethod()); - logEntity.setIp(ip); - logEntity.setProvince(province); - logEntity.setCity(city); - logEntity.setDevice(userAgent.getOs().getName()); - logEntity.setOs(userAgent.getOs().getName()); - logEntity.setBrowser(userAgent.getBrowser().getName()); - logEntity.setStatus(exception == null ? 1 : 0); - logEntity.setErrorMsg(exception != null ? exception.getMessage() : null); - logEntity.setExecutionTime((int) executionTime); - logEntity.setCreateTime(LocalDateTime.now()); - - logService.save(logEntity); - } catch (Exception e) { - log.error("保存操作日志异常: {}", e.getMessage()); - } - } -} 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/BaseQuery.java b/src/main/java/com/youlai/boot/common/base/BaseQuery.java index 026d9673..7548937d 100644 --- a/src/main/java/com/youlai/boot/common/base/BaseQuery.java +++ b/src/main/java/com/youlai/boot/common/base/BaseQuery.java @@ -7,6 +7,12 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; +/** + * 查询参数基类 + * + * @author Ray.Hao + * @since 0.0.1 + */ @Data @Schema public class BaseQuery 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
- * 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。
- * 支持写入成功响应和错误响应。
- * 此类为工具类,所有方法均为静态方法,禁止实例化。
- *
- * @author Ray.Hao
- * @since 2.0.0
- */
-@Slf4j
-public final class ResponseWriter {
-
- /**
- * 私有构造函数,防止实例化
- */
- private ResponseWriter() {
- throw new UnsupportedOperationException("工具类不允许实例化");
- }
-
- /**
- * 写入成功响应
- *
- * @param response HttpServletResponse
- * @param data 响应数据(可选)
- */
- public static void writeSuccess(HttpServletResponse response, Object data) {
- writeResult(response, Result.success(data), HttpStatus.OK.value());
- }
-
- /**
- * 写入成功响应(无数据)
- *
- * @param response HttpServletResponse
- */
- public static void writeSuccess(HttpServletResponse response) {
- writeSuccess(response, null);
- }
-
- /**
- * 写入错误响应
- *
- * @param response HttpServletResponse
- * @param resultCode 响应结果码
- */
- public static void writeError(HttpServletResponse response, ResultCode resultCode) {
- writeError(response, resultCode, null);
- }
-
- /**
- * 写入错误响应(带自定义消息)
- *
- * @param response HttpServletResponse
- * @param resultCode 响应结果码
- * @param message 自定义消息(可选,为 null 时使用 resultCode 的默认消息)
- */
- public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
- Result> result = message == null
- ? Result.failed(resultCode)
- : Result.failed(resultCode, message);
-
- int httpStatus = mapHttpStatus(resultCode);
- writeResult(response, result, httpStatus);
- }
-
- /**
- * 写入响应结果(通用方法)
- *
- * @param response HttpServletResponse
- * @param result 响应结果对象
- * @param httpStatus HTTP状态码
- */
- private static void writeResult(HttpServletResponse response, Result> result, int httpStatus) {
- try {
- // 设置HTTP状态码
- response.setStatus(httpStatus);
-
- // 设置响应编码和内容类型
- response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
- response.setContentType(MediaType.APPLICATION_JSON_VALUE);
-
- // 写入响应
- JakartaServletUtil.write(response,
- JSONUtil.toJsonStr(result),
- MediaType.APPLICATION_JSON_VALUE
- );
-
- } catch (Exception e) {
- log.error("写入响应时发生未知异常: httpStatus={}, result={}", httpStatus, result, e);
- }
- }
-
- /**
- * 根据业务结果码映射HTTP状态码
- * 401: 未认证(token无效/过期)
- * 403: 权限不足
- * 400: 其他业务错误
- *
- * @param resultCode 业务结果码
- * @return HTTP状态码
- */
- private static int mapHttpStatus(ResultCode resultCode) {
- return switch (resultCode) {
- case ACCESS_UNAUTHORIZED,
- ACCESS_TOKEN_INVALID,
- REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
- case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value();
- default -> HttpStatus.BAD_REQUEST.value();
- };
- }
-}
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