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(400, "验证码错误或已过期"); } 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 + ":"; } }