You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为短信重发接口添加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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 22:15:37