|
|
|
@ -1,7 +1,6 @@ |
|
|
|
package com.project.statistics.domain.service.impl; |
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil; |
|
|
|
import cn.hutool.core.collection.CollectionUtil; |
|
|
|
import cn.hutool.core.util.StrUtil; |
|
|
|
import com.project.exam.domain.entity.ExamRecordEntity; |
|
|
|
import com.project.question.domain.entity.QuestionEntity; |
|
|
|
@ -15,6 +14,7 @@ import com.project.task.domain.enums.ExamModeEnum; |
|
|
|
import com.project.task.domain.enums.QuestionTypeEnum; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate; |
|
|
|
import org.springframework.scheduling.annotation.Async; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
|
|
|
|
@ -22,6 +22,7 @@ import java.util.ArrayList; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Objects; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
|
|
|
|
@Service |
|
|
|
@Slf4j |
|
|
|
@ -36,6 +37,17 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
@Autowired |
|
|
|
private TaskKnowledgePointBaseService taskKnowledgePointBaseService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private StringRedisTemplate redisTemplate; |
|
|
|
|
|
|
|
private static final String LOCK_PREFIX = "lock:error_prone:"; |
|
|
|
// 锁自动过期时间5秒,防止死锁(拿到锁后程序崩溃没释放,5s自动失效)
|
|
|
|
private static final int LOCK_EXPIRE_SECONDS = 5; |
|
|
|
// 自旋最大重试次数:最多抢锁10次
|
|
|
|
private static final int LOCK_RETRY_TIMES = 10; |
|
|
|
// 每次抢锁失败后,休眠50ms再重试
|
|
|
|
private static final long LOCK_RETRY_INTERVAL_MS = 50; |
|
|
|
|
|
|
|
@Override |
|
|
|
@Async |
|
|
|
public void collectErrorProneStatistics(Long taskId, Integer examMode, List<ExamRecordEntity.QuestionSnapshot> snapshots) { |
|
|
|
@ -78,30 +90,47 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() |
|
|
|
.eq(ErrorProneStatisticsEntity::getTaskId, taskId) |
|
|
|
.eq(ErrorProneStatisticsEntity::getKpId, kp.getId()) |
|
|
|
.one(); |
|
|
|
|
|
|
|
if (entity == null) { |
|
|
|
entity = new ErrorProneStatisticsEntity(); |
|
|
|
entity.setTaskId(taskId); |
|
|
|
entity.setExamMode(ExamModeEnum.GENERATIVE.getValue()); |
|
|
|
entity.setContent(kp.getContent()); |
|
|
|
entity.setKpId(kp.getId()); |
|
|
|
entity.setErrorCount(0); |
|
|
|
entity.setAppearCount(0); |
|
|
|
String lockKey = LOCK_PREFIX + "gen:" + taskId + ":" + kp.getId(); |
|
|
|
boolean locked = tryLock(lockKey); |
|
|
|
if (!locked) { |
|
|
|
log.warn("[易错统计] 获取锁失败, lockKey={}, 题目ID={}", lockKey, questionId); |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
List<Long> questionIdList = CollectionUtil.isEmpty(entity.getQuestionIdList()) ? new ArrayList<>() : entity.getQuestionIdList(); |
|
|
|
questionIdList.add(questionId); |
|
|
|
entity.setQuestionIdList(questionIdList); |
|
|
|
entity.setAppearCount(entity.getAppearCount() + 1); |
|
|
|
if (Boolean.FALSE.equals(snapshot.getIsRight())) { |
|
|
|
entity.setErrorCount(entity.getErrorCount() + 1); |
|
|
|
} |
|
|
|
try { |
|
|
|
ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() |
|
|
|
.eq(ErrorProneStatisticsEntity::getTaskId, taskId) |
|
|
|
.eq(ErrorProneStatisticsEntity::getKpId, kp.getId()) |
|
|
|
.one(); |
|
|
|
|
|
|
|
errorProneStatisticsBaseService.saveOrUpdate(entity); |
|
|
|
if (entity == null) { |
|
|
|
entity = new ErrorProneStatisticsEntity(); |
|
|
|
entity.setTaskId(taskId); |
|
|
|
entity.setExamMode(ExamModeEnum.GENERATIVE.getValue()); |
|
|
|
entity.setContent(kp.getContent()); |
|
|
|
entity.setKpId(kp.getId()); |
|
|
|
entity.setErrorCount(0); |
|
|
|
entity.setAppearCount(0); |
|
|
|
entity.setQuestionIdList(new ArrayList<>()); |
|
|
|
} |
|
|
|
|
|
|
|
List<Long> questionIdList = entity.getQuestionIdList(); |
|
|
|
if (questionIdList == null) { |
|
|
|
questionIdList = new ArrayList<>(); |
|
|
|
} |
|
|
|
if (!questionIdList.contains(questionId)) { |
|
|
|
questionIdList.add(questionId); |
|
|
|
} |
|
|
|
entity.setQuestionIdList(questionIdList); |
|
|
|
entity.setAppearCount(entity.getAppearCount() + 1); |
|
|
|
if (Boolean.FALSE.equals(snapshot.getIsRight())) { |
|
|
|
entity.setErrorCount(entity.getErrorCount() + 1); |
|
|
|
} |
|
|
|
|
|
|
|
errorProneStatisticsBaseService.saveOrUpdate(entity); |
|
|
|
} finally { |
|
|
|
unlock(lockKey); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -121,32 +150,72 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() |
|
|
|
.eq(ErrorProneStatisticsEntity::getTaskId, taskId) |
|
|
|
.eq(ErrorProneStatisticsEntity::getExamMode, snapshot.getQuestionId()) |
|
|
|
.one(); |
|
|
|
|
|
|
|
if (entity == null) { |
|
|
|
entity = new ErrorProneStatisticsEntity(); |
|
|
|
entity.setTaskId(taskId); |
|
|
|
entity.setQuestionIdList(List.of(snapshot.getQuestionId())); |
|
|
|
entity.setExamMode(ExamModeEnum.CLASSIC.getValue()); |
|
|
|
entity.setContent(content); |
|
|
|
entity.setQuestionType(snapshot.getType()); |
|
|
|
entity.setErrorCount(0); |
|
|
|
entity.setAppearCount(0); |
|
|
|
entity.setOptionDetails(buildOptionDetail(snapshot)); |
|
|
|
String lockKey = LOCK_PREFIX + "classic:" + taskId + ":" + snapshot.getQuestionId(); |
|
|
|
boolean locked = tryLock(lockKey); |
|
|
|
if (!locked) { |
|
|
|
log.warn("[易错统计] 获取锁失败, lockKey={}, 题目ID={}", lockKey, snapshot.getQuestionId()); |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
entity.setAppearCount(entity.getAppearCount() + 1); |
|
|
|
if (Boolean.FALSE.equals(snapshot.getIsRight())) { |
|
|
|
entity.setErrorCount(entity.getErrorCount() + 1); |
|
|
|
} |
|
|
|
try { |
|
|
|
ErrorProneStatisticsEntity entity = errorProneStatisticsBaseService.lambdaQuery() |
|
|
|
.eq(ErrorProneStatisticsEntity::getTaskId, taskId) |
|
|
|
.eq(ErrorProneStatisticsEntity::getExamMode, ExamModeEnum.CLASSIC.getValue()) |
|
|
|
.apply("JSON_CONTAINS(question_id_list, CAST({0} AS JSON))", snapshot.getQuestionId()) |
|
|
|
.one(); |
|
|
|
|
|
|
|
updateOptionDetail(entity, snapshot.getUserAnswer()); |
|
|
|
if (entity == null) { |
|
|
|
entity = new ErrorProneStatisticsEntity(); |
|
|
|
entity.setTaskId(taskId); |
|
|
|
entity.setQuestionIdList(new ArrayList<>(List.of(snapshot.getQuestionId()))); |
|
|
|
entity.setExamMode(ExamModeEnum.CLASSIC.getValue()); |
|
|
|
entity.setContent(content); |
|
|
|
entity.setQuestionType(snapshot.getType()); |
|
|
|
entity.setErrorCount(0); |
|
|
|
entity.setAppearCount(0); |
|
|
|
entity.setOptionDetails(buildOptionDetail(snapshot)); |
|
|
|
} |
|
|
|
|
|
|
|
errorProneStatisticsBaseService.saveOrUpdate(entity); |
|
|
|
entity.setAppearCount(entity.getAppearCount() + 1); |
|
|
|
if (Boolean.FALSE.equals(snapshot.getIsRight())) { |
|
|
|
entity.setErrorCount(entity.getErrorCount() + 1); |
|
|
|
} |
|
|
|
|
|
|
|
updateOptionDetail(entity, snapshot.getUserAnswer()); |
|
|
|
|
|
|
|
errorProneStatisticsBaseService.saveOrUpdate(entity); |
|
|
|
} finally { |
|
|
|
unlock(lockKey); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 尝试获取 Redis 分布式锁(带自旋重试) |
|
|
|
*/ |
|
|
|
private boolean tryLock(String lockKey) { |
|
|
|
int retry = 0; |
|
|
|
while (retry < LOCK_RETRY_TIMES) { |
|
|
|
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS); |
|
|
|
if (Boolean.TRUE.equals(locked)) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
retry++; |
|
|
|
try { |
|
|
|
Thread.sleep(LOCK_RETRY_INTERVAL_MS); |
|
|
|
} catch (InterruptedException e) { |
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 释放 Redis 分布式锁 |
|
|
|
*/ |
|
|
|
private void unlock(String lockKey) { |
|
|
|
redisTemplate.delete(lockKey); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
|