Skip to content

阅卷任务

阅卷流程:

  • 客观题试卷流程:考试结束时间到->自动结束考试->统计总分、排名等。
  • 主观题试卷流程:考试结束时间到->自动结束考试->等待人工阅卷->考试阅卷结束时间到->自动结束阅卷->统计总分、排名等。

程序启动时,会激活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);
}

小猫考试