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() {
}
}
}