“mitao”
2026-04-07 42dd06b5f83968155817b942200676cf521bb7fe
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package com.tcm.training.admin.app.auth.service;
 
import com.tcm.training.admin.app.auth.config.AppSmsAuthProperties;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
 
/**
 * APP 登录短信验证码服务,负责验证码的生成、缓存和校验。
 */
@Service
public class AppSmsCodeService {
 
    private static final String LOGIN_CODE_CACHE_PREFIX = "app:sms:login:";
    private static final String REGISTER_CODE_CACHE_PREFIX = "app:sms:register:";
    private static final String PASSWORD_RESET_CODE_CACHE_PREFIX = "app:sms:password-reset:";
 
    private final StringRedisTemplate stringRedisTemplate;
    private final AppSmsSender appSmsSender;
    private final AppSmsAuthProperties appSmsAuthProperties;
 
    /**
     * 注入验证码缓存、发送器与调试配置。
     */
    public AppSmsCodeService(StringRedisTemplate stringRedisTemplate,
                             AppSmsSender appSmsSender,
                             AppSmsAuthProperties appSmsAuthProperties) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.appSmsSender = appSmsSender;
        this.appSmsAuthProperties = appSmsAuthProperties;
    }
 
    /**
     * 生成并发送登录验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createLoginCode(String phone) {
        return createCode(phone, LOGIN_CODE_CACHE_PREFIX, "login");
    }
 
    /**
     * 生成并发送注册验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createRegisterCode(String phone) {
        return createCode(phone, REGISTER_CODE_CACHE_PREFIX, "register");
    }
 
    /**
     * 生成并发送忘记密码验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createPasswordResetCode(String phone) {
        return createCode(phone, PASSWORD_RESET_CODE_CACHE_PREFIX, "password-reset");
    }
 
    /**
     * 校验登录验证码,成功后销毁缓存中的一次性验证码。
     */
    public void validateLoginCode(String phone, String code) {
        validateCode(phone, code, LOGIN_CODE_CACHE_PREFIX);
    }
 
    /**
     * 校验注册验证码,成功后销毁缓存中的一次性验证码。
     */
    public void validateRegisterCode(String phone, String code) {
        validateCode(phone, code, REGISTER_CODE_CACHE_PREFIX);
    }
 
    /**
     * 校验忘记密码验证码,成功后销毁缓存中的一次性验证码。
     */
    public void validatePasswordResetCode(String phone, String code) {
        validateCode(phone, code, PASSWORD_RESET_CODE_CACHE_PREFIX);
    }
 
    /**
     * 统一生成指定场景的验证码并写入缓存。
     */
    private AppSmsCodeSendResponse createCode(String phone, String cachePrefix, String scene) {
        String code = generateCode();
        stringRedisTemplate.opsForValue().set(
                buildCacheKey(cachePrefix, phone),
                code,
                appSmsAuthProperties.getExpireSeconds(),
                TimeUnit.SECONDS
        );
        if ("login".equals(scene)) {
            appSmsSender.sendLoginCode(phone, code);
        } else if ("register".equals(scene)) {
            appSmsSender.sendRegisterCode(phone, code);
        } else {
            appSmsSender.sendPasswordResetCode(phone, code);
        }
        return new AppSmsCodeSendResponse(appSmsAuthProperties.getExpireSeconds());
    }
 
    /**
     * 统一校验指定场景的验证码并执行一次性销毁。
     */
    private void validateCode(String phone, String code, String cachePrefix) {
        if (matchesDebugCode(code)) {
            // 开发环境允许固定验证码直通,便于前后端联调。
            return;
        }
        String cacheKey = buildCacheKey(cachePrefix, phone);
        String cachedCode = stringRedisTemplate.opsForValue().get(cacheKey);
        if (cachedCode == null || cachedCode.trim().isEmpty()) {
            throw new IllegalArgumentException("验证码已失效,请重新获取");
        }
        if (!cachedCode.equals(code)) {
            throw new IllegalArgumentException("验证码错误");
        }
        // 验证码登录成功后立即删除,避免重复使用。
        stringRedisTemplate.delete(cacheKey);
    }
 
    /**
     * 生成 6 位数字验证码。
     */
    private String generateCode() {
        int code = ThreadLocalRandom.current().nextInt(100000, 1000000);
        return String.valueOf(code);
    }
 
    /**
     * 判断是否命中本地调试验证码。
     */
    private boolean matchesDebugCode(String code) {
        if (!appSmsAuthProperties.isDebugEnabled()) {
            return false;
        }
        String debugCode = appSmsAuthProperties.getDebugCode();
        return debugCode != null && !debugCode.trim().isEmpty() && debugCode.equals(code);
    }
 
    /**
     * 构造手机号维度的验证码缓存 key。
     */
    private String buildCacheKey(String cachePrefix, String phone) {
        return cachePrefix + phone;
    }
}