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; } }