Преглед на файлове

feat(course): 添加分布式锁防止重复答题和红包领取

- 在答题记录插入时增加分布式锁保护,避免并发重复提交
- 实现双重检查机制,防止同用户对同一视频重复答题
- 对红包领取操作添加分布式锁,防止短时间内重复点击导致重复发放- 增加Redisson客户端依赖和相关日志记录- 处理锁获取失败、中断异常及数据库唯一索引冲突情况
- 优化答题结果返回逻辑,区分成功和失败场景的响应数据
xw преди 1 ден
родител
ревизия
98759070ac

+ 115 - 12
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -19,11 +19,17 @@ import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsStorePaymentService;
 import com.fs.system.service.ISysConfigService;
 import lombok.Getter;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -37,6 +43,8 @@ import java.util.stream.Collectors;
 @Service
 public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankService
 {
+    private static final Logger logger = LoggerFactory.getLogger(FsCourseQuestionBankServiceImpl.class);
+    
     @Autowired
     private FsCourseQuestionBankMapper fsCourseQuestionBankMapper;
     @Autowired
@@ -57,6 +65,8 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
     private FsCourseWatchLogMapper courseWatchLogMapper;
     @Autowired
     private FsUserCourseCategoryMapper courseCategoryMapper;
+    @Autowired
+    private RedissonClient redissonClient;
 
     /**
      * 查询题库
@@ -231,12 +241,13 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
 
         if (thisRightCount == questions.size()) {
             logs.setIsRight(1);
-            courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
-            return R.ok("答题成功");
+            //  使用分布式锁保护答题记录插入
+            return insertAnswerLogWithLock(logs, param, "答题成功", null);
         } else {
             logs.setIsRight(0);
-            courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
-            return R.ok("答题失败").put("incorrectQuestions", incorrectQuestions).put("remain",remainCount);
+            //  使用分布式锁保护答题记录插入
+            return insertAnswerLogWithLock(logs, param, "答题失败", 
+                R.ok().put("incorrectQuestions", incorrectQuestions).put("remain", remainCount));
         }
     }
 
@@ -336,12 +347,13 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
 
         if (thisRightCount == param.getQuestions().size()) {
             logs.setIsRight(1);
-            courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
-            return R.ok("答题成功");
+            //  使用分布式锁保护答题记录插入
+            return insertAnswerLogWithLock(logs, param, "答题成功", null);
         } else {
             logs.setIsRight(0);
-            courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
-            return R.ok("答题失败").put("incorrectQuestions", incorrectQuestions).put("remain",remainCount);
+            //  使用分布式锁保护答题记录插入
+            return insertAnswerLogWithLock(logs, param, "答题失败",
+                R.ok().put("incorrectQuestions", incorrectQuestions).put("remain", remainCount));
         }
     }
 
@@ -451,12 +463,13 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
 
         if (thisRightCount == param.getQuestions().size()) {
             logs.setIsRight(1);
-            courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
-            return R.ok("答题成功");
+            //  使用分布式锁保护答题记录插入
+            return insertAnswerLogWithLock(logs, param, "答题成功", null);
         } else {
             logs.setIsRight(0);
-            courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
-            return R.ok("答题失败").put("incorrectQuestions", incorrectQuestions).put("remain",remainCount);
+            //  使用分布式锁保护答题记录插入
+            return insertAnswerLogWithLock(logs, param, "答题失败",
+                R.ok().put("incorrectQuestions", incorrectQuestions).put("remain", remainCount));
         }
     }
 
@@ -864,4 +877,94 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         String cleanString = inputString.replaceAll("[\\[\\]\"\\s]", "");
         return cleanString.split(",");
     }
+
+    /**
+     * 使用分布式锁插入答题记录(防止重复答题)
+     * @param logs 答题记录
+     * @param param 答题参数
+     * @param successMsg 成功消息
+     * @param extraData 额外数据(用于答题失败时返回错误题目)
+     * @return 结果
+     */
+    private R insertAnswerLogWithLock(FsCourseAnswerLogs logs, FsCourseQuestionAnswerUParam param, 
+                                       String successMsg, R extraData) {
+        // 构建分布式锁key(基于业务键)
+        String lockKey = String.format("answer:user:%s:video:%s:period:%s:right:%s",
+            param.getUserId(), param.getVideoId(), param.getPeriodId(), logs.getIsRight());
+        RLock lock = redissonClient.getLock(lockKey);
+        
+        try {
+            //  尝试获取锁(等待3秒,持有10秒)
+            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
+            if (!locked) {
+                logger.warn("【答题提交】获取锁失败,userId:{}, videoId:{}, periodId:{}",
+                    param.getUserId(), param.getVideoId(), param.getPeriodId());
+                return R.error("系统繁忙,请稍后重试!");
+            }
+            
+            //  双重检查:再次查询是否已经答过题
+            FsCourseAnswerLogs existQuery = new FsCourseAnswerLogs();
+            existQuery.setUserId(param.getUserId());
+            existQuery.setVideoId(param.getVideoId());
+            existQuery.setPeriodId(param.getPeriodId());
+            existQuery.setIsRight(logs.getIsRight());
+            List<FsCourseAnswerLogs> existLogs = 
+                courseAnswerLogsMapper.selectFsCourseAnswerLogsList(existQuery);
+            
+            if (existLogs != null && !existLogs.isEmpty()) {
+                logger.warn("【答题提交】用户已答过题,userId:{}, videoId:{}, periodId:{}, is_right:{}, 已存在{}条记录",
+                    param.getUserId(), param.getVideoId(), param.getPeriodId(), 
+                    logs.getIsRight(), existLogs.size());
+                
+                // 如果是答对了,返回成功;如果是答错了,也返回之前的结果
+                if (logs.getIsRight() == 1) {
+                    return R.ok("该课程已答题完成,不可重复答题");
+                } else {
+                    return R.ok(successMsg);
+                }
+            }
+            
+            //  插入答题记录
+            try {
+                courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
+                logger.info("【答题提交】答题记录插入成功,userId:{}, videoId:{}, is_right:{}",
+                    param.getUserId(), param.getVideoId(), logs.getIsRight());
+                
+                // 返回结果
+                if (extraData != null) {
+                    return R.ok(successMsg)
+                        .put("incorrectQuestions", extraData.get("incorrectQuestions"))
+                        .put("remain", extraData.get("remain"));
+                } else {
+                    return R.ok(successMsg);
+                }
+            } catch (DuplicateKeyException e) {
+                //  捕获唯一索引冲突异常(数据库最后一道防线)
+                logger.warn("【答题提交】唯一索引冲突,用户已答过题,userId:{}, videoId:{}, periodId:{}",
+                    param.getUserId(), param.getVideoId(), param.getPeriodId());
+                
+                if (logs.getIsRight() == 1) {
+                    return R.error("该课程已答题完成,不可重复答题");
+                } else {
+                    return R.ok(successMsg);
+                }
+            }
+            
+        } catch (InterruptedException e) {
+            logger.error("【答题提交】获取锁被中断", e);
+            Thread.currentThread().interrupt();
+            return R.error("系统繁忙,请稍后重试!");
+        } catch (Exception e) {
+            logger.error("【答题提交】插入答题记录异常,userId:{}, videoId:{}",
+                param.getUserId(), param.getVideoId(), e);
+            return R.error("答题提交失败,请联系客服");
+        } finally {
+            //  释放锁
+            if (lock != null && lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                logger.info("【答题提交】释放锁成功,userId:{}, videoId:{}",
+                    param.getUserId(), param.getVideoId());
+            }
+        }
+    }
 }

+ 43 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -1349,6 +1349,32 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     private R sendRedPacketRewardToUser(FsCourseSendRewardUParam param, FsCourseWatchLog log, CourseConfig config, WxSendRedPacketParam packetParam, BigDecimal amount) {
 
+        //  添加分布式锁,防止同一秒内重复点击
+        String lockKey = String.format("redPacket:user:%s:video:%s:period:%s", 
+            param.getUserId(), param.getVideoId(), param.getPeriodId());
+        RLock lock = redissonClient.getLock(lockKey);
+        
+        try {
+            // 尝试获取锁(等待3秒,持有10秒)
+            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
+            if (!locked) {
+                logger.warn("【红包领取】获取锁失败,userId:{}, videoId:{}, periodId:{}", 
+                    param.getUserId(), param.getVideoId(), param.getPeriodId());
+                return R.error("系统繁忙,请稍后重试!");
+            }
+            
+            //  双重检查:再次查询是否已经领取过
+            FsCourseRedPacketLog existQuery = new FsCourseRedPacketLog();
+            existQuery.setUserId(param.getUserId());
+            existQuery.setVideoId(param.getVideoId());
+            existQuery.setPeriodId(param.getPeriodId());
+            List<FsCourseRedPacketLog> existLogs = redPacketLogMapper.selectFsCourseRedPacketLogList(existQuery);
+            
+            if (existLogs != null && !existLogs.isEmpty()) {
+                logger.warn("【红包领取】用户已领取过红包,userId:{}, videoId:{}, periodId:{}, 已存在{}条记录",
+                    param.getUserId(), param.getVideoId(), param.getPeriodId(), existLogs.size());
+                return R.error("已领取该课程奖励,不可重复领取!");
+            }
 
         // 发送红包
         R sendRedPacket = paymentService.sendRedPacket(packetParam);
@@ -1386,6 +1412,23 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         } else {
             return R.error("奖励发送失败,请联系客服");
         }
+        
+        } catch (InterruptedException e) {
+            logger.error("【红包领取】获取锁被中断", e);
+            Thread.currentThread().interrupt();
+            return R.error("系统繁忙,请稍后重试!");
+        } catch (Exception e) {
+            logger.error("【红包领取】发放红包异常,userId:{}, videoId:{}", 
+                param.getUserId(), param.getVideoId(), e);
+            return R.error("红包发放失败,请联系客服");
+        } finally {
+            // 释放锁
+            if (lock != null && lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                logger.info("【红包领取】释放锁成功,userId:{}, videoId:{}", 
+                    param.getUserId(), param.getVideoId());
+            }
+        }
     }