package jnpf.tmsService.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.exception.DataException; import jnpf.tmsEntity.TmsExamEntity; import jnpf.tmsEntity.TmsExamItemEntity; import jnpf.tmsEntity.TmsPaperEntity; import jnpf.tmsEntity.TmsPaperQuestionEntity; import jnpf.tmsEntity.TmsPaperSectionEntity; import jnpf.tmsEntity.TmsPersonTaskEntity; import jnpf.tmsEntity.TmsQuestionEntity; import jnpf.tmsEntity.TmsQuestionOptionEntity; import jnpf.tmsEntity.TmsTaskEntity; import jnpf.tmsEntity.exam.TmsOnlineExamAttemptVO; import jnpf.tmsEntity.exam.TmsOnlineExamDetailVO; import jnpf.tmsEntity.exam.TmsOnlineExamGradeItemVO; import jnpf.tmsEntity.exam.TmsOnlineExamListVO; import jnpf.tmsEntity.exam.TmsOnlineExamOptionVO; import jnpf.tmsEntity.exam.TmsOnlineExamPaperVO; import jnpf.tmsEntity.exam.TmsOnlineExamQuery; import jnpf.tmsEntity.exam.TmsOnlineExamQuestionVO; import jnpf.tmsEntity.exam.TmsOnlineExamSubmitForm; import jnpf.tmsEntity.exam.TmsOnlineExamSubmitResultVO; import jnpf.tmsMapper.TmsExamItemMapper; import jnpf.tmsMapper.TmsExamMapper; import jnpf.tmsMapper.TmsPaperMapper; import jnpf.tmsMapper.TmsPaperQuestionMapper; import jnpf.tmsMapper.TmsPaperSectionMapper; import jnpf.tmsMapper.TmsPersonTaskMapper; import jnpf.tmsMapper.TmsQuestionMapper; import jnpf.tmsMapper.TmsQuestionOptionMapper; import jnpf.tmsMapper.TmsTaskMapper; import jnpf.tmsService.TmsOnlineExamService; import jnpf.tmsService.TmsPersonTaskService; import jnpf.tmsEntity.enums.TmsPassFlag; import jnpf.tmsEntity.enums.TmsOpenClosedInvalid; import jnpf.tmsEntity.enums.TmsExamStatus; import jnpf.tmsEntity.enums.TmsGradeStatus; import jnpf.tmsEntity.enums.TmsPaperSortMode; import jnpf.tmsEntity.enums.TmsQuestionType; import jnpf.tmsEntity.enums.TmsPersonTaskStatus; import jnpf.tmsEntity.enums.TmsTaskPublishStatus; import jnpf.tmsEntity.enums.TmsEvalMode; import jnpf.util.DateUtil; import jnpf.util.RandomUtil; import jnpf.util.StringUtil; import jnpf.util.UserProvider; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 学员在线考试:抽卷/开考、答题保存、交卷阅卷(客观题)、回写个人任务。 * *

发布任务时若考核方式=考试,会预建 {@code tms_exam} 待考壳;开考优先复用未开考壳。 * 交卷后调用 {@link TmsPersonTaskService#writeBackExamResult},不直接写档案目录。

*/ @Service @RequiredArgsConstructor public class TmsOnlineExamServiceImpl implements TmsOnlineExamService { private static final String FMT = "yyyy-MM-dd HH:mm:ss"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final TmsPaperMapper paperMapper; private final TmsPaperSectionMapper sectionMapper; private final TmsPaperQuestionMapper paperQuestionMapper; private final TmsQuestionMapper questionMapper; private final TmsQuestionOptionMapper optionMapper; private final TmsExamMapper examMapper; private final TmsExamItemMapper examItemMapper; private final TmsTaskMapper taskMapper; private final TmsPersonTaskMapper personTaskMapper; private final TmsPersonTaskService personTaskService; /** * 查询我的试卷/考试列表 */ @Override public List getMyPapers(TmsOnlineExamQuery query) { String userId = UserProvider.getLoginUserId(); // 只展示与「我的培训任务」或「已有答卷」相关的试卷,避免题库全量试卷都出现在考试记录 Set relatedPaperIds = this.loadRelatedPaperIds(userId); if (relatedPaperIds.isEmpty()) { query.setData(new ArrayList<>(), 0L); return new ArrayList<>(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsPaperEntity::getBizStatus, TmsOpenClosedInvalid.OPEN.getCode()); qw.in(TmsPaperEntity::getId, relatedPaperIds); if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) { qw.like(TmsPaperEntity::getPaperName, query.getKeyword().trim()); } Date examFrom = this.parseDayStart(query.getExamTimeFrom()); Date examTo = this.parseDayEnd(query.getExamTimeTo()); // 试卷考试窗口与查询区间有交集即可(该时段内可考的试卷) if (examFrom != null) { qw.and(w -> w.isNull(TmsPaperEntity::getExamEnd).or().ge(TmsPaperEntity::getExamEnd, examFrom)); } if (examTo != null) { qw.and(w -> w.isNull(TmsPaperEntity::getExamStart).or().le(TmsPaperEntity::getExamStart, examTo)); } qw.orderByDesc(TmsPaperEntity::getCreatorTime); List papers = this.paperMapper.selectList(qw); Map examMap = this.loadLatestExams(userId, papers); ArrayList all = new ArrayList(); for (TmsPaperEntity paper : papers) { TmsExamEntity exam = examMap.get(paper.getId()); // 尚无答卷壳且任务未到可考:不进考试记录(学满后才会生成待考壳) if (exam == null) { continue; } String status = this.statusOf(exam); if (StringUtil.isNotEmpty(query.getStatus()) && !"null".equalsIgnoreCase(query.getStatus()) && !query.getStatus().equals(status)) { continue; } TmsOnlineExamListVO vo = this.toListVo(paper, exam, status); if (!this.matchPassFlag(query.getPassFlag(), vo.getPassFlag())) { continue; } if (!this.matchAttemptType(query.getAttemptType(), vo.getAttemptNo())) { continue; } if (!this.matchCanRetake(query.getCanRetake(), vo.getCanRetake())) { continue; } all.add(vo); } long current = query.getCurrentPage() > 0L ? query.getCurrentPage() : 1L; long size = query.getPageSize() > 0L ? query.getPageSize() : 20L; int from = (int)((current - 1L) * size); int to = Math.min(from + (int)size, all.size()); ArrayList page = from >= all.size() ? new ArrayList() : new ArrayList(all.subList(from, to)); query.setData(page, (long)all.size()); return page; } /** * 加载与我相关的试卷 ID */ private Set loadRelatedPaperIds(String userId) { LinkedHashSet ids = new LinkedHashSet<>(); if (StringUtil.isEmpty(userId)) { return ids; } List exams = this.examMapper.selectList(new LambdaQueryWrapper() .eq(TmsExamEntity::getUserId, userId) .select(TmsExamEntity::getPaperId)); for (TmsExamEntity exam : exams) { if (exam != null && StringUtil.isNotEmpty(exam.getPaperId())) { ids.add(exam.getPaperId()); } } List persons = this.personTaskMapper.selectList(new LambdaQueryWrapper() .eq(TmsPersonTaskEntity::getUserId, userId) .ne(TmsPersonTaskEntity::getBizStatus, TmsPersonTaskStatus.CANCELLED.getCode())); if (persons == null || persons.isEmpty()) { return ids; } List taskIds = persons.stream() .map(TmsPersonTaskEntity::getTaskId) .filter(StringUtil::isNotEmpty) .distinct() .collect(Collectors.toList()); if (taskIds.isEmpty()) { return ids; } List tasks = this.taskMapper.selectList(new LambdaQueryWrapper() .in(TmsTaskEntity::getId, taskIds) .eq(TmsTaskEntity::getPublishStatus, TmsTaskPublishStatus.PUBLISHED.getCode()) .eq(TmsTaskEntity::getEvalMode, TmsEvalMode.EXAM.getCode())); for (TmsTaskEntity task : tasks) { if (task != null && StringUtil.isNotEmpty(task.getPaperId())) { ids.add(task.getPaperId()); } } return ids; } /** * 筛选合格标识 */ private boolean matchPassFlag(String queryFlag, String passFlag) { if (StringUtil.isEmpty(queryFlag) || "null".equalsIgnoreCase(queryFlag)) { return true; } if ("none".equalsIgnoreCase(queryFlag) || "-".equals(queryFlag)) { return StringUtil.isEmpty(passFlag); } String q = TmsPassFlag.normalize(queryFlag); String p = TmsPassFlag.normalize(passFlag); if (StringUtil.isNotEmpty(q) && q.equals(p)) { return true; } return queryFlag.equals(passFlag); } /** * 筛选考试次数类型(正考/补考) */ private boolean matchAttemptType(String attemptType, Integer attemptNo) { if (StringUtil.isEmpty(attemptType) || "null".equalsIgnoreCase(attemptType)) { return true; } int n = attemptNo == null ? 1 : attemptNo; if ("first".equalsIgnoreCase(attemptType)) { return n <= 1; } if ("retake".equalsIgnoreCase(attemptType)) { return n > 1; } return true; } /** * 筛选是否可补考 */ private boolean matchCanRetake(String canRetake, Boolean value) { if (StringUtil.isEmpty(canRetake) || "null".equalsIgnoreCase(canRetake)) { return true; } boolean want = "1".equals(canRetake) || "true".equalsIgnoreCase(canRetake) || "yes".equalsIgnoreCase(canRetake); return want == Boolean.TRUE.equals(value); } /** * 解析日开始时间 */ private Date parseDayStart(String day) { if (StringUtil.isEmpty(day) || "null".equalsIgnoreCase(day)) { return null; } String s = day.trim(); try { if (s.matches("^\\d{10,13}$")) { long ts = Long.parseLong(s); if (s.length() == 10) { ts = ts * 1000L; } return new Date(ts); } if (s.length() > 10) { s = s.substring(0, 10); } return DateUtil.stringToDate(s + " 00:00:00"); } catch (Exception e) { return null; } } /** * 解析日结束时间 */ private Date parseDayEnd(String day) { if (StringUtil.isEmpty(day) || "null".equalsIgnoreCase(day)) { return null; } String s = day.trim(); try { if (s.matches("^\\d{10,13}$")) { long ts = Long.parseLong(s); if (s.length() == 10) { ts = ts * 1000L; } // 结束日按当天 23:59:59 Date d = new Date(ts); return DateUtil.stringToDate(DateUtil.dateToString(d, "yyyy-MM-dd") + " 23:59:59"); } if (s.length() > 10) { s = s.substring(0, 10); } return DateUtil.stringToDate(s + " 23:59:59"); } catch (Exception e) { return null; } } /** * 查询详情 */ @Override public TmsOnlineExamDetailVO getDetail(String paperId) { if (StringUtil.isEmpty(paperId)) { throw new DataException("请选择试卷"); } TmsPaperEntity paper = this.paperMapper.selectById(paperId); if (paper == null) { throw new DataException("试卷不存在"); } TmsExamEntity exam = this.latestExam(UserProvider.getLoginUserId(), paperId); if (exam == null && !TmsOpenClosedInvalid.OPEN.equalsCode(paper.getBizStatus())) { throw new DataException("试卷未开放"); } TmsOnlineExamDetailVO vo = new TmsOnlineExamDetailVO(); vo.setId(paper.getId()); vo.setPaperId(paper.getId()); vo.setPaperName(paper.getPaperName()); vo.setStatus(this.statusOf(exam)); vo.setExamTimeText(this.timeText(paper.getExamStart(), paper.getExamEnd())); vo.setTotalScore(paper.getTotalScore()); vo.setPassScore(paper.getPassScore()); vo.setDurationMin(paper.getDurationMin()); if (exam != null) { vo.setExamId(exam.getId()); vo.setGotScore(exam.getTotalScore()); vo.setStartTime(this.fmt(exam.getStartTime())); vo.setSubmitTime(this.fmt(exam.getSubmitTime())); vo.setPassFlag(exam.getPassFlag()); vo.setGradeStatus(exam.getGradeStatus()); if (TmsExamStatus.SUBMITTED.equalsCode(exam.getExamStatus())) { vo.setQuestions(this.toReviewQuestions(this.loadItems(exam.getId()))); if (vo.getTotalScore() == null) { BigDecimal total = BigDecimal.ZERO; for (TmsOnlineExamQuestionVO q : vo.getQuestions()) { total = total.add(this.nvl(q.getScore())); } vo.setTotalScore(total); } } } this.fillRetakeMeta(vo, paper.getId(), exam, UserProvider.getLoginUserId()); vo.setAttempts(this.listAttempts(UserProvider.getLoginUserId(), paper.getId())); return vo; } /** * 考试回顾详情 */ @Override public TmsOnlineExamDetailVO getExamReview(String examId) { if (StringUtil.isEmpty(examId)) { throw new DataException("请选择答卷"); } TmsExamEntity exam = this.examMapper.selectById(examId); if (exam == null) { throw new DataException("答卷不存在"); } if (!Objects.equals(UserProvider.getLoginUserId(), exam.getUserId())) { throw new DataException("无权查看该答卷"); } if (!TmsExamStatus.SUBMITTED.equalsCode(exam.getExamStatus())) { throw new DataException("交卷后才能进行考试回顾"); } TmsPaperEntity paper = this.paperMapper.selectById((exam.getPaperId())); if (paper == null) { throw new DataException("试卷不存在"); } TmsOnlineExamDetailVO vo = new TmsOnlineExamDetailVO(); vo.setId(paper.getId()); vo.setPaperId(paper.getId()); vo.setPaperName(paper.getPaperName()); vo.setStatus(this.statusOf(exam)); vo.setExamTimeText(this.timeText(paper.getExamStart(), paper.getExamEnd())); vo.setTotalScore(paper.getTotalScore()); vo.setPassScore(paper.getPassScore() != null ? paper.getPassScore() : exam.getPassScore()); vo.setDurationMin(paper.getDurationMin()); vo.setExamId(exam.getId()); vo.setGotScore(exam.getTotalScore()); vo.setStartTime(this.fmt(exam.getStartTime())); vo.setSubmitTime(this.fmt(exam.getSubmitTime())); vo.setPassFlag(exam.getPassFlag()); vo.setGradeStatus(exam.getGradeStatus()); vo.setAttemptNo(Integer.valueOf(exam.getAttemptNo() == null ? 1 : exam.getAttemptNo())); vo.setAttemptLabel(this.attemptLabel(vo.getAttemptNo())); vo.setQuestions(this.toReviewQuestions(this.loadItems(exam.getId()))); if (vo.getTotalScore() == null) { BigDecimal total = BigDecimal.ZERO; for (TmsOnlineExamQuestionVO q : vo.getQuestions()) { total = total.add(this.nvl(q.getScore())); } vo.setTotalScore(total); } return vo; } /** * 查询考试尝试记录 */ private List listAttempts(String userId, String paperId) { if (StringUtil.isEmpty(userId) || StringUtil.isEmpty(paperId)) { return Collections.emptyList(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.eq(TmsExamEntity::getPaperId, paperId); qw.orderByAsc(TmsExamEntity::getAttemptNo); qw.orderByAsc(TmsExamEntity::getCreatorTime); List exams = this.examMapper.selectList(qw); ArrayList list = new ArrayList(exams.size()); for (TmsExamEntity exam : exams) { TmsOnlineExamAttemptVO row = new TmsOnlineExamAttemptVO(); row.setExamId(exam.getId()); int attemptNo = exam.getAttemptNo() == null ? 1 : exam.getAttemptNo(); row.setAttemptNo(Integer.valueOf(attemptNo)); row.setAttemptLabel(this.attemptLabel(attemptNo)); row.setStatus(this.statusOf(exam)); row.setGradeStatus(exam.getGradeStatus()); row.setStartTime(this.fmt(exam.getStartTime())); row.setSubmitTime(this.fmt(exam.getSubmitTime())); row.setGotScore(exam.getTotalScore()); row.setPassFlag(exam.getPassFlag()); list.add(row); } return list; } @Override @Transactional(rollbackFor = Exception.class, noRollbackFor = ExamTimedOutException.class) /** * 开考:优先复用发布时预建的未开考壳,否则新建尝试;受补考次数限制。 */ public TmsOnlineExamPaperVO start(String paperId) { if (StringUtil.isEmpty(paperId)) { throw new DataException("请选择试卷"); } String userId = UserProvider.getLoginUserId(); TmsExamEntity doing = this.findDoing(userId, paperId); if (doing != null) { if (this.isTimedOut(doing)) { // 超时须先交卷再返回;抛异常会导致事务回滚,答卷一直停在考试中 this.submitInternal(doing, null); TmsOnlineExamPaperVO vo = this.toPaperVo(doing, this.loadItems(doing.getId())); vo.setAutoSubmitted(Boolean.TRUE); vo.setRemainSeconds(Integer.valueOf(0)); return vo; } return this.toPaperVo(doing, this.loadItems(doing.getId())); } TmsPaperEntity paper = this.requireOpenPaper(paperId); this.assertExamWindow(paper); RetakeContext retake = this.resolveRetake(userId, paperId); if (!retake.allowStart) { throw new DataException(retake.message); } TmsExamEntity exam = this.createExam(paper, userId, retake); return this.toPaperVo(exam, this.loadItems(exam.getId())); } /** * 保存答题 */ @Override @Transactional(rollbackFor = Exception.class, noRollbackFor = ExamTimedOutException.class) public void saveAnswers(String examId, TmsOnlineExamSubmitForm form) { TmsExamEntity exam = this.requireDoingExam(examId); if (this.isTimedOut(exam)) { this.submitInternal(exam, form == null ? null : form.getAnswers()); throw new ExamTimedOutException(); } this.writeAnswers(exam.getId(), form == null ? null : form.getAnswers()); } @Override @Transactional(rollbackFor={Exception.class}) @AuditLog(action = "EXAM_SUBMIT", label = "在线考试交卷", bizType = "TMS_EXAM", bizCodeExpr = "#examId", targetTable = "tms_exam", targetIdExpr = "#examId") /** * 交卷 */ public TmsOnlineExamSubmitResultVO submit(String examId, TmsOnlineExamSubmitForm form) { TmsExamEntity exam = this.requireDoingExam(examId); return this.submitInternal(exam, form == null ? null : form.getAnswers()); } /** * 交卷内部处理(计分/落库) */ private TmsOnlineExamSubmitResultVO submitInternal(TmsExamEntity exam, Map answers) { List items = this.loadItems(exam.getId()); HashMap normalized = new HashMap(); for (TmsExamItemEntity item : items) { if (answers != null && answers.containsKey(item.getId())) { normalized.put(item.getId(), this.normalizeAnswer(answers.get(item.getId()), item.getQuestionType())); continue; } normalized.put(item.getId(), item.getUserAnswer() == null ? "" : item.getUserAnswer()); } BigDecimal objective = BigDecimal.ZERO; boolean pending = false; ArrayList grades = new ArrayList(); Date now = new Date(); for (TmsExamItemEntity item : items) { String userAnswer = normalized.getOrDefault(item.getId(), ""); item.setUserAnswer(userAnswer); TmsOnlineExamGradeItemVO grade = new TmsOnlineExamGradeItemVO(); grade.setId(item.getId()); grade.setCorrectAnswer(item.getCorrectAnswer()); grade.setAnalysis(item.getAnalysis()); boolean subjective = "1".equals(item.getIsSubjective()); grade.setSubjective(Boolean.valueOf(subjective)); if (subjective) { pending = true; item.setGotScore(null); grade.setGotScore(null); grade.setRight(null); } else { boolean right = this.answerRight(item, userAnswer); BigDecimal got = right ? this.nvl(item.getScore()) : BigDecimal.ZERO; item.setGotScore(got); objective = objective.add(got); grade.setGotScore(got); grade.setRight(Boolean.valueOf(right)); } item.setLastModifyTime(now); this.examItemMapper.updateById(item); grades.add(grade); } objective = objective.setScale(1, RoundingMode.HALF_UP); exam.setObjectiveScore(objective); exam.setSubmitTime(now); exam.setExamStatus(TmsExamStatus.SUBMITTED.getCode()); exam.setLastModifyTime(now); if (pending) { exam.setGradeStatus(TmsGradeStatus.PENDING.getCode()); exam.setSubjectiveScore(null); exam.setTotalScore(objective); exam.setPassFlag(null); } else { exam.setGradeStatus(TmsGradeStatus.AUTO.getCode()); exam.setSubjectiveScore(BigDecimal.ZERO); exam.setTotalScore(objective); exam.setPassFlag(this.passFlag(objective, exam.getPassScore())); } this.examMapper.updateById(exam); if (!pending) { this.personTaskService.writeBackExamResult(exam.getPersonTaskId(), exam.getTotalScore(), exam.getPassFlag()); } TmsOnlineExamSubmitResultVO result = new TmsOnlineExamSubmitResultVO(); result.setObjectiveScore(exam.getObjectiveScore()); result.setTotalScore(exam.getTotalScore()); result.setPassFlag(exam.getPassFlag()); result.setGradeStatus(exam.getGradeStatus()); result.setPendingGrade(Boolean.valueOf(pending)); result.setItems(grades); return result; } /** * 创建答卷记录 */ private TmsExamEntity createExam(TmsPaperEntity paper, String userId, RetakeContext retake) { List snaps = this.loadPaperQuestions(paper.getId()); if (snaps.isEmpty()) { throw new DataException("试卷没有试题,无法开考"); } if (TmsPaperSortMode.RANDOM.equalsCode(paper.getSortMode())) { Collections.shuffle(snaps); } Date now = new Date(); int attemptNo = retake == null || retake.nextAttemptNo == null ? 1 : retake.nextAttemptNo; TmsExamEntity exam = this.findUnstartedShell(userId, paper.getId(), retake, attemptNo); boolean reuseShell = exam != null; if (!reuseShell) { exam = new TmsExamEntity(); exam.setId(RandomUtil.uuId()); exam.setTaskId(retake == null ? null : retake.taskId); exam.setPersonTaskId(retake == null ? null : retake.personTaskId); exam.setPaperId(paper.getId()); exam.setPaperName(paper.getPaperName()); exam.setUserId(userId); exam.setAttemptNo(Integer.valueOf(attemptNo)); exam.setDurationMin(paper.getDurationMin()); BigDecimal passScore = paper.getPassScore(); if (retake != null && retake.passScore != null) { passScore = retake.passScore; } exam.setPassScore(passScore); exam.setCreatorUserId(userId); exam.setCreatorTime(now); } else { if (StringUtil.isEmpty(exam.getPaperName())) { exam.setPaperName(paper.getPaperName()); } if (exam.getDurationMin() == null) { exam.setDurationMin(paper.getDurationMin()); } if (exam.getPassScore() == null) { BigDecimal passScore = paper.getPassScore(); if (retake != null && retake.passScore != null) { passScore = retake.passScore; } exam.setPassScore(passScore); } if (StringUtil.isEmpty(exam.getTaskId()) && retake != null) { exam.setTaskId(retake.taskId); } if (StringUtil.isEmpty(exam.getPersonTaskId()) && retake != null) { exam.setPersonTaskId(retake.personTaskId); } } exam.setStartTime(now); exam.setGradeStatus(TmsGradeStatus.AUTO.getCode()); exam.setExamStatus(TmsExamStatus.DOING.getCode()); exam.setLastModifyUserId(userId); exam.setLastModifyTime(now); if (reuseShell) { this.examMapper.updateById(exam); } else { this.examMapper.insert(exam); } int sort = 1; for (QuestionSnap snap : snaps) { TmsExamItemEntity item = new TmsExamItemEntity(); item.setId(RandomUtil.uuId()); item.setForeignId(exam.getId()); item.setQuestionId(snap.question.getId()); item.setQuestionType(snap.question.getQuestionType()); item.setStem(snap.question.getStem()); item.setOptionsJson(this.toJson(snap.options)); item.setCorrectAnswer(this.correctOf(snap)); item.setScore(snap.score); item.setIsSubjective(TmsQuestionType.ESSAY.equalsCode(snap.question.getQuestionType()) ? "1" : "0"); item.setAnalysis(snap.question.getAnalysis()); item.setSortNo(Integer.valueOf(sort++)); item.setCreatorUserId(userId); item.setCreatorTime(now); this.examItemMapper.insert(item); } return exam; } /** * 查找未开考答卷壳 */ private TmsExamEntity findUnstartedShell(String userId, String paperId, RetakeContext retake, int attemptNo) { if (StringUtil.isEmpty(userId) || StringUtil.isEmpty(paperId)) { return null; } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.eq(TmsExamEntity::getPaperId, paperId); qw.eq(TmsExamEntity::getAttemptNo, Integer.valueOf(attemptNo)); qw.and(w -> w.isNull(TmsExamEntity::getExamStatus) .or().eq(TmsExamEntity::getExamStatus, "") .or().eq(TmsExamEntity::getExamStatus, TmsExamStatus.NOT_STARTED.getCode())); if (retake != null && StringUtil.isNotEmpty(retake.personTaskId)) { qw.eq(TmsExamEntity::getPersonTaskId, retake.personTaskId); } else if (retake != null && StringUtil.isNotEmpty(retake.taskId)) { qw.eq(TmsExamEntity::getTaskId, retake.taskId); } qw.orderByAsc(TmsExamEntity::getCreatorTime); qw.last("limit 1"); return this.examMapper.selectOne(qw); } /** * 加载试卷题目 */ private List loadPaperQuestions(String paperId) { LambdaQueryWrapper secQw = new LambdaQueryWrapper<>(); secQw.eq(TmsPaperSectionEntity::getForeignId, paperId); secQw.orderByAsc(TmsPaperSectionEntity::getSortNo); List sections = this.sectionMapper.selectList(secQw); LambdaQueryWrapper qQw = new LambdaQueryWrapper<>(); qQw.eq(TmsPaperQuestionEntity::getForeignId, paperId); qQw.orderByAsc(TmsPaperQuestionEntity::getSortNo); List links = this.paperQuestionMapper.selectList(qQw); HashMap> bySection = new HashMap<>(); for (TmsPaperQuestionEntity link : links) { bySection.computeIfAbsent(link.getSectionId(), k -> new ArrayList<>()).add(link); } List questionIds = links.stream().map(TmsPaperQuestionEntity::getQuestionId).collect(Collectors.toList()); HashMap questions = new HashMap(); if (!questionIds.isEmpty()) { for (TmsQuestionEntity q : this.questionMapper.selectBatchIds(questionIds)) { questions.put(q.getId(), q); } } Map> optionMap = this.loadOptions(questionIds); ArrayList snaps = new ArrayList(); for (TmsPaperSectionEntity section : sections) { List rows = bySection.getOrDefault(section.getId(), Collections.emptyList()); rows.sort(Comparator.comparing(r -> r.getSortNo() == null ? 0 : r.getSortNo())); for (TmsPaperQuestionEntity row : rows) { TmsQuestionEntity question = questions.get(row.getQuestionId()); if (question == null) { throw new DataException("试卷中的试题已不存在,无法开考"); } QuestionSnap snap = new QuestionSnap(); snap.question = question; snap.score = row.getScore() == null ? BigDecimal.ZERO : row.getScore(); snap.options = this.toStoredOptions(optionMap.getOrDefault(question.getId(), Collections.emptyList())); snaps.add(snap); } } return snaps; } /** * 写入考生答案 */ private void writeAnswers(String examId, Map answers) { List items = this.loadItems(examId); Date now = new Date(); for (TmsExamItemEntity item : items) { Object raw = answers == null ? null : answers.get(item.getId()); item.setUserAnswer(this.normalizeAnswer(raw, item.getQuestionType())); item.setLastModifyTime(now); this.examItemMapper.updateById(item); } } /** * 转为试卷作答 VO */ private TmsOnlineExamPaperVO toPaperVo(TmsExamEntity exam, List items) { TmsOnlineExamPaperVO vo = new TmsOnlineExamPaperVO(); vo.setExamId(exam.getId()); vo.setPaperId(exam.getPaperId()); vo.setPaperName(exam.getPaperName()); vo.setPassScore(exam.getPassScore()); vo.setDurationMin(exam.getDurationMin()); vo.setStartTime(this.fmt(exam.getStartTime())); vo.setRemainSeconds(this.remainSeconds(exam)); BigDecimal total = BigDecimal.ZERO; ArrayList questions = new ArrayList(); for (TmsExamItemEntity item : items) { total = total.add(this.nvl(item.getScore())); TmsOnlineExamQuestionVO q = new TmsOnlineExamQuestionVO(); q.setId(item.getId()); q.setQuestionId(item.getQuestionId()); q.setQuestionType(item.getQuestionType()); q.setStem(item.getStem()); q.setScore(item.getScore()); q.setUserAnswer(item.getUserAnswer()); q.setOptions(this.toClientOptions(item.getOptionsJson())); questions.add(q); } vo.setTotalScore(total); vo.setQuestions(questions); vo.setAttemptNo(Integer.valueOf(exam.getAttemptNo() == null ? 1 : exam.getAttemptNo())); vo.setAttemptLabel(this.attemptLabel(vo.getAttemptNo())); return vo; } /** * 转为回顾题目 */ private List toReviewQuestions(List items) { ArrayList questions = new ArrayList(); for (TmsExamItemEntity item : items) { TmsOnlineExamQuestionVO q = new TmsOnlineExamQuestionVO(); q.setId(item.getId()); q.setQuestionId(item.getQuestionId()); q.setQuestionType(item.getQuestionType()); q.setStem(item.getStem()); q.setScore(item.getScore()); q.setGotScore(item.getGotScore()); q.setUserAnswer(item.getUserAnswer()); q.setCorrectAnswer(item.getCorrectAnswer()); q.setAnalysis(item.getAnalysis()); boolean subjective = "1".equals(item.getIsSubjective()); q.setSubjective(Boolean.valueOf(subjective)); if (subjective) { q.setRight(null); } else if (item.getScore() != null && item.getScore().compareTo(BigDecimal.ZERO) > 0) { q.setRight(Boolean.valueOf(this.nvl(item.getGotScore()).compareTo(item.getScore()) >= 0)); } else { q.setRight(Boolean.valueOf(Objects.equals(this.normalizeCorrect(item.getCorrectAnswer()), this.normalizeCorrect(item.getUserAnswer())))); } q.setOptions(this.toReviewOptions(item.getOptionsJson())); questions.add(q); } return questions; } /** * 转为回顾选项 */ private List toReviewOptions(String json) { List stored = this.parseOptions(json); ArrayList list = new ArrayList(stored.size()); for (StoredOption opt : stored) { TmsOnlineExamOptionVO vo = new TmsOnlineExamOptionVO(); vo.setOptionLabel(opt.getOptionLabel()); vo.setOptionContent(opt.getOptionContent()); vo.setIsCorrect(opt.getIsCorrect()); list.add(vo); } return list; } /** * 转为前端选项 */ private List toClientOptions(String json) { List stored = this.parseOptions(json); ArrayList list = new ArrayList(stored.size()); for (StoredOption opt : stored) { TmsOnlineExamOptionVO vo = new TmsOnlineExamOptionVO(); vo.setOptionLabel(opt.getOptionLabel()); vo.setOptionContent(opt.getOptionContent()); list.add(vo); } return list; } /** * 转为列表 VO */ private TmsOnlineExamListVO toListVo(TmsPaperEntity paper, TmsExamEntity exam, String status) { TmsOnlineExamListVO vo = new TmsOnlineExamListVO(); vo.setId(paper.getId()); vo.setPaperId(paper.getId()); vo.setPaperName(paper.getPaperName()); vo.setStatus(status); vo.setExamStart(this.fmt(paper.getExamStart())); vo.setExamEnd(this.fmt(paper.getExamEnd())); vo.setExamTimeText(this.timeText(paper.getExamStart(), paper.getExamEnd())); vo.setTotalScore(paper.getTotalScore()); vo.setPassScore(paper.getPassScore()); vo.setDurationMin(paper.getDurationMin()); if (exam != null && TmsExamStatus.SUBMITTED.equalsCode(exam.getExamStatus())) { vo.setGotScore(exam.getTotalScore()); vo.setPassFlag(exam.getPassFlag()); vo.setExamId(exam.getId()); } else if (exam != null) { vo.setExamId(exam.getId()); } this.fillRetakeMeta(vo, paper.getId(), exam, UserProvider.getLoginUserId()); return vo; } /** * 回填补考元数据 */ private void fillRetakeMeta(TmsOnlineExamListVO vo, String paperId, TmsExamEntity exam, String userId) { RetakeMeta meta = this.buildRetakeMeta(paperId, exam, userId); vo.setAttemptNo(meta.attemptNo); vo.setAttemptLabel(meta.attemptLabel); vo.setSubmittedCount(meta.submittedCount); vo.setRetakeLimit(meta.retakeLimit); vo.setRemainingRetakes(meta.remainingRetakes); vo.setCanRetake(meta.canRetake); } /** * 回填补考元数据 */ private void fillRetakeMeta(TmsOnlineExamDetailVO vo, String paperId, TmsExamEntity exam, String userId) { RetakeMeta meta = this.buildRetakeMeta(paperId, exam, userId); vo.setAttemptNo(meta.attemptNo); vo.setAttemptLabel(meta.attemptLabel); vo.setSubmittedCount(meta.submittedCount); vo.setRetakeLimit(meta.retakeLimit); vo.setRemainingRetakes(meta.remainingRetakes); vo.setCanRetake(meta.canRetake); } /** * 构建补考元数据 */ private RetakeMeta buildRetakeMeta(String paperId, TmsExamEntity exam, String userId) { RetakeMeta meta = new RetakeMeta(); RetakeContext ctx = this.resolveRetake(userId, paperId); meta.retakeLimit = ctx.retakeLimit; meta.submittedCount = ctx.submittedCount == null ? 0 : ctx.submittedCount; meta.attemptNo = exam != null && exam.getAttemptNo() != null ? exam.getAttemptNo() : (ctx.nextAttemptNo != null ? ctx.nextAttemptNo : Integer.valueOf(1)); meta.attemptLabel = this.attemptLabel(meta.attemptNo); if (meta.retakeLimit == null) { meta.remainingRetakes = 0; meta.canRetake = false; } else { int usedRetakes = Math.max(0, meta.submittedCount - 1); meta.remainingRetakes = Math.max(0, meta.retakeLimit - usedRetakes); boolean submittedFail = exam != null && TmsExamStatus.SUBMITTED.equalsCode(exam.getExamStatus()) && TmsPassFlag.isFail(exam.getPassFlag()); meta.canRetake = submittedFail && ctx.allowStart; } return meta; } /** * 考试次数标签 */ private String attemptLabel(Integer attemptNo) { int n; int n2 = n = attemptNo == null ? 1 : attemptNo; if (n <= 1) { return "首考"; } return "补考(第" + (n - 1) + "次)"; } /** * 加载用户各试卷最新答卷 */ private Map loadLatestExams(String userId, List papers) { HashMap map = new HashMap(); if (papers.isEmpty() || StringUtil.isEmpty(userId)) { return map; } List ids = papers.stream().map(TmsPaperEntity::getId).collect(Collectors.toList()); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.in(TmsExamEntity::getPaperId, ids); qw.orderByDesc(TmsExamEntity::getCreatorTime); for (TmsExamEntity exam : this.examMapper.selectList(qw)) { TmsExamEntity exists = map.get(exam.getPaperId()); if (exists != null && !TmsExamStatus.DOING.equalsCode(exam.getExamStatus())) continue; map.put(exam.getPaperId(), exam); } return map; } /** * 取最新答卷 */ private TmsExamEntity latestExam(String userId, String paperId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.eq(TmsExamEntity::getPaperId, paperId); qw.orderByDesc(TmsExamEntity::getCreatorTime); List list = this.examMapper.selectList(qw); TmsExamEntity picked = null; for (TmsExamEntity exam : list) { if (TmsExamStatus.DOING.equalsCode(exam.getExamStatus())) { return exam; } if (picked != null) continue; picked = exam; } return picked; } /** * 查找进行中的答卷 */ private TmsExamEntity findDoing(String userId, String paperId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.eq(TmsExamEntity::getPaperId, paperId); qw.eq(TmsExamEntity::getExamStatus, TmsExamStatus.DOING.getCode()); qw.orderByDesc(TmsExamEntity::getCreatorTime); qw.last("limit 1"); return this.examMapper.selectOne(qw); } /** * 取最近一次已交卷 */ private TmsExamEntity latestSubmitted(String userId, String paperId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.eq(TmsExamEntity::getPaperId, paperId); qw.eq(TmsExamEntity::getExamStatus, TmsExamStatus.SUBMITTED.getCode()); qw.orderByDesc(TmsExamEntity::getCreatorTime); qw.last("limit 1"); return this.examMapper.selectOne(qw); } /** * 解析是否可补考 */ private RetakeContext resolveRetake(String userId, String paperId) { RetakeContext ctx = new RetakeContext(); ctx.allowStart = true; ctx.nextAttemptNo = 1; TmsExamEntity last = this.latestSubmitted(userId, paperId); TmsPersonTaskEntity personTask = this.findActivePersonTask(userId, paperId); TmsTaskEntity task = null; if (personTask != null) { task = this.taskMapper.selectById((personTask.getTaskId())); } if (task != null) { ctx.taskId = task.getId(); ctx.personTaskId = personTask == null ? null : personTask.getId(); ctx.passScore = task.getPassScore(); int retakeLimit = task.getRetakeLimit() == null ? 1 : Math.max(0, task.getRetakeLimit()); long submitted = this.countSubmitted(userId, paperId, task.getId()); ctx.retakeLimit = retakeLimit; ctx.submittedCount = (int)submitted; ctx.nextAttemptNo = (int)submitted + 1; if (last == null) { return ctx; } if (TmsPassFlag.isPass(last.getPassFlag())) { ctx.allowStart = false; ctx.message = "该试卷已合格,无需重考"; return ctx; } if (submitted > (long)retakeLimit) { ctx.allowStart = false; ctx.message = "补考次数已用尽(上限 " + retakeLimit + " 次)"; return ctx; } return ctx; } if (last != null) { ctx.submittedCount = 1; ctx.retakeLimit = 0; ctx.allowStart = false; ctx.message = "该试卷已交卷"; } return ctx; } /** * 统计已交卷数 */ private long countSubmitted(String userId, String paperId, String taskId) { Long cnt; LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getUserId, userId); qw.eq(TmsExamEntity::getPaperId, paperId); qw.eq(TmsExamEntity::getExamStatus, TmsExamStatus.SUBMITTED.getCode()); if (StringUtil.isNotEmpty(taskId)) { qw.and(w -> ((w.eq(TmsExamEntity::getTaskId, taskId)).or()).isNull(TmsExamEntity::getTaskId)); } return (cnt = this.examMapper.selectCount(qw)) == null ? 0L : cnt; } /** * 查找进行中的个人任务 */ private TmsPersonTaskEntity findActivePersonTask(String userId, String paperId) { List tasks = this.taskMapper.selectList( new LambdaQueryWrapper() .eq(TmsTaskEntity::getPaperId, paperId) .eq(TmsTaskEntity::getPublishStatus, TmsTaskPublishStatus.PUBLISHED.getCode()) .orderByDesc(TmsTaskEntity::getPublishTime)); for (TmsTaskEntity t : tasks) { TmsPersonTaskEntity pt = this.findPersonTask(t.getId(), userId); if (pt == null || TmsPersonTaskStatus.CANCELLED.equalsCode(pt.getBizStatus())) { continue; } return pt; } return null; } /** * 查找个人培训任务 */ private TmsPersonTaskEntity findPersonTask(String taskId, String userId) { return this.personTaskMapper.selectOne(new LambdaQueryWrapper() .eq(TmsPersonTaskEntity::getTaskId, taskId) .eq(TmsPersonTaskEntity::getUserId, userId) .last("limit 1")); } /** * 校验进行中答卷并返回 */ private TmsExamEntity requireDoingExam(String examId) { TmsExamEntity exam = this.examMapper.selectById(examId); if (exam == null) { throw new DataException("答卷不存在"); } if (!Objects.equals(UserProvider.getLoginUserId(), exam.getUserId())) { throw new DataException("无权操作该答卷"); } if (!TmsExamStatus.DOING.equalsCode(exam.getExamStatus())) { throw new DataException("试卷已交卷"); } return exam; } /** * 校验试卷可考并返回 */ private TmsPaperEntity requireOpenPaper(String paperId) { TmsPaperEntity paper = this.paperMapper.selectById(paperId); if (paper == null) { throw new DataException("试卷不存在"); } if (!TmsOpenClosedInvalid.OPEN.equalsCode(paper.getBizStatus())) { throw new DataException("试卷未开放,不能参加考试"); } return paper; } /** * 校验考试时间窗口 */ private void assertExamWindow(TmsPaperEntity paper) { Date now = new Date(); if (paper.getExamStart() != null && now.before(paper.getExamStart())) { throw new DataException("未到考试时间"); } if (paper.getExamEnd() != null && now.after(paper.getExamEnd())) { throw new DataException("考试时间已结束"); } } /** * 加载答卷题目明细 */ private List loadItems(String examId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamItemEntity::getForeignId, examId); qw.orderByAsc(TmsExamItemEntity::getSortNo); return this.examItemMapper.selectList(qw); } /** * 加载题目选项 */ private Map> loadOptions(List questionIds) { if (questionIds == null || questionIds.isEmpty()) { return Collections.emptyMap(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.in(TmsQuestionOptionEntity::getForeignId, questionIds); qw.orderByAsc(TmsQuestionOptionEntity::getSortNo); HashMap> map = new HashMap>(); for (TmsQuestionOptionEntity opt : this.optionMapper.selectList(qw)) { map.computeIfAbsent(opt.getForeignId(), k -> new ArrayList()).add(opt); } return map; } /** * 转为落库选项结构 */ private List toStoredOptions(List opts) { ArrayList list = new ArrayList(); for (TmsQuestionOptionEntity opt : opts) { StoredOption stored = new StoredOption(); stored.setOptionLabel(opt.getOptionLabel()); stored.setOptionContent(opt.getOptionContent()); stored.setIsCorrect(opt.getIsCorrect()); list.add(stored); } return list; } /** * 取标准答案 */ private String correctOf(QuestionSnap snap) { String type = snap.question.getQuestionType(); if (TmsQuestionType.ESSAY.equalsCode(type)) { return ""; } if (TmsQuestionType.BLANK.equalsCode(type)) { return snap.options.stream().map(StoredOption::getOptionContent).filter(StringUtil::isNotEmpty).collect(Collectors.joining("?")); } return snap.options.stream().filter(o -> "1".equals(o.getIsCorrect())).map(StoredOption::getOptionLabel).filter(StringUtil::isNotEmpty).sorted().collect(Collectors.joining(",")); } /** * 判断作答是否正确 */ private boolean answerRight(TmsExamItemEntity item, String userAnswer) { if (StringUtil.isEmpty(item.getCorrectAnswer())) { return false; } if (TmsQuestionType.BLANK.equalsCode(item.getQuestionType())) { return item.getCorrectAnswer().trim().equals(userAnswer == null ? "" : userAnswer.trim()); } return Objects.equals(this.normalizeCorrect(item.getCorrectAnswer()), this.normalizeCorrect(userAnswer)); } /** * 规范化作答内容 */ private String normalizeAnswer(Object raw, String questionType) { if (raw == null) { return ""; } if (TmsQuestionType.MULTI.equalsCode(questionType)) { LinkedHashSet labels = new LinkedHashSet(); if (raw instanceof List) { for (Object o : (List)raw) { if (o == null || !StringUtil.isNotEmpty(String.valueOf(o))) continue; labels.add(String.valueOf(o).trim()); } } else { String text = String.valueOf(raw).trim(); if (StringUtil.isNotEmpty(text)) { for (String part : text.split("[,?]")) { if (!StringUtil.isNotEmpty(part.trim())) continue; labels.add(part.trim()); } } } return labels.stream().sorted().collect(Collectors.joining(",")); } return String.valueOf(raw).trim(); } /** * 规范化标准答案 */ private String normalizeCorrect(String correct) { if (StringUtil.isEmpty(correct)) { return ""; } return Arrays.stream(correct.split("[,?]")).map(String::trim).filter(StringUtil::isNotEmpty).sorted().collect(Collectors.joining(",")); } /** * 是否已超时 */ private boolean isTimedOut(TmsExamEntity exam) { Integer remain = this.remainSeconds(exam); return remain != null && remain <= 0; } /** * 剩余答题秒数 */ private Integer remainSeconds(TmsExamEntity exam) { if (exam == null || exam.getDurationMin() == null || exam.getDurationMin() <= 0 || exam.getStartTime() == null) { return null; } long endMs = exam.getStartTime().getTime() + (long)exam.getDurationMin().intValue() * 60000L; long remain = (endMs - System.currentTimeMillis()) / 1000L; return (int)Math.max(0L, remain); } /** * 根据得分与及格分计算合格标识 */ private String passFlag(BigDecimal score, BigDecimal passScore) { return TmsPassFlag.ofScore(score, passScore); } /** * 解析答卷状态文案/编码 */ private String statusOf(TmsExamEntity exam) { if (exam == null || StringUtil.isEmpty(exam.getExamStatus()) || TmsExamStatus.NOT_STARTED.equalsCode(exam.getExamStatus())) { return TmsExamStatus.NOT_STARTED.getCode(); } if (TmsExamStatus.DOING.equalsCode(exam.getExamStatus())) { return TmsExamStatus.DOING.getCode(); } return TmsExamStatus.SUBMITTED.getCode(); } /** * 时间展示文本 */ private String timeText(Date start, Date end) { if (start == null && end == null) { return "??"; } String left = start == null ? "??" : this.fmt(start); String right = end == null ? "??" : this.fmt(end); return left + " ~ " + right; } /** * 格式化日期 */ private String fmt(Date date) { return date == null ? null : DateUtil.dateToString((Date)date, (String)FMT); } /** * 空值兜底 */ private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } /** * 对象转 JSON */ private String toJson(Object obj) { try { return OBJECT_MAPPER.writeValueAsString(obj); } catch (Exception e) { throw new DataException("JSON序列化失败"); } } /** * 解析选项 JSON */ private List parseOptions(String json) { if (StringUtil.isEmpty(json)) { return Collections.emptyList(); } try { return (List)OBJECT_MAPPER.readValue(json, (TypeReference)new TypeReference>(){}); } catch (Exception e) { return Collections.emptyList(); } } private static class RetakeContext { private boolean allowStart; private String message; private String taskId; private String personTaskId; private Integer nextAttemptNo; private BigDecimal passScore; private Integer retakeLimit; private Integer submittedCount; private RetakeContext() { } } private static class QuestionSnap { private TmsQuestionEntity question; private BigDecimal score; private List options; private QuestionSnap() { } } @JsonIgnoreProperties(ignoreUnknown=true) private static class StoredOption { private String optionLabel; private String optionContent; private String isCorrect; public StoredOption() { } /** * 获取选项标签 */ public String getOptionLabel() { return this.optionLabel; } /** * 获取选项内容 */ public String getOptionContent() { return this.optionContent; } /** * 获取是否正确 */ public String getIsCorrect() { return this.isCorrect; } /** * 设置选项标签 */ public void setOptionLabel(String optionLabel) { this.optionLabel = optionLabel; } /** * 设置选项内容 */ public void setOptionContent(String optionContent) { this.optionContent = optionContent; } /** * 设置是否正确 */ public void setIsCorrect(String isCorrect) { this.isCorrect = isCorrect; } /** * 相等比较 */ public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof StoredOption)) { return false; } StoredOption other = (StoredOption)o; if (!other.canEqual(this)) { return false; } String this$optionLabel = this.getOptionLabel(); String other$optionLabel = other.getOptionLabel(); if (this$optionLabel == null ? other$optionLabel != null : !this$optionLabel.equals(other$optionLabel)) { return false; } String this$optionContent = this.getOptionContent(); String other$optionContent = other.getOptionContent(); if (this$optionContent == null ? other$optionContent != null : !this$optionContent.equals(other$optionContent)) { return false; } String this$isCorrect = this.getIsCorrect(); String other$isCorrect = other.getIsCorrect(); return !(this$isCorrect == null ? other$isCorrect != null : !this$isCorrect.equals(other$isCorrect)); } /** * 是否可与同类比较 */ protected boolean canEqual(Object other) { return other instanceof StoredOption; } /** * 哈希值 */ public int hashCode() { int PRIME = 59; int result = 1; String $optionLabel = this.getOptionLabel(); result = result * 59 + ($optionLabel == null ? 43 : $optionLabel.hashCode()); String $optionContent = this.getOptionContent(); result = result * 59 + ($optionContent == null ? 43 : $optionContent.hashCode()); String $isCorrect = this.getIsCorrect(); result = result * 59 + ($isCorrect == null ? 43 : $isCorrect.hashCode()); return result; } /** * 调试字符串 */ public String toString() { return "TmsOnlineExamServiceImpl.StoredOption(optionLabel=" + this.getOptionLabel() + ", optionContent=" + this.getOptionContent() + ", isCorrect=" + this.getIsCorrect() + ")"; } } /** 超时已自动交卷:配合 noRollbackFor,保证交卷结果先提交 */ public static class ExamTimedOutException extends DataException { public ExamTimedOutException() { super("考试时间已到,已自动交卷"); } } private static class RetakeMeta { private Integer attemptNo; private String attemptLabel; private Integer submittedCount; private Integer retakeLimit; private Integer remainingRetakes; private Boolean canRetake; private RetakeMeta() { } } }