“mitao”
2026-04-07 4ba9020b7c67cc318448a8aeac741492b23b4189
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
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 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) {
        String code = generateCode();
        stringRedisTemplate.opsForValue().set(
                buildCacheKey(phone),
                code,
                appSmsAuthProperties.getExpireSeconds(),
                TimeUnit.SECONDS
        );
        appSmsSender.sendLoginCode(phone, code);
        return new AppSmsCodeSendResponse(appSmsAuthProperties.getExpireSeconds());
    }
 
    /**
     * 校验登录验证码,成功后销毁缓存中的一次性验证码。
     */
    public void validateLoginCode(String phone, String code) {
        if (matchesDebugCode(code)) {
            // 开发环境允许固定验证码直通,便于前后端联调。
            return;
        }
        String cacheKey = buildCacheKey(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 phone) {
        return LOGIN_CODE_CACHE_PREFIX + phone;
    }
}