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