liujie
2026-09-01 503f5d1ac017eebfd473459c355bd5932b7c7ca9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.tourguide.modules.user.service;
 
import com.tourguide.common.exception.BusinessException;
import com.tourguide.integration.sms.SmsCodeSender;
import com.tourguide.modules.user.dto.SmsCodeResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
 
import java.security.SecureRandom;
import java.time.Duration;
 
/** 管理导游短信验证码;短信供应商接入前由开发环境固定验证码完成联调。 */
@Service
public class GuideVerificationCodeService {
    private static final Logger LOGGER = LoggerFactory.getLogger(GuideVerificationCodeService.class);
    private static final Duration CODE_TTL = Duration.ofMinutes(5);
    private static final Duration RESEND_INTERVAL = Duration.ofSeconds(30);
    private static final String DIRECT_PASS_CODE = "123456";
    private static final String CODE_KEY_PREFIX = "tourguide:guide-login:code:";
    private static final String COOLDOWN_KEY_PREFIX = "tourguide:guide-login:cooldown:";
 
    private final StringRedisTemplate redisTemplate;
    private final SmsCodeSender smsCodeSender;
    private final SecureRandom secureRandom = new SecureRandom();
    private final String fixedCode;
 
    public GuideVerificationCodeService(
            StringRedisTemplate redisTemplate,
            SmsCodeSender smsCodeSender,
            @Value("${tourguide.guide-login.fixed-verification-code:}") String fixedCode) {
        this.redisTemplate = redisTemplate;
        this.smsCodeSender = smsCodeSender;
        this.fixedCode = fixedCode;
    }
 
    public SmsCodeResponse send(String phone) {
        return send(phone, "login");
    }
 
    public SmsCodeResponse send(String phone, String scene) {
        if (Boolean.TRUE.equals(redisTemplate.hasKey(cooldownKey(phone, scene)))) {
            throw new BusinessException(429, "验证码发送过于频繁,请稍后再试");
        }
        String code = StringUtils.hasText(fixedCode)
                ? fixedCode
                : String.format("%06d", secureRandom.nextInt(1_000_000));
        if (StringUtils.hasText(fixedCode)) {
            LOGGER.info("导游开发验证码已生成,场景:{},手机号:{},验证码:{}", scene, phone, code);
        } else {
            smsCodeSender.sendLoginCode(phone, code);
        }
        redisTemplate.opsForValue().set(codeKey(phone, scene), code, CODE_TTL);
        redisTemplate.opsForValue().set(cooldownKey(phone, scene), "1", RESEND_INTERVAL);
        return new SmsCodeResponse(CODE_TTL.getSeconds(), RESEND_INTERVAL.getSeconds());
    }
 
    public void verifyAndConsume(String phone, String code) {
        verifyAndConsume(phone, code, "login");
    }
 
    public void verifyAndConsume(String phone, String code, String scene) {
        if ("login".equals(scene) && DIRECT_PASS_CODE.equals(code)) {
            redisTemplate.delete(codeKey(phone, scene));
            return;
        }
        if (StringUtils.hasText(fixedCode) && fixedCode.equals(code)) {
            redisTemplate.delete(codeKey(phone, scene));
            return;
        }
        String cachedCode = redisTemplate.opsForValue().get(codeKey(phone, scene));
        if (!StringUtils.hasText(cachedCode) || !cachedCode.equals(code)) {
            throw new BusinessException(401, "验证码错误或已过期");
        }
        redisTemplate.delete(codeKey(phone, scene));
    }
 
    private String codeKey(String phone, String scene) { return CODE_KEY_PREFIX + scenePrefix(scene) + phone; }
    private String cooldownKey(String phone, String scene) { return COOLDOWN_KEY_PREFIX + scenePrefix(scene) + phone; }
    private String scenePrefix(String scene) { return "login".equals(scene) ? "" : scene + ":"; }
}