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;
|
|
/**
|
* 学员在线考试:抽卷/开考、答题保存、交卷阅卷(客观题)、回写个人任务。
|
*
|
* <p>发布任务时若考核方式=考试,会预建 {@code tms_exam} 待考壳;开考优先复用未开考壳。
|
* 交卷后调用 {@link TmsPersonTaskService#writeBackExamResult},不直接写档案目录。</p>
|
*/
|
@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<TmsOnlineExamListVO> getMyPapers(TmsOnlineExamQuery query) {
|
String userId = UserProvider.getLoginUserId();
|
// 只展示与「我的培训任务」或「已有答卷」相关的试卷,避免题库全量试卷都出现在考试记录
|
Set<String> relatedPaperIds = this.loadRelatedPaperIds(userId);
|
if (relatedPaperIds.isEmpty()) {
|
query.setData(new ArrayList<>(), 0L);
|
return new ArrayList<>();
|
}
|
LambdaQueryWrapper<TmsPaperEntity> 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<TmsPaperEntity> papers = this.paperMapper.selectList(qw);
|
Map<String, TmsExamEntity> examMap = this.loadLatestExams(userId, papers);
|
ArrayList<TmsOnlineExamListVO> all = new ArrayList<TmsOnlineExamListVO>();
|
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<TmsOnlineExamListVO> page = from >= all.size() ? new ArrayList<TmsOnlineExamListVO>() : new ArrayList(all.subList(from, to));
|
query.setData(page, (long)all.size());
|
return page;
|
}
|
|
/**
|
* 加载与我相关的试卷 ID
|
*/
|
private Set<String> loadRelatedPaperIds(String userId) {
|
LinkedHashSet<String> ids = new LinkedHashSet<>();
|
if (StringUtil.isEmpty(userId)) {
|
return ids;
|
}
|
List<TmsExamEntity> exams = this.examMapper.selectList(new LambdaQueryWrapper<TmsExamEntity>()
|
.eq(TmsExamEntity::getUserId, userId)
|
.select(TmsExamEntity::getPaperId));
|
for (TmsExamEntity exam : exams) {
|
if (exam != null && StringUtil.isNotEmpty(exam.getPaperId())) {
|
ids.add(exam.getPaperId());
|
}
|
}
|
List<TmsPersonTaskEntity> persons = this.personTaskMapper.selectList(new LambdaQueryWrapper<TmsPersonTaskEntity>()
|
.eq(TmsPersonTaskEntity::getUserId, userId)
|
.ne(TmsPersonTaskEntity::getBizStatus, TmsPersonTaskStatus.CANCELLED.getCode()));
|
if (persons == null || persons.isEmpty()) {
|
return ids;
|
}
|
List<String> taskIds = persons.stream()
|
.map(TmsPersonTaskEntity::getTaskId)
|
.filter(StringUtil::isNotEmpty)
|
.distinct()
|
.collect(Collectors.toList());
|
if (taskIds.isEmpty()) {
|
return ids;
|
}
|
List<TmsTaskEntity> tasks = this.taskMapper.selectList(new LambdaQueryWrapper<TmsTaskEntity>()
|
.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<TmsOnlineExamAttemptVO> listAttempts(String userId, String paperId) {
|
if (StringUtil.isEmpty(userId) || StringUtil.isEmpty(paperId)) {
|
return Collections.emptyList();
|
}
|
LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
|
qw.eq(TmsExamEntity::getUserId, userId);
|
qw.eq(TmsExamEntity::getPaperId, paperId);
|
qw.orderByAsc(TmsExamEntity::getAttemptNo);
|
qw.orderByAsc(TmsExamEntity::getCreatorTime);
|
List<TmsExamEntity> exams = this.examMapper.selectList(qw);
|
ArrayList<TmsOnlineExamAttemptVO> list = new ArrayList<TmsOnlineExamAttemptVO>(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<String, Object> answers) {
|
List<TmsExamItemEntity> items = this.loadItems(exam.getId());
|
HashMap<String, String> normalized = new HashMap<String, String>();
|
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<TmsOnlineExamGradeItemVO> grades = new ArrayList<TmsOnlineExamGradeItemVO>();
|
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<QuestionSnap> 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<TmsExamEntity> 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<QuestionSnap> loadPaperQuestions(String paperId) {
|
LambdaQueryWrapper<TmsPaperSectionEntity> secQw = new LambdaQueryWrapper<>();
|
secQw.eq(TmsPaperSectionEntity::getForeignId, paperId);
|
secQw.orderByAsc(TmsPaperSectionEntity::getSortNo);
|
List<TmsPaperSectionEntity> sections = this.sectionMapper.selectList(secQw);
|
LambdaQueryWrapper<TmsPaperQuestionEntity> qQw = new LambdaQueryWrapper<>();
|
qQw.eq(TmsPaperQuestionEntity::getForeignId, paperId);
|
qQw.orderByAsc(TmsPaperQuestionEntity::getSortNo);
|
List<TmsPaperQuestionEntity> links = this.paperQuestionMapper.selectList(qQw);
|
HashMap<String, List<TmsPaperQuestionEntity>> bySection = new HashMap<>();
|
for (TmsPaperQuestionEntity link : links) {
|
bySection.computeIfAbsent(link.getSectionId(), k -> new ArrayList<>()).add(link);
|
}
|
List<String> questionIds = links.stream().map(TmsPaperQuestionEntity::getQuestionId).collect(Collectors.toList());
|
HashMap<String, TmsQuestionEntity> questions = new HashMap<String, TmsQuestionEntity>();
|
if (!questionIds.isEmpty()) {
|
for (TmsQuestionEntity q : this.questionMapper.selectBatchIds(questionIds)) {
|
questions.put(q.getId(), q);
|
}
|
}
|
Map<String, List<TmsQuestionOptionEntity>> optionMap = this.loadOptions(questionIds);
|
ArrayList<QuestionSnap> snaps = new ArrayList<QuestionSnap>();
|
for (TmsPaperSectionEntity section : sections) {
|
List<TmsPaperQuestionEntity> 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<String, Object> answers) {
|
List<TmsExamItemEntity> 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<TmsExamItemEntity> 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<TmsOnlineExamQuestionVO> questions = new ArrayList<TmsOnlineExamQuestionVO>();
|
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<TmsOnlineExamQuestionVO> toReviewQuestions(List<TmsExamItemEntity> items) {
|
ArrayList<TmsOnlineExamQuestionVO> questions = new ArrayList<TmsOnlineExamQuestionVO>();
|
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<TmsOnlineExamOptionVO> toReviewOptions(String json) {
|
List<StoredOption> stored = this.parseOptions(json);
|
ArrayList<TmsOnlineExamOptionVO> list = new ArrayList<TmsOnlineExamOptionVO>(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<TmsOnlineExamOptionVO> toClientOptions(String json) {
|
List<StoredOption> stored = this.parseOptions(json);
|
ArrayList<TmsOnlineExamOptionVO> list = new ArrayList<TmsOnlineExamOptionVO>(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<String, TmsExamEntity> loadLatestExams(String userId, List<TmsPaperEntity> papers) {
|
HashMap<String, TmsExamEntity> map = new HashMap<String, TmsExamEntity>();
|
if (papers.isEmpty() || StringUtil.isEmpty(userId)) {
|
return map;
|
}
|
List<String> ids = papers.stream().map(TmsPaperEntity::getId).collect(Collectors.toList());
|
LambdaQueryWrapper<TmsExamEntity> 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<TmsExamEntity> qw = new LambdaQueryWrapper<>();
|
qw.eq(TmsExamEntity::getUserId, userId);
|
qw.eq(TmsExamEntity::getPaperId, paperId);
|
qw.orderByDesc(TmsExamEntity::getCreatorTime);
|
List<TmsExamEntity> 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<TmsExamEntity> 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<TmsExamEntity> 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<TmsExamEntity> 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<TmsTaskEntity> tasks = this.taskMapper.selectList(
|
new LambdaQueryWrapper<TmsTaskEntity>()
|
.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<TmsPersonTaskEntity>()
|
.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<TmsExamItemEntity> loadItems(String examId) {
|
LambdaQueryWrapper<TmsExamItemEntity> qw = new LambdaQueryWrapper<>();
|
qw.eq(TmsExamItemEntity::getForeignId, examId);
|
qw.orderByAsc(TmsExamItemEntity::getSortNo);
|
return this.examItemMapper.selectList(qw);
|
}
|
|
/**
|
* 加载题目选项
|
*/
|
private Map<String, List<TmsQuestionOptionEntity>> loadOptions(List<String> questionIds) {
|
if (questionIds == null || questionIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
LambdaQueryWrapper<TmsQuestionOptionEntity> qw = new LambdaQueryWrapper<>();
|
qw.in(TmsQuestionOptionEntity::getForeignId, questionIds);
|
qw.orderByAsc(TmsQuestionOptionEntity::getSortNo);
|
HashMap<String, List<TmsQuestionOptionEntity>> map = new HashMap<String, List<TmsQuestionOptionEntity>>();
|
for (TmsQuestionOptionEntity opt : this.optionMapper.selectList(qw)) {
|
map.computeIfAbsent(opt.getForeignId(), k -> new ArrayList()).add(opt);
|
}
|
return map;
|
}
|
|
/**
|
* 转为落库选项结构
|
*/
|
private List<StoredOption> toStoredOptions(List<TmsQuestionOptionEntity> opts) {
|
ArrayList<StoredOption> list = new ArrayList<StoredOption>();
|
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<String> labels = new LinkedHashSet<String>();
|
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<StoredOption> parseOptions(String json) {
|
if (StringUtil.isEmpty(json)) {
|
return Collections.emptyList();
|
}
|
try {
|
return (List)OBJECT_MAPPER.readValue(json, (TypeReference)new TypeReference<List<StoredOption>>(){});
|
}
|
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<StoredOption> 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() {
|
}
|
}
|
}
|