|
|
@@ -1,127 +1,173 @@
|
|
|
-package com.etotem.cfc.service;
|
|
|
-
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
-import com.etotem.cfc.entity.VerificationCode;
|
|
|
-import com.etotem.cfc.mapper.VerificationCodeMapper;
|
|
|
-import com.etotem.cfc.service.api.VerificationCodeServiceInterface;
|
|
|
-import lombok.extern.slf4j.Slf4j;
|
|
|
-import javax.annotation.Resource;
|
|
|
-import org.springframework.beans.factory.annotation.Value;
|
|
|
-import org.springframework.stereotype.Service;
|
|
|
-
|
|
|
-import java.util.Date;
|
|
|
-import java.security.SecureRandom;
|
|
|
-
|
|
|
-@Slf4j
|
|
|
-@Service
|
|
|
-public class VerificationCodeService implements VerificationCodeServiceInterface {
|
|
|
-
|
|
|
- @Resource
|
|
|
- private VerificationCodeMapper verificationCodeMapper;
|
|
|
-
|
|
|
- @Value("${wechat.test-mode:false}")
|
|
|
- private boolean testMode;
|
|
|
-
|
|
|
- private static final int CODE_LENGTH = 6;
|
|
|
- private static final int VALID_MINUTES = 5;
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成验证码
|
|
|
- */
|
|
|
- public String generateCode(String phone, String type) {
|
|
|
- // 生成6位随机数字
|
|
|
- String code = generateRandomCode();
|
|
|
-
|
|
|
- // 先删除该手机号之前的验证码
|
|
|
- verificationCodeMapper.delete(new LambdaQueryWrapper<VerificationCode>()
|
|
|
- .eq(VerificationCode::getPhone, phone)
|
|
|
- .eq(VerificationCode::getType, type));
|
|
|
-
|
|
|
- // 创建新验证码
|
|
|
- VerificationCode verificationCode = new VerificationCode();
|
|
|
- verificationCode.setPhone(phone);
|
|
|
- verificationCode.setCode(code);
|
|
|
- verificationCode.setType(type);
|
|
|
- verificationCode.setExpiresIn(VALID_MINUTES * 60);
|
|
|
- verificationCode.setCreatedAt(new Date());
|
|
|
- verificationCode.setExpiresAt(new Date(System.currentTimeMillis() + VALID_MINUTES * 60 * 1000));
|
|
|
- verificationCode.setUsed(0);
|
|
|
- verificationCodeMapper.insert(verificationCode);
|
|
|
-
|
|
|
- if (testMode) {
|
|
|
- log.info("Test mode: code = {}", code);
|
|
|
- } else {
|
|
|
- throw new UnsupportedOperationException("SMS provider not configured - cannot send SMS to " + phone);
|
|
|
- }
|
|
|
-
|
|
|
- return code;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 验证验证码
|
|
|
- */
|
|
|
- public boolean verifyCode(String phone, String code, String type) {
|
|
|
- // 测试模式下接受固定验证码
|
|
|
- if (testMode && "123456".equals(code)) {
|
|
|
- log.info("测试模式:跳过验证码校验 phone={}", phone);
|
|
|
- return true;
|
|
|
- }
|
|
|
- VerificationCode verificationCode = verificationCodeMapper.selectOne(
|
|
|
- new LambdaQueryWrapper<VerificationCode>()
|
|
|
- .eq(VerificationCode::getPhone, phone)
|
|
|
- .eq(VerificationCode::getCode, code)
|
|
|
- .eq(VerificationCode::getType, type)
|
|
|
- .eq(VerificationCode::getUsed, 0)
|
|
|
- );
|
|
|
-
|
|
|
- if (verificationCode == null) {
|
|
|
- log.warn("验证码不存在或已使用: phone={}, code={}", phone, code);
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // 检查是否过期
|
|
|
- if (verificationCode.getExpiresAt().before(new Date())) {
|
|
|
- log.warn("验证码已过期: phone={}", phone);
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // 标记为已使用
|
|
|
- verificationCode.setUsed(1);
|
|
|
- verificationCodeMapper.updateById(verificationCode);
|
|
|
-
|
|
|
- log.info("验证码验证成功: phone={}", phone);
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查验证码是否有效(不标记为已使用)
|
|
|
- */
|
|
|
- public boolean checkCodeValid(String phone, String code, String type) {
|
|
|
- VerificationCode verificationCode = verificationCodeMapper.selectOne(
|
|
|
- new LambdaQueryWrapper<VerificationCode>()
|
|
|
- .eq(VerificationCode::getPhone, phone)
|
|
|
- .eq(VerificationCode::getCode, code)
|
|
|
- .eq(VerificationCode::getType, type)
|
|
|
- .eq(VerificationCode::getUsed, 0)
|
|
|
- );
|
|
|
-
|
|
|
- if (verificationCode == null) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // 检查是否过期
|
|
|
- return !verificationCode.getExpiresAt().before(new Date());
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成随机6位数字验证码
|
|
|
- */
|
|
|
- private String generateRandomCode() {
|
|
|
- SecureRandom random = new SecureRandom();
|
|
|
- StringBuilder code = new StringBuilder();
|
|
|
- for (int i = 0; i < CODE_LENGTH; i++) {
|
|
|
- code.append(random.nextInt(10));
|
|
|
- }
|
|
|
- return code.toString();
|
|
|
- }
|
|
|
-}
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.entity.VerificationCode;
|
|
|
+import com.etotem.cfc.mapper.VerificationCodeMapper;
|
|
|
+import com.etotem.cfc.service.api.VerificationCodeServiceInterface;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import javax.annotation.Resource;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.util.Date;
|
|
|
+import java.security.SecureRandom;
|
|
|
+import java.time.Duration;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class VerificationCodeService implements VerificationCodeServiceInterface {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private VerificationCodeMapper verificationCodeMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private RedisTemplate<String, Object> redisTemplate;
|
|
|
+
|
|
|
+ @Value("${wechat.test-mode:false}")
|
|
|
+ private boolean testMode;
|
|
|
+
|
|
|
+ private static final int CODE_LENGTH = 6;
|
|
|
+ private static final int VALID_MINUTES = 5;
|
|
|
+
|
|
|
+ /** Redis 验证码 key 前缀 */
|
|
|
+ private static final String VC_KEY_PREFIX = "vc:";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成验证码
|
|
|
+ */
|
|
|
+ public String generateCode(String phone, String type) {
|
|
|
+ // 生成6位随机数字
|
|
|
+ String code = generateRandomCode();
|
|
|
+
|
|
|
+ // 优先写入 Redis(自动覆盖旧码,TTL 5 分钟)
|
|
|
+ try {
|
|
|
+ redisTemplate.opsForValue().set(vcKey(phone, type), code,
|
|
|
+ Duration.ofMinutes(VALID_MINUTES));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("Redis 写入验证码失败,回退数据库: {}", e.getMessage());
|
|
|
+ // 先删除该手机号之前的验证码
|
|
|
+ verificationCodeMapper.delete(new LambdaQueryWrapper<VerificationCode>()
|
|
|
+ .eq(VerificationCode::getPhone, phone)
|
|
|
+ .eq(VerificationCode::getType, type));
|
|
|
+
|
|
|
+ // 创建新验证码
|
|
|
+ VerificationCode verificationCode = new VerificationCode();
|
|
|
+ verificationCode.setPhone(phone);
|
|
|
+ verificationCode.setCode(code);
|
|
|
+ verificationCode.setType(type);
|
|
|
+ verificationCode.setExpiresIn(VALID_MINUTES * 60);
|
|
|
+ verificationCode.setCreatedAt(new Date());
|
|
|
+ verificationCode.setExpiresAt(new Date(System.currentTimeMillis() + VALID_MINUTES * 60 * 1000));
|
|
|
+ verificationCode.setUsed(0);
|
|
|
+ verificationCodeMapper.insert(verificationCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (testMode) {
|
|
|
+ log.info("Test mode: code = {}", code);
|
|
|
+ } else {
|
|
|
+ throw new UnsupportedOperationException("SMS provider not configured - cannot send SMS to " + phone);
|
|
|
+ }
|
|
|
+
|
|
|
+ return code;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证验证码
|
|
|
+ */
|
|
|
+ public boolean verifyCode(String phone, String code, String type) {
|
|
|
+ // 测试模式下接受固定验证码
|
|
|
+ if (testMode && "123456".equals(code)) {
|
|
|
+ log.info("测试模式:跳过验证码校验 phone={}", phone);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 优先走 Redis:比对成功即删除(一次性),失败/不存在返回 false
|
|
|
+ try {
|
|
|
+ Object cached = redisTemplate.opsForValue().get(vcKey(phone, type));
|
|
|
+ if (cached == null) {
|
|
|
+ log.warn("验证码不存在或已使用: phone={}, code={}", phone, code);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!code.equals(cached.toString())) {
|
|
|
+ log.warn("验证码错误: phone={}", phone);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 匹配成功,删除(一次性使用)
|
|
|
+ redisTemplate.delete(vcKey(phone, type));
|
|
|
+ log.info("验证码验证成功: phone={}", phone);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("Redis 验证码校验失败,回退数据库: {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ VerificationCode verificationCode = verificationCodeMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<VerificationCode>()
|
|
|
+ .eq(VerificationCode::getPhone, phone)
|
|
|
+ .eq(VerificationCode::getCode, code)
|
|
|
+ .eq(VerificationCode::getType, type)
|
|
|
+ .eq(VerificationCode::getUsed, 0)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (verificationCode == null) {
|
|
|
+ log.warn("验证码不存在或已使用: phone={}, code={}", phone, code);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否过期
|
|
|
+ if (verificationCode.getExpiresAt().before(new Date())) {
|
|
|
+ log.warn("验证码已过期: phone={}", phone);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标记为已使用
|
|
|
+ verificationCode.setUsed(1);
|
|
|
+ verificationCodeMapper.updateById(verificationCode);
|
|
|
+
|
|
|
+ log.info("验证码验证成功: phone={}", phone);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查验证码是否有效(不标记为已使用)
|
|
|
+ */
|
|
|
+ public boolean checkCodeValid(String phone, String code, String type) {
|
|
|
+ // 优先走 Redis:只读比对,不删除
|
|
|
+ try {
|
|
|
+ Object cached = redisTemplate.opsForValue().get(vcKey(phone, type));
|
|
|
+ return cached != null && code.equals(cached.toString());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("Redis 验证码检查失败,回退数据库: {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ VerificationCode verificationCode = verificationCodeMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<VerificationCode>()
|
|
|
+ .eq(VerificationCode::getPhone, phone)
|
|
|
+ .eq(VerificationCode::getCode, code)
|
|
|
+ .eq(VerificationCode::getType, type)
|
|
|
+ .eq(VerificationCode::getUsed, 0)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (verificationCode == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否过期
|
|
|
+ return !verificationCode.getExpiresAt().before(new Date());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String vcKey(String phone, String type) {
|
|
|
+ return VC_KEY_PREFIX + phone + ":" + type;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成随机6位数字验证码
|
|
|
+ */
|
|
|
+ private String generateRandomCode() {
|
|
|
+ SecureRandom random = new SecureRandom();
|
|
|
+ StringBuilder code = new StringBuilder();
|
|
|
+ for (int i = 0; i < CODE_LENGTH; i++) {
|
|
|
+ code.append(random.nextInt(10));
|
|
|
+ }
|
|
|
+ return code.toString();
|
|
|
+ }
|
|
|
+}
|