阅卷任务
阅卷流程:
- 客观题试卷流程:考试结束时间到->自动结束考试->统计总分、排名等。
- 主观题试卷流程:考试结束时间到->自动结束考试->等待人工阅卷->考试阅卷结束时间到->自动结束阅卷->统计总分、排名等。
程序启动时,会激活ExamCoreRunner的自动轮询任务,其中两项功能是:
- 自动检索已提交但尚未批阅的试卷,并触发相应的试卷批阅流程。
- 实时监控每场考试的结束时间,一旦到达预设时间,则自动结束考试,并随后执行成绩排名在内的多项任务。
以下内容为实现阅卷功能的核心代码片段,更多具体细节请查阅实际代码库。
查找待阅试卷,自动批阅
- 考试用户交卷后,自动清理待阅试卷列表缓存。
- 自动轮询任务重新加载到最新待阅试卷列表,通过线程池批量完成批阅试卷的客观题部分。
java
@Caching(evict = { //
@CacheEvict(value = ExamConstant.MYEXAM_CACHE, key = ExamConstant.MYEXAM_KEY_PRE
+ "#examId + ':' + #userId"),
@CacheEvict(value = ExamConstant.MYEXAM_CACHE, key = ExamConstant.MYEXAM_LIST_KEY_PRE + "#examId"), //
@CacheEvict(value = ExamConstant.MYEXAM_CACHE, key = ExamConstant.MYEXAM_UNMARK_LIST_KEY), //
})
public void finish(Integer examId, Integer userId) {
/**
* 交卷
*
* 正常在页面自动交卷,会有网络延时,更新交卷时间时,如果它超出正常范围时修正一下,保证计算答题分钟等结果正常。 <br/>
* 如:限时2分钟考试,2024-09-03 至 13:00:11 2024-09-03 13:02:12,页面计算答题时长为2分1秒 <br/>
* 交卷时间需修正为2024-09-03 13:02:11
*/
MyExam myExam = examCacheService.getMyExam(examId, userId);
Date curTime = new Date();
if (myExam.getAnswerEndTime().getTime() > curTime.getTime()) {
myExam.setAnswerEndTime(curTime);
updateById(myExam);
}
}
java
while (true) {
TimeUnit.SECONDS.sleep(1);
List<Callable<Boolean>> taskList = examCacheService.getUnMarkList().stream()//
.map(myExam -> new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
myPaperService.doMark(myExam.getExamId(), myExam.getUserId());
return null;
}
}).collect(Collectors.toList());
EXECUTOR_SERVICE.invokeAll(taskList);
}
客观题自动批阅
填空题和问答题可以被视为客观题型,其运作机制在于通过关键词匹配来评分,每个关键词对应一定的分数,并且允许一个关键词存在多个同义词作为匹配项。
java
public static void fillBlankHandle(Question question, List<QuestionAnswer> questionAnswerList, MyQuestion myQuestion) {
/**
* 阅题
*
* 涉密人员上岗前要经过_______和_______。 关键词一:保密审查 保密调查 关键词二:培训 岗前培训 用户答案:培训 审查
* 匹配结果:【培训】得分;【审查】不得分
*/
myQuestion.setUserScore(BigDecimal.ZERO);// 先初始化用户分数为0,防止多次累加
boolean caseSensitive = !myQuestion.getMarkOptions().contains(3);// 区分大小写
boolean answerOrder = !myQuestion.getMarkOptions().contains(2);// 答案有顺序
String[] userAnswers = caseSensitive ? myQuestion.getUserAnswer().split("\n")// 获取用户答案(多空就是多个答案)
: myQuestion.getUserAnswer().toLowerCase().split("\n");
Set<Integer> useAnswers = new HashSet<>();// bug:客观多空填空题、答案无顺序-》一个正确答案分别填到三个空上-》当前题满分。所以标记一下,使用过的试题关键词就不在二次对比答案
for (int i = 0; i < userAnswers.length; i++) {// 循环用户每一项答案([培训 审查])
for (int j = 0; j < questionAnswerList.size(); j++) {// 循环试题答案关键词([[保密审查,保密调查], [培训 审查]])
QuestionAnswer questionAnswer = questionAnswerList.get(j);
String[] synonyms = caseSensitive ? questionAnswer.getAnswer().split("\n") // 获取试题答案关键词的所有同义词
: questionAnswer.getAnswer().toLowerCase().split("\n");
if (answerOrder) {// 如果答案有顺序
if (i != j) {// 则用户第1个答案和试题第1个答案对比,第二个和第二个对比,以此类推
continue;
}
}
if (useAnswers.contains(j)) {// 该关键词使用过,不在对比
continue;
}
for (String synonym : synonyms) {// 循环每一项同义词(保密审查 保密调查)
if (userAnswers[i].contains(synonym)) {// 如果用户某一空答案,匹配某一项关键词的同义词
myQuestion.setUserScore(BigDecimalUtil.newInstance(myQuestion.getUserScore())
.add(myQuestion.getScores().get(j)).getResult());// 累计该关键词的分数
useAnswers.add(j);
break;// 匹配到一个同义词就结束;继续对比下一个用户答案。(这里的循环不能退出上层循环,下面还有一个break去处理)
}
}
if (useAnswers.contains(j)) {
break;// 处理下一个用户答案(作用于上一段for循环,用户答案匹配某个标准答案,就不在循环其他标准答案)
}
}
}
}
主观题人工批阅
主观题由管理员、子管理员或者具备阅卷权限的阅卷用户进行批阅。
java
@CacheEvict(value = ExamConstant.MYQUESTION_CACHE, key = ExamConstant.MYQUESTION_LIST_KEY_PRE
+ "#examId + ':' + #userId")
public void score(Integer examId, Integer userId, Integer questionId, BigDecimal userScore) {
myQuestion.setUserScore(userScore);
myQuestion.setMarkTime(new Date());
myQuestion.setMarkUserId(getCurUser().getId());
myQuestionService.updateById(myQuestion);
}
查找已结束的考试,进行收尾
- 考试结束时间到,自动结束考试。
- 完成统计总分、排名等后续任务。
java
while (true) {
TimeUnit.SECONDS.sleep(1);
Long curTime = System.currentTimeMillis();
List<Callable<Boolean>> taskList = examCacheService.getExamingList().stream()//
.filter(exam -> (exam.getMarkState() == 1 && exam.getEndTime().getTime() <= curTime)
|| (exam.getMarkState() == 2 && exam.getMarkEndTime().getTime() <= curTime))//
.map(exam -> new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
myPaperService.doExam(exam.getId());
return null;
}
}).collect(Collectors.toList());
EXECUTOR_SERVICE.invokeAll(taskList);
}