Jelajahi Sumber

feat(backend): 验证码改用 Redis 存储(TTL 5分钟,一次性),Redis 异常时回退数据库

Xiaogang Liao 6 hari lalu
induk
melakukan
bdfb0add15

+ 173 - 127
cfc-backend/src/main/java/com/etotem/cfc/service/VerificationCodeService.java

@@ -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();
+    }
+}