如何为短信重发接口添加30秒频次校验并返回剩余等待时间
短信重发频次校验实现方案
核心逻辑是基于用户唯一标识(这里是login字段)记录上次短信发送时间,每次请求先判断和上次发送的时间间隔是否小于30秒,不满足间隔要求直接返回带剩余等待时长的错误,满足要求才执行原有发送逻辑,发送成功后更新记录的发送时间。
因为当前接口是WebFlux响应式返回Mono,所有校验逻辑都要用非阻塞实现,生产多实例部署优先用Redis做存储,单实例可以用本地缓存。
1. 定义基础常量与自定义异常
首先定义时间窗口常量,以及用来携带剩余等待时长的自定义业务异常:
// 短信重发最小间隔:30秒,单位毫秒 private static final long SMS_RESEND_INTERVAL_MS = 30 * 1000L; // 缓存key前缀,按用户login维度隔离 private static final String SMS_LIMIT_KEY_PREFIX = "sms:resend:limit:"; // 自定义频繁操作异常,携带剩余等待时长 public class SmsTooFrequentException extends RuntimeException { private final long remainWaitMs; public SmsTooFrequentException(long remainWaitMs) { super("操作过于频繁"); this.remainWaitMs = remainWaitMs; } public long getRemainWaitMs() { return remainWaitMs; } }
2. 改造控制器逻辑,加入前置校验
注入响应式Redis操作模板ReactiveStringRedisTemplate,在原有业务逻辑前加校验,发送成功后更新缓存的发送时间:
// 注入响应式Redis模板 @Autowired private ReactiveStringRedisTemplate redisTemplate; @PostMapping("/resend/sms") @PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('SCOPE_trusted')") public Mono<UserDTO> trustedResendRegistrationSms( @Validated @RequestBody RegistrationDTO registrationDTO) { String login = registrationDTO.getLogin(); String limitKey = SMS_LIMIT_KEY_PREFIX + login; long currentTs = System.currentTimeMillis(); return redisTemplate.opsForValue() .get(limitKey) // 无记录时默认给0,相当于从未发送过 .defaultIfEmpty("0") .flatMap(lastSendTsStr -> { long lastSendTs = Long.parseLong(lastSendTsStr); long gap = currentTs - lastSendTs; // 未到允许发送时间,直接抛异常 if (gap < SMS_RESEND_INTERVAL_MS) { long remainMs = SMS_RESEND_INTERVAL_MS - gap; return Mono.error(new SmsTooFrequentException(remainMs)); } // 校验通过,执行原有短信重发逻辑 return accountService.resendRegistrationSms(login) // 发送成功后记录本次发送时间,缓存30秒自动过期 .flatMap(userDTO -> redisTemplate.opsForValue() .set(limitKey, String.valueOf(currentTs), Duration.ofMillis(SMS_RESEND_INTERVAL_MS)) .thenReturn(userDTO)); }); }
3. 配置全局异常捕获,返回规范错误响应
添加全局异常处理器,捕获刚才定义的频繁操作异常,按要求返回错误提示和剩余等待时长:
@RestControllerAdvice public class BizExceptionHandler { @ExceptionHandler(SmsTooFrequentException.class) @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) public Mono<Map<String, Object>> handleSmsFrequent(SmsTooFrequentException e) { Map<String, Object> result = new HashMap<>(); result.put("code", 429); result.put("message", "操作过于频繁,请稍后再试"); // 剩余时长可按需返回单位,这里示例返回向上取整的秒数 result.put("remainWaitSeconds", (e.getRemainWaitMs() + 999) / 1000); return Mono.just(result); } }
补充说明
- 单实例部署不想引入Redis的话,把Redis存储替换成Caffeine响应式本地缓存即可,校验逻辑完全不用改
- 超高并发场景如果要严格防止并发穿透,可以把时间判断、写入的逻辑封装成Lua脚本在Redis侧原子执行,普通短信业务场景上面的实现完全满足需求
- 剩余时长的字段名、单位可以根据前后端约定调整,比如返回毫秒数、格式化的倒计时文本都可以
内容的提问来源于stack exchange,提问作者LAki
相关产品推荐
相关产品推荐

