|
|
|
@ -1,7 +1,9 @@ |
|
|
|
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 cn.hutool.json.JSONUtil; |
|
|
|
import com.project.exam.domain.entity.ExamRecordEntity; |
|
|
|
import com.project.question.domain.entity.QuestionEntity; |
|
|
|
import com.project.question.domain.entity.TaskKnowledgePointEntity; |
|
|
|
@ -10,8 +12,10 @@ import com.project.question.domain.service.TaskKnowledgePointBaseService; |
|
|
|
import com.project.statistics.domain.entity.ErrorProneStatisticsEntity; |
|
|
|
import com.project.statistics.domain.service.ErrorProneStatisticsBaseService; |
|
|
|
import com.project.statistics.domain.service.SaveErrorProneStatisticsDomainService; |
|
|
|
import com.project.task.domain.entity.TaskEntity; |
|
|
|
import com.project.task.domain.enums.ExamModeEnum; |
|
|
|
import com.project.task.domain.enums.QuestionTypeEnum; |
|
|
|
import com.project.task.domain.service.TaskBaseService; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate; |
|
|
|
@ -23,6 +27,7 @@ import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Objects; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
@Service |
|
|
|
@Slf4j |
|
|
|
@ -37,14 +42,15 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
@Autowired |
|
|
|
private TaskKnowledgePointBaseService taskKnowledgePointBaseService; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private TaskBaseService taskBaseService; |
|
|
|
|
|
|
|
@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; |
|
|
|
|
|
|
|
@ -55,11 +61,18 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
TaskEntity task = taskBaseService.getById(taskId); |
|
|
|
if (task == null){ |
|
|
|
throw new IllegalArgumentException("考试任务不存在,taskId: " + taskId); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
Double shortAnswerScore = task.getShortAnswerScore(); |
|
|
|
|
|
|
|
if (ExamModeEnum.GENERATIVE.getValue().equals(examMode)) { |
|
|
|
collectGenerativeStatistics(taskId, snapshots); |
|
|
|
collectGenerativeStatistics(taskId, shortAnswerScore, snapshots); |
|
|
|
} else if (ExamModeEnum.CLASSIC.getValue().equals(examMode)) { |
|
|
|
collectClassicStatistics(taskId, snapshots); |
|
|
|
collectClassicStatistics(taskId, shortAnswerScore, snapshots); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("[易错统计] 收集失败, taskId={}", taskId, e); |
|
|
|
@ -69,12 +82,8 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
/** |
|
|
|
* 生成式套题:以知识点为单位统计 |
|
|
|
*/ |
|
|
|
private void collectGenerativeStatistics(Long taskId, List<ExamRecordEntity.QuestionSnapshot> snapshots) { |
|
|
|
private void collectGenerativeStatistics(Long taskId, Double shortAnswerScore, List<ExamRecordEntity.QuestionSnapshot> snapshots) { |
|
|
|
for (ExamRecordEntity.QuestionSnapshot snapshot : snapshots) { |
|
|
|
// 跳过简答题
|
|
|
|
if (isShortAnswer(snapshot.getType())) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
Long questionId = snapshot.getQuestionId(); |
|
|
|
if (questionId == null) { |
|
|
|
continue; |
|
|
|
@ -123,7 +132,7 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
} |
|
|
|
entity.setQuestionIdList(questionIdList); |
|
|
|
entity.setAppearCount(entity.getAppearCount() + 1); |
|
|
|
if (Boolean.FALSE.equals(snapshot.getIsRight())) { |
|
|
|
if (isErrorQuestion(snapshot, shortAnswerScore)) { |
|
|
|
entity.setErrorCount(entity.getErrorCount() + 1); |
|
|
|
} |
|
|
|
|
|
|
|
@ -138,18 +147,15 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
/** |
|
|
|
* 经典套题:以题目为单位统计,并统计选项选择情况 |
|
|
|
*/ |
|
|
|
private void collectClassicStatistics(Long taskId, List<ExamRecordEntity.QuestionSnapshot> snapshots) { |
|
|
|
private void collectClassicStatistics(Long taskId, Double shortAnswerScore, List<ExamRecordEntity.QuestionSnapshot> snapshots) { |
|
|
|
for (ExamRecordEntity.QuestionSnapshot snapshot : snapshots) { |
|
|
|
// 跳过简答题
|
|
|
|
if (isShortAnswer(snapshot.getType())) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
String content = snapshot.getQuestionContent(); |
|
|
|
if (StrUtil.isBlank(content)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
QuestionEntity question = questionBaseService.getById(snapshot.getQuestionId()); |
|
|
|
|
|
|
|
String lockKey = LOCK_PREFIX + "classic:" + taskId + ":" + snapshot.getQuestionId(); |
|
|
|
boolean locked = tryLock(lockKey); |
|
|
|
if (!locked) { |
|
|
|
@ -173,15 +179,22 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
entity.setQuestionType(snapshot.getType()); |
|
|
|
entity.setErrorCount(0); |
|
|
|
entity.setAppearCount(0); |
|
|
|
entity.setOptionDetails(buildOptionDetail(snapshot)); |
|
|
|
//构建题目详细信息
|
|
|
|
List<ErrorProneStatisticsEntity.OptionDetail> optionDetails; |
|
|
|
if (isShortAnswer(snapshot.getType())) { |
|
|
|
optionDetails = buildOptionDetail(question.getQuestionDetail()); |
|
|
|
} else { |
|
|
|
optionDetails = buildOptionDetail(snapshot); |
|
|
|
} |
|
|
|
entity.setOptionDetails(optionDetails); |
|
|
|
} |
|
|
|
|
|
|
|
entity.setAppearCount(entity.getAppearCount() + 1); |
|
|
|
if (Boolean.FALSE.equals(snapshot.getIsRight())) { |
|
|
|
if (isErrorQuestion(snapshot, shortAnswerScore)) { |
|
|
|
entity.setErrorCount(entity.getErrorCount() + 1); |
|
|
|
} |
|
|
|
|
|
|
|
updateOptionDetail(entity, snapshot.getUserAnswer()); |
|
|
|
updateOptionDetail(entity, snapshot); |
|
|
|
|
|
|
|
errorProneStatisticsBaseService.saveOrUpdate(entity); |
|
|
|
} finally { |
|
|
|
@ -194,13 +207,11 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
* 尝试获取 Redis 分布式锁(带自旋重试) |
|
|
|
*/ |
|
|
|
private boolean tryLock(String lockKey) { |
|
|
|
int retry = 0; |
|
|
|
while (retry < LOCK_RETRY_TIMES) { |
|
|
|
while (true) { |
|
|
|
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) { |
|
|
|
@ -208,7 +219,6 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -219,7 +229,27 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 构建选项详情 |
|
|
|
* 构建简答题得分点详情 |
|
|
|
*/ |
|
|
|
private List<ErrorProneStatisticsEntity.OptionDetail> buildOptionDetail(QuestionEntity.QuestionDetail questionDetail) { |
|
|
|
List<ErrorProneStatisticsEntity.OptionDetail> list = new ArrayList<>(); |
|
|
|
List<QuestionEntity.ScoringPoint> scoringPoints = questionDetail.getScoringPoints(); |
|
|
|
if (CollectionUtil.isEmpty(scoringPoints)) { |
|
|
|
return list; |
|
|
|
} |
|
|
|
|
|
|
|
for (QuestionEntity.ScoringPoint scoringPoint : scoringPoints) { |
|
|
|
ErrorProneStatisticsEntity.OptionDetail od = new ErrorProneStatisticsEntity.OptionDetail(); |
|
|
|
od.setOption(scoringPoint.getIndex().toString()); |
|
|
|
od.setOptionContent(scoringPoint.getContent()); |
|
|
|
od.setChoosenCount(0); |
|
|
|
list.add(od); |
|
|
|
} |
|
|
|
return list; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 构建客观题选项详情 |
|
|
|
*/ |
|
|
|
private List<ErrorProneStatisticsEntity.OptionDetail> buildOptionDetail(ExamRecordEntity.QuestionSnapshot snapshot) { |
|
|
|
List<ErrorProneStatisticsEntity.OptionDetail> list = new ArrayList<>(); |
|
|
|
@ -244,8 +274,14 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
/** |
|
|
|
* 更新选项被选择次数 |
|
|
|
*/ |
|
|
|
private void updateOptionDetail(ErrorProneStatisticsEntity entity, String userAnswer) { |
|
|
|
String normalizedUserAnswer = normalizeAnswer(userAnswer); |
|
|
|
private void updateOptionDetail(ErrorProneStatisticsEntity entity, ExamRecordEntity.QuestionSnapshot snapshot) { |
|
|
|
String normalizedUserAnswer; |
|
|
|
if(isShortAnswer(snapshot.getType())){ |
|
|
|
List<Integer> hitPoints = snapshot.getHitPoints(); |
|
|
|
normalizedUserAnswer = CollectionUtil.isEmpty(hitPoints) ? "" : hitPoints.stream().map(String::valueOf).collect(Collectors.joining(",")); |
|
|
|
}else{ |
|
|
|
normalizedUserAnswer = normalizeAnswer(snapshot.getUserAnswer()); |
|
|
|
} |
|
|
|
if (StrUtil.isBlank(normalizedUserAnswer)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
@ -278,4 +314,15 @@ public class SaveSaveErrorProneStatisticsDomainServiceImpl implements SaveErrorP |
|
|
|
private boolean isShortAnswer(Integer type) { |
|
|
|
return Objects.equals(type, QuestionTypeEnum.SHORT_ANSWER.getValue()); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 判断是否为错题:客观题按 isRight,简答题按 aiScore 是否得满 |
|
|
|
*/ |
|
|
|
private boolean isErrorQuestion(ExamRecordEntity.QuestionSnapshot snapshot, Double shortAnswerScore) { |
|
|
|
if (isShortAnswer(snapshot.getType())) { |
|
|
|
Double aiScore = snapshot.getAiScore(); |
|
|
|
return aiScore == null || aiScore < shortAnswerScore; |
|
|
|
} |
|
|
|
return Boolean.FALSE.equals(snapshot.getIsRight()); |
|
|
|
} |
|
|
|
} |
|
|
|
|