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 SCENE_LOGIN = "login"; private static final String SCENE_REGISTER = "register"; private static final String SCENE_PASSWORD_RESET = "password-reset"; private static final String SCENE_PHONE_CHANGE = "phone-change"; 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 static final String PHONE_CHANGE_CODE_CACHE_PREFIX = "app:sms:phone-change:"; 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 AppSmsCodeSendResponse createPhoneChangeCode(String phone) { return createCode(phone, PHONE_CHANGE_CODE_CACHE_PREFIX, "phone-change"); } /** * 校验登录验证码,缓存有效期内允许重复提交。 */ public void validateLoginCode(String phone, String code) { validateCode(phone, code, LOGIN_CODE_CACHE_PREFIX, SCENE_LOGIN); } /** * 校验注册验证码,缓存有效期内允许重复提交。 */ public void validateRegisterCode(String phone, String code) { validateCode(phone, code, REGISTER_CODE_CACHE_PREFIX, SCENE_REGISTER); } /** * 校验忘记密码验证码,缓存有效期内允许重复提交。 */ public void validatePasswordResetCode(String phone, String code) { validateCode(phone, code, PASSWORD_RESET_CODE_CACHE_PREFIX, SCENE_PASSWORD_RESET); } /** * 校验修改手机号验证码,缓存有效期内允许重复提交。 */ public void validatePhoneChangeCode(String phone, String code) { validateCode(phone, code, PHONE_CHANGE_CODE_CACHE_PREFIX, SCENE_PHONE_CHANGE); } /** * 统一生成指定场景的验证码并写入缓存。 */ private AppSmsCodeSendResponse createCode(String phone, String cachePrefix, String scene) { String code = generateCode(); if (SCENE_LOGIN.equals(scene)) { appSmsSender.sendLoginCode(phone, code); } else if (SCENE_REGISTER.equals(scene)) { appSmsSender.sendRegisterCode(phone, code); } else if (SCENE_PASSWORD_RESET.equals(scene)) { appSmsSender.sendPasswordResetCode(phone, code); } else { appSmsSender.sendPhoneChangeCode(phone, code); } stringRedisTemplate.opsForValue().set( buildCacheKey(cachePrefix, phone), code, appSmsAuthProperties.getExpireSeconds(), TimeUnit.SECONDS ); return new AppSmsCodeSendResponse(appSmsAuthProperties.getExpireSeconds()); } /** * 统一校验指定场景的验证码,验证码仅按缓存 TTL 自然失效。 */ private void validateCode(String phone, String code, String cachePrefix, String scene) { if (matchesDebugCode(scene, 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("验证码错误"); } } /** * 生成 6 位数字验证码。 */ private String generateCode() { int code = ThreadLocalRandom.current().nextInt(100000, 1000000); return String.valueOf(code); } /** * 判断当前场景是否命中调试验证码。 */ private boolean matchesDebugCode(String scene, String code) { AppSmsAuthProperties.DebugScene debugScene = resolveDebugScene(scene); if (debugScene == null || !debugScene.isEnabled()) { return false; } String debugCode = debugScene.getCode(); return debugCode != null && !debugCode.trim().isEmpty() && debugCode.equals(code); } /** * 读取指定场景的调试验证码配置。 */ private AppSmsAuthProperties.DebugScene resolveDebugScene(String scene) { if (SCENE_LOGIN.equals(scene)) { return appSmsAuthProperties.getLogin(); } if (SCENE_REGISTER.equals(scene)) { return appSmsAuthProperties.getRegister(); } if (SCENE_PASSWORD_RESET.equals(scene)) { return appSmsAuthProperties.getPasswordReset(); } if (SCENE_PHONE_CHANGE.equals(scene)) { return appSmsAuthProperties.getPhoneChange(); } return null; } /** * 构造手机号维度的验证码缓存 key。 */ private String buildCacheKey(String cachePrefix, String phone) { return cachePrefix + phone; } }