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 7039965e..e7e73424 100644 --- a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java +++ b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java @@ -1,11 +1,10 @@ package com.youlai.boot.common.annotation; import java.lang.annotation.*; -import java.util.concurrent.TimeUnit; /** * 接口限流 - *

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

+ *

标注在 Controller 方法上,基于 Redis 滑动窗口实现

* * @author Ray.Hao * @since 4.3.1 @@ -23,21 +22,9 @@ public @interface RateLimit { int limit() default 0; /** - * 滑动窗口大小。 + * 滑动窗口大小(秒)。 *

- * 单位由 {@link #timeUnit()} 决定;{@code <=0} 时使用全局默认值。 + * {@code <=0} 时使用全局默认值 {@code rate-limit.default-window}。 */ int window() default 0; - - /** - * 窗口单位,默认秒。 - */ - TimeUnit timeUnit() default TimeUnit.SECONDS; - - /** - * 限流 Key 的分组标签,用于区分不同接口。 - *

- * 默认 {@code api}。 - */ - String prefix() default "api"; } diff --git a/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java b/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java index 610380d9..8014f69f 100644 --- a/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java +++ b/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java @@ -24,12 +24,10 @@ public class RedisConfig { * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer * * @param redisConnectionFactory {@link RedisConnectionFactory} - * @param jsonMapper Jackson 序列化器(统一使用不写入类型信息的配置) * @return {@link RedisTemplate} */ @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, - JsonMapper jsonMapper) { + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); @@ -38,7 +36,9 @@ public class RedisConfig { redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); - // Value 使用自定义 JSON 序列化(不写入类型信息,避免 HashSet 等集合被序列化成带 @class 的结构) + JsonMapper jsonMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); JacksonJsonRedisSerializer jsonSerializer = new JacksonJsonRedisSerializer<>(jsonMapper, Object.class); redisTemplate.setValueSerializer(jsonSerializer); @@ -48,17 +48,4 @@ public class RedisConfig { return redisTemplate; } - /** - * 统一的 JsonMapper Bean - *

- * 禁止将日期序列化为时间戳;不写入类型信息,保证 Redis 存储的 JSON 纯净可读。 - * 需要反序列化到特定类型时,调用方应使用 {@link JsonMapper#convertValue(Object, Class)} 显式转换。 - */ - @Bean - public JsonMapper jsonMapper() { - return JsonMapper.builder() - .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) - .build(); - } - } 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 1e2b1367..31639b96 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 @@ -55,8 +55,9 @@ public class RateLimitAspect { String key = buildKey(request, rateLimit); int limit = rateLimit.limit() > 0 ? rateLimit.limit() : rateLimitProperties.getDefaultLimit(); - int window = rateLimit.window() > 0 ? rateLimit.window() : rateLimitProperties.getDefaultWindowSeconds(); - long windowMs = rateLimit.timeUnit().toMillis(window); + long windowMs = rateLimit.window() > 0 + ? rateLimit.window() * 1000L + : rateLimitProperties.getDefaultWindow().toMillis(); Long count = SlidingWindowScript.execute(redisTemplate, key, windowMs); @@ -73,8 +74,7 @@ public class RateLimitAspect { private String buildKey(HttpServletRequest request, RateLimit rateLimit) { String user = resolveUser(request); - return StrUtil.format(RedisConstants.RateLimit.API, - rateLimit.prefix(), user, request.getRequestURI()); + return StrUtil.format(RedisConstants.RateLimit.API, user, request.getRequestURI()); } private String resolveUser(HttpServletRequest request) { diff --git a/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java index 288d4410..72d8c5f4 100644 --- a/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java +++ b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java @@ -4,11 +4,11 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import java.time.Duration; + /** * 限流配置 - *

- * 对应 application.yml 中的 {@code rate-limit} 配置节点。 - *

+ *

对应 application.yml 中的 {@code rate-limit} 配置节点。

* * @author Ray.Hao * @since 4.3.1 @@ -24,9 +24,9 @@ public class RateLimitProperties { private int defaultLimit = 5; /** - * @RateLimit 注解未显式指定 window 时的默认窗口大小(秒) + * @RateLimit 注解未显式指定 window 时的默认窗口大小,支持 60s / 1m / 500ms 等 Duration 写法 */ - private int defaultWindowSeconds = 60; + private Duration defaultWindow = Duration.ofSeconds(60); /** * IP 全局限流配置 @@ -48,9 +48,9 @@ public class RateLimitProperties { private int limit = 1000; /** - * 窗口大小(秒,默认 60) + * 滑动窗口大小,支持 60s / 1m / 500ms 等 Duration 写法(默认 60s) */ - private int windowSeconds = 60; + private Duration window = Duration.ofSeconds(60); } diff --git a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java index bc1c66ea..b6d2b13f 100644 --- a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java +++ b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java @@ -68,7 +68,7 @@ public class IpRateLimitFilter extends OncePerRequestFilter { String ip = IPUtils.getIpAddr(request); String key = StrUtil.format(RedisConstants.RateLimit.IP, ip); - long windowMs = ipConfig.getWindowSeconds() * 1000L; + long windowMs = ipConfig.getWindow().toMillis(); // 执行滑动窗口计数(Lua 原子操作) Long count = SlidingWindowScript.execute(redisTemplate, key, windowMs); @@ -80,7 +80,7 @@ public class IpRateLimitFilter extends OncePerRequestFilter { if (current > limit) { log.warn("IP 限流触发 ip={} count={} limit={}", ip, current, limit); - response.setHeader("Retry-After", String.valueOf(ipConfig.getWindowSeconds())); + response.setHeader("Retry-After", String.valueOf(ipConfig.getWindow().getSeconds())); ResponseWriter.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED); return; } diff --git a/src/main/java/com/youlai/boot/system/controller/HealthController.java b/src/main/java/com/youlai/boot/system/controller/HealthController.java new file mode 100644 index 00000000..09b0652d --- /dev/null +++ b/src/main/java/com/youlai/boot/system/controller/HealthController.java @@ -0,0 +1,26 @@ +package com.youlai.boot.system.controller; + +import com.youlai.boot.common.result.Result; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 系统健康检查接口,返回 ok。 + * 不标注 @RateLimit,仅受 IP 全局限流层约束,用作该层的验证端点。 + */ +@Tag(name = "00.系统健康检查") +@RestController +@RequestMapping("/api/v1/health") +@Slf4j +public class HealthController { + + @Operation(summary = "健康检查", description = "返回 ok,供限流测试与探针使用") + @GetMapping + public Result health() { + return Result.success("ok"); + } +}