Explorar el Código

feat(api): 对外部第三方接口添加分布式限流功能

- 新增 @ApiRateLimit 注解,支持IP、全局和用户三种限流策略
- 基于Redis实现分布式限流,确保高并发调用稳定性
- 在LinkDataController关键接口上应用限流注解
- 限流阈值及提示消息支持自定义配置
- 实现切面ApiRateLimitAspect,自动校验请求频率
- 获取客户端IP作为限流维度,支持动态限流key生成
- 异常时允许请求通过,避免限流机制影响业务正常运行
SheepHy hace 1 día
padre
commit
e9a73580b9

+ 16 - 0
src/main/java/com/zsElectric/boot/charging/controller/LinkDataController.java

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.MapperFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.zsElectric.boot.charging.service.ChargingReceptionService;
+import com.zsElectric.boot.common.annotation.ApiRateLimit;
 import com.zsElectric.boot.common.annotation.Log;
 import com.zsElectric.boot.common.constant.ConnectivityConstants;
 import com.zsElectric.boot.common.enums.LogModuleEnum;
@@ -31,6 +32,15 @@ public class LinkDataController {
 
     private final JwtTokenUtil jwtTokenUtil;
 
+
+//    @ApiRateLimit(
+//            prefix = "third_party:query_token",  // Redis key前缀
+//            limitType = ApiRateLimit.LimitType.IP,  // 限流类型:IP/GLOBAL/USER
+//            count = 200,  // 时间窗口内允许的最大请求次数
+//            time = 60,    // 时间窗口大小(秒)
+//            message = "获取Token请求过于频繁,请稍后再试"  // 限流提示信息
+//    )
+
     /**
      *  获取Token
      *  Token作为全局唯一凭证,调用各接口时均需要使用,该接口作为双方获取Token的接口,双方均需要实现。
@@ -41,6 +51,7 @@ public class LinkDataController {
     @Operation(summary = "获取token")
     @PostMapping("/query_token")
     @Log(value = "获取token", module = LogModuleEnum.PARKING, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:query_token", limitType = ApiRateLimit.LimitType.IP, count = 200, time = 60, message = "获取Token请求过于频繁,请稍后再试")
     public ResponseParmsEntity getToken(@RequestBody RequestParmsEntity request) throws Exception {
         ResponseParmsEntity responseParmsEntity = new ResponseParmsEntity();
         try {
@@ -121,6 +132,7 @@ public class LinkDataController {
     @Operation(summary = "推送启动充电结果")
     @PostMapping("/notification_start_charge_result")
     @Log(value = "推送启动充电结果", module = LogModuleEnum.PARKING, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:start_charge", limitType = ApiRateLimit.LimitType.IP, count = 300, time = 60, message = "启动充电请求过于频繁,请稍后再试")
     public ResponseParmsEntity chargeResponse(@RequestBody RequestParmsEntity requestDTO){
         return chargingReceptionService.chargeResponse(requestDTO);
     }
@@ -131,6 +143,7 @@ public class LinkDataController {
     @Operation(summary = "推送充电状态")
     @PostMapping("/notification_equip_charge_status")
     @Log(value = "推送充电状态", module = LogModuleEnum.PARKING, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:charge_status", limitType = ApiRateLimit.LimitType.IP, count = 500, time = 60, message = "充电状态推送过于频繁,请稍后再试")
     public ResponseParmsEntity chargeStatusResponse(@RequestBody RequestParmsEntity requestDTO){
         return chargingReceptionService.chargeStatusResponse(requestDTO);
     }
@@ -141,6 +154,7 @@ public class LinkDataController {
     @Operation(summary = "推送停止充电结果")
     @PostMapping("/notification_stop_charge_result")
     @Log(value = "推送停止充电结果", module = LogModuleEnum.PARKING, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:stop_charge", limitType = ApiRateLimit.LimitType.IP, count = 300, time = 60, message = "停止充电请求过于频繁,请稍后再试")
     public ResponseParmsEntity stopChargeResponse(@RequestBody RequestParmsEntity requestDTO){
         return chargingReceptionService.stopChargeResponse(requestDTO);
     }
@@ -151,6 +165,7 @@ public class LinkDataController {
     @Operation(summary = "推送充电订单信息")
     @PostMapping("/notification_charge_order_info")
     @Log(value = "推送充电订单信息", module = LogModuleEnum.PARKING, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:charge_order", limitType = ApiRateLimit.LimitType.IP, count = 200, time = 60, message = "订单信息推送过于频繁,请稍后再试")
     public ResponseParmsEntity chargeOrderResponse(@RequestBody RequestParmsEntity requestDTO){
         return chargingReceptionService.chargeOrderResponse(requestDTO);
     }
@@ -163,6 +178,7 @@ public class LinkDataController {
     @Operation(summary = "设备状态变化推送")
     @PostMapping("/notification_stationStatus")
     @Log(value = "设备状态变化推送", module = LogModuleEnum.PARKING, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:station_status", limitType = ApiRateLimit.LimitType.IP, count = 500, time = 60, message = "设备状态推送过于频繁,请稍后再试")
     public ResponseParmsEntity stationStatus(@RequestBody RequestParmsEntity requestDTO){
         return chargingReceptionService.stationStatus(requestDTO);
     }

+ 77 - 0
src/main/java/com/zsElectric/boot/common/annotation/ApiRateLimit.java

@@ -0,0 +1,77 @@
+package com.zsElectric.boot.common.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * API接口限流注解
+ * <p>
+ * 用于对外部第三方接口进行限流控制,支持基于IP和全局的限流策略
+ *
+ * @author system
+ * @since 2025-12-19
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface ApiRateLimit {
+
+    /**
+     * 限流key前缀(用于区分不同接口)
+     * <p>
+     * 默认为空,使用方法签名作为key
+     */
+    String prefix() default "";
+
+    /**
+     * 限流类型
+     */
+    LimitType limitType() default LimitType.IP;
+
+    /**
+     * 时间窗口内允许的最大请求次数
+     * <p>
+     * 默认100次
+     */
+    int count() default 100;
+
+    /**
+     * 时间窗口大小
+     * <p>
+     * 默认60秒
+     */
+    long time() default 60;
+
+    /**
+     * 时间单位
+     * <p>
+     * 默认秒
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+    /**
+     * 限流提示信息
+     */
+    String message() default "请求过于频繁,请稍后再试";
+
+    /**
+     * 限流类型枚举
+     */
+    enum LimitType {
+        /**
+         * 基于IP地址限流
+         */
+        IP,
+
+        /**
+         * 全局限流(所有请求共享配额)
+         */
+        GLOBAL,
+
+        /**
+         * 基于用户限流(需要用户登录)
+         */
+        USER
+    }
+}

+ 164 - 0
src/main/java/com/zsElectric/boot/core/aspect/ApiRateLimitAspect.java

@@ -0,0 +1,164 @@
+package com.zsElectric.boot.core.aspect;
+
+import cn.hutool.core.util.StrUtil;
+import com.zsElectric.boot.common.annotation.ApiRateLimit;
+import com.zsElectric.boot.common.util.IPUtils;
+import com.zsElectric.boot.core.exception.BusinessException;
+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.aspectj.lang.reflect.MethodSignature;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * API接口限流切面
+ * <p>
+ * 基于Redis实现分布式限流,支持IP、全局和用户三种限流策略
+ *
+ * @author system
+ * @since 2025-12-19
+ */
+@Slf4j
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class ApiRateLimitAspect {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    private static final String RATE_LIMIT_KEY_PREFIX = "api:rate_limit:";
+
+    /**
+     * 切点:标注了 @ApiRateLimit 注解的方法
+     */
+    @Pointcut("@annotation(com.zsElectric.boot.common.annotation.ApiRateLimit)")
+    public void rateLimitPointcut() {
+    }
+
+    /**
+     * 环绕通知:执行限流逻辑
+     */
+    @Around("rateLimitPointcut()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        ApiRateLimit rateLimit = method.getAnnotation(ApiRateLimit.class);
+
+        if (rateLimit == null) {
+            return joinPoint.proceed();
+        }
+
+        // 构建限流key
+        String key = buildRateLimitKey(rateLimit, method);
+
+        // 执行限流检查
+        if (!tryAcquire(key, rateLimit)) {
+            log.warn("接口 [{}] 触发限流,key: {}", method.getName(), key);
+            throw new BusinessException(rateLimit.message());
+        }
+
+        return joinPoint.proceed();
+    }
+
+    /**
+     * 构建限流key
+     *
+     * @param rateLimit 限流注解
+     * @param method    方法
+     * @return 限流key
+     */
+    private String buildRateLimitKey(ApiRateLimit rateLimit, Method method) {
+        StringBuilder keyBuilder = new StringBuilder(RATE_LIMIT_KEY_PREFIX);
+
+        // 添加自定义前缀
+        if (StrUtil.isNotBlank(rateLimit.prefix())) {
+            keyBuilder.append(rateLimit.prefix()).append(":");
+        } else {
+            // 使用方法签名作为默认前缀
+            keyBuilder.append(method.getDeclaringClass().getSimpleName())
+                    .append(".")
+                    .append(method.getName())
+                    .append(":");
+        }
+
+        // 根据限流类型添加不同的标识
+        switch (rateLimit.limitType()) {
+            case IP:
+                String ip = getClientIp();
+                keyBuilder.append("ip:").append(ip);
+                break;
+            case GLOBAL:
+                keyBuilder.append("global");
+                break;
+            case USER:
+                // 这里可以根据实际情况获取用户ID,暂时使用IP作为替代
+                String userIp = getClientIp();
+                keyBuilder.append("user:").append(userIp);
+                break;
+            default:
+                keyBuilder.append("unknown");
+        }
+
+        return keyBuilder.toString();
+    }
+
+    /**
+     * 尝试获取限流令牌
+     *
+     * @param key       限流key
+     * @param rateLimit 限流注解
+     * @return 是否获取成功
+     */
+    private boolean tryAcquire(String key, ApiRateLimit rateLimit) {
+        try {
+            // 获取当前计数
+            Long count = redisTemplate.opsForValue().increment(key);
+
+            if (count == null) {
+                return false;
+            }
+
+            // 如果是第一次访问,设置过期时间
+            if (count == 1) {
+                long timeout = rateLimit.timeUnit().toSeconds(rateLimit.time());
+                redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
+            }
+
+            // 判断是否超过限流阈值
+            if (count > rateLimit.count()) {
+                log.warn("限流触发 - key: {}, count: {}, limit: {}", key, count, rateLimit.count());
+                return false;
+            }
+
+            return true;
+        } catch (Exception e) {
+            log.error("限流检查异常", e);
+            // 异常情况下允许通过,避免影响业务
+            return true;
+        }
+    }
+
+    /**
+     * 获取客户端IP地址
+     *
+     * @return IP地址
+     */
+    private String getClientIp() {
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes != null) {
+            HttpServletRequest request = attributes.getRequest();
+            return IPUtils.getIpAddr(request);
+        }
+        return "unknown";
+    }
+}