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 jnpf.exception.DataException; import jnpf.permission.UserApi; import jnpf.permission.entity.UserEntity; import jnpf.tmsEntity.TmsExamEntity; import jnpf.tmsEntity.TmsExamItemEntity; import jnpf.tmsEntity.TmsPaperEntity; import jnpf.tmsEntity.examgrade.TmsExamGradeDetailVO; import jnpf.tmsEntity.examgrade.TmsExamGradeExamVO; import jnpf.tmsEntity.examgrade.TmsExamGradeItemVO; import jnpf.tmsEntity.examgrade.TmsExamGradeOptionVO; import jnpf.tmsEntity.examgrade.TmsExamGradeQuery; import jnpf.tmsEntity.examgrade.TmsExamGradeSessionVO; import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitForm; import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitResultVO; import jnpf.tmsMapper.TmsExamItemMapper; import jnpf.tmsMapper.TmsExamMapper; import jnpf.tmsMapper.TmsPaperMapper; import jnpf.tmsService.TmsExamGradeService; import jnpf.util.DateUtil; import jnpf.util.StringUtil; import jnpf.util.UserProvider; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class TmsExamGradeServiceImpl implements TmsExamGradeService { private static final String EXAM_SUBMITTED = "submitted"; private static final String GRADE_PENDING = "pending"; private static final String GRADE_GRADED = "graded"; private static final String FMT = "yyyy-MM-dd HH:mm:ss"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final TmsExamMapper examMapper; private final TmsExamItemMapper examItemMapper; private final TmsPaperMapper paperMapper; private final UserApi userApi; @Override public List getSessionList(TmsExamGradeQuery query) { List exams = loadGradeExams(null); Map> byPaper = groupByPaper(exams); Map papers = loadPapers(new ArrayList<>(byPaper.keySet())); List all = new ArrayList<>(); for (Map.Entry> entry : byPaper.entrySet()) { all.add(toSession(entry.getKey(), papers.get(entry.getKey()), entry.getValue())); } all.sort(Comparator .comparing(TmsExamGradeSessionVO::getPendingCount, Comparator.nullsLast(Integer::compareTo)).reversed() .thenComparing(TmsExamGradeSessionVO::getExamEnd, Comparator.nullsLast(String::compareTo)).reversed()); String keyword = trim(query.getKeyword()); String taskNo = trim(query.getTaskNo()); if (StringUtil.isNotEmpty(keyword)) { String kw = keyword.toLowerCase(); all = all.stream() .filter(x -> contains(x.getTaskSubject(), kw) || contains(x.getPaperName(), kw) || contains(x.getTaskNo(), kw)) .collect(Collectors.toList()); } if (StringUtil.isNotEmpty(taskNo)) { String tn = taskNo.toLowerCase(); all = all.stream().filter(x -> contains(x.getTaskNo(), tn)).collect(Collectors.toList()); } long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1; long size = query.getPageSize() > 0 ? query.getPageSize() : 20; int from = (int) ((current - 1) * size); int to = Math.min(from + (int) size, all.size()); List page = from >= all.size() ? new ArrayList<>() : new ArrayList<>(all.subList(from, to)); query.setData(page, all.size()); return page; } @Override public TmsExamGradeSessionVO getSession(String sessionId) { if (StringUtil.isEmpty(sessionId)) { throw new DataException("阅卷场次不存在"); } List exams = loadGradeExams(sessionId); if (exams.isEmpty()) { throw new DataException("阅卷场次不存在"); } return toSession(sessionId, paperMapper.selectById(sessionId), exams); } @Override public List getExamList(String sessionId) { if (StringUtil.isEmpty(sessionId)) { return Collections.emptyList(); } List exams = loadGradeExams(sessionId); exams.sort(Comparator.comparing(TmsExamEntity::getSubmitTime, Comparator.nullsLast(Date::compareTo)).reversed()); Map names = loadUserNames(exams.stream().map(TmsExamEntity::getUserId).collect(Collectors.toList())); List rows = new ArrayList<>(); for (TmsExamEntity exam : exams) { TmsExamGradeExamVO vo = new TmsExamGradeExamVO(); vo.setId(exam.getId()); vo.setSessionId(sessionId); vo.setUserId(exam.getUserId()); vo.setUserName(names.getOrDefault(exam.getUserId(), exam.getUserId())); vo.setSubmitTime(fmt(exam.getSubmitTime())); vo.setObjectiveScore(exam.getObjectiveScore()); vo.setSubjectiveScore(exam.getSubjectiveScore()); vo.setTotalScore(GRADE_PENDING.equals(exam.getGradeStatus()) ? null : exam.getTotalScore()); vo.setPassScore(exam.getPassScore()); vo.setGradeStatus(exam.getGradeStatus()); vo.setPassFlag(exam.getPassFlag()); rows.add(vo); } return rows; } @Override public TmsExamGradeDetailVO getExamDetail(String examId) { TmsExamEntity exam = requireGradeExam(examId); List items = loadItems(exam.getId()); Map names = loadUserNames(Collections.singletonList(exam.getUserId())); TmsExamGradeDetailVO vo = new TmsExamGradeDetailVO(); vo.setExamId(exam.getId()); vo.setSessionId(exam.getPaperId()); vo.setPaperName(exam.getPaperName()); vo.setUserName(names.getOrDefault(exam.getUserId(), exam.getUserId())); vo.setSubmitTime(fmt(exam.getSubmitTime())); vo.setTotalScore(resolvePaperTotal(exam)); vo.setPassScore(exam.getPassScore()); vo.setObjectiveScore(nvl(exam.getObjectiveScore())); vo.setSubjectiveScore(nvl(exam.getSubjectiveScore())); vo.setGradeStatus(exam.getGradeStatus()); vo.setItems(items.stream().map(this::toItemVo).collect(Collectors.toList())); return vo; } @Override @Transactional(rollbackFor = Exception.class) public TmsExamGradeSubmitResultVO submitGrade(TmsExamGradeSubmitForm form) { if (form == null || StringUtil.isEmpty(form.getExamId())) { throw new DataException("答卷不存在"); } TmsExamEntity exam = requireGradeExam(form.getExamId()); if (!GRADE_PENDING.equals(exam.getGradeStatus())) { throw new DataException("该答卷已批改,无需重复提交"); } List items = loadItems(exam.getId()); Map scoreMap = new HashMap<>(); if (form.getItems() != null) { for (TmsExamGradeSubmitForm.ItemScore row : form.getItems()) { if (row == null || StringUtil.isEmpty(row.getId())) { continue; } scoreMap.put(row.getId(), row.getGotScore()); } } BigDecimal subjective = BigDecimal.ZERO; Date now = new Date(); String graderId = UserProvider.getLoginUserId(); for (TmsExamItemEntity item : items) { if (!"1".equals(item.getIsSubjective())) { continue; } if (!scoreMap.containsKey(item.getId()) || scoreMap.get(item.getId()) == null) { throw new DataException("请为所有主观题打分"); } BigDecimal got = scoreMap.get(item.getId()); BigDecimal max = nvl(item.getScore()); if (got.compareTo(BigDecimal.ZERO) < 0 || got.compareTo(max) > 0) { throw new DataException("主观题得分需在 0 ~ " + max.stripTrailingZeros().toPlainString() + " 之间"); } item.setGotScore(got); item.setLastModifyUserId(graderId); item.setLastModifyTime(now); examItemMapper.updateById(item); subjective = subjective.add(got); } BigDecimal objective = nvl(exam.getObjectiveScore()); BigDecimal total = objective.add(subjective); exam.setSubjectiveScore(subjective); exam.setTotalScore(total); exam.setPassFlag(passFlag(total, exam.getPassScore())); exam.setGradeStatus(GRADE_GRADED); exam.setGraderId(graderId); exam.setGradeTime(now); exam.setLastModifyUserId(graderId); exam.setLastModifyTime(now); examMapper.updateById(exam); TmsExamGradeSubmitResultVO result = new TmsExamGradeSubmitResultVO(); result.setExamId(exam.getId()); result.setSubjectiveScore(subjective); result.setTotalScore(total); result.setPassFlag(exam.getPassFlag()); result.setGradeStatus(GRADE_GRADED); result.setNextExamId(findNextPending(exam.getPaperId(), exam.getId())); return result; } private String findNextPending(String paperId, String excludeExamId) { List exams = loadGradeExams(paperId); return exams.stream() .filter(x -> GRADE_PENDING.equals(x.getGradeStatus())) .filter(x -> !Objects.equals(x.getId(), excludeExamId)) .sorted(Comparator.comparing(TmsExamEntity::getSubmitTime, Comparator.nullsLast(Date::compareTo))) .map(TmsExamEntity::getId) .findFirst() .orElse(null); } private TmsExamEntity requireGradeExam(String examId) { if (StringUtil.isEmpty(examId)) { throw new DataException("答卷不存在"); } TmsExamEntity exam = examMapper.selectById(examId); if (exam == null || !EXAM_SUBMITTED.equals(exam.getExamStatus())) { throw new DataException("答卷不存在"); } if (!GRADE_PENDING.equals(exam.getGradeStatus()) && !GRADE_GRADED.equals(exam.getGradeStatus())) { throw new DataException("该答卷无需阅卷"); } return exam; } private List loadGradeExams(String paperId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamEntity::getExamStatus, EXAM_SUBMITTED) .in(TmsExamEntity::getGradeStatus, GRADE_PENDING, GRADE_GRADED) .eq(StringUtil.isNotEmpty(paperId), TmsExamEntity::getPaperId, paperId) .orderByDesc(TmsExamEntity::getSubmitTime); return examMapper.selectList(qw); } private Map> groupByPaper(List exams) { Map> map = new LinkedHashMap<>(); for (TmsExamEntity exam : exams) { if (StringUtil.isEmpty(exam.getPaperId())) { continue; } map.computeIfAbsent(exam.getPaperId(), k -> new ArrayList<>()).add(exam); } return map; } private Map loadPapers(List ids) { if (ids == null || ids.isEmpty()) { return Collections.emptyMap(); } List list = paperMapper.selectBatchIds(ids); Map map = new HashMap<>(); for (TmsPaperEntity paper : list) { map.put(paper.getId(), paper); } return map; } private TmsExamGradeSessionVO toSession(String paperId, TmsPaperEntity paper, List exams) { TmsExamGradeSessionVO vo = new TmsExamGradeSessionVO(); vo.setId(paperId); vo.setPaperId(paperId); String paperName = paper != null && StringUtil.isNotEmpty(paper.getPaperName()) ? paper.getPaperName() : (exams.isEmpty() ? paperId : exams.get(0).getPaperName()); vo.setPaperName(paperName); vo.setTaskSubject(paperName); String paperNo = paper != null && StringUtil.isNotEmpty(paper.getPaperNo()) ? paper.getPaperNo() : paperId; vo.setTaskNo(paperNo); vo.setDurationMin(paper != null && paper.getDurationMin() != null ? paper.getDurationMin() : (exams.isEmpty() ? null : exams.get(0).getDurationMin())); vo.setTotalScore(paper != null && paper.getTotalScore() != null ? paper.getTotalScore() : resolvePaperTotal(exams.isEmpty() ? null : exams.get(0))); vo.setPassScore(paper != null && paper.getPassScore() != null ? paper.getPassScore() : (exams.isEmpty() ? null : exams.get(0).getPassScore())); Date start = paper != null ? paper.getExamStart() : null; Date end = paper != null ? paper.getExamEnd() : null; if (start == null) { start = exams.stream().map(TmsExamEntity::getStartTime).filter(Objects::nonNull).min(Date::compareTo).orElse(null); } if (end == null) { end = exams.stream().map(TmsExamEntity::getSubmitTime).filter(Objects::nonNull).max(Date::compareTo).orElse(null); } vo.setExamStart(fmt(start)); vo.setExamEnd(fmt(end)); vo.setSubmittedCount(exams.size()); vo.setPendingCount((int) exams.stream().filter(x -> GRADE_PENDING.equals(x.getGradeStatus())).count()); return vo; } private BigDecimal resolvePaperTotal(TmsExamEntity exam) { if (exam == null) { return BigDecimal.ZERO; } if (exam.getPaperId() != null) { TmsPaperEntity paper = paperMapper.selectById(exam.getPaperId()); if (paper != null && paper.getTotalScore() != null) { return paper.getTotalScore(); } } return nvl(exam.getTotalScore()); } private List loadItems(String examId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsExamItemEntity::getForeignId, examId).orderByAsc(TmsExamItemEntity::getSortNo); return examItemMapper.selectList(qw); } private TmsExamGradeItemVO toItemVo(TmsExamItemEntity item) { TmsExamGradeItemVO vo = new TmsExamGradeItemVO(); vo.setId(item.getId()); vo.setQuestionType(item.getQuestionType()); vo.setStem(item.getStem()); vo.setOptions(parseOptions(item.getOptionsJson())); vo.setCorrectAnswer(item.getCorrectAnswer()); vo.setUserAnswer(item.getUserAnswer()); vo.setScore(item.getScore()); vo.setGotScore(item.getGotScore()); vo.setIsSubjective(item.getIsSubjective()); vo.setAnalysis(item.getAnalysis()); return vo; } private List parseOptions(String json) { if (StringUtil.isEmpty(json)) { return Collections.emptyList(); } try { List list = OBJECT_MAPPER.readValue(json, new TypeReference>() {}); List options = new ArrayList<>(); for (OptSnap snap : list) { TmsExamGradeOptionVO opt = new TmsExamGradeOptionVO(); opt.setOptionLabel(snap.getOptionLabel()); opt.setOptionContent(snap.getOptionContent()); options.add(opt); } return options; } catch (Exception e) { return Collections.emptyList(); } } private Map loadUserNames(List userIds) { Map map = new HashMap<>(); List ids = userIds == null ? Collections.emptyList() : userIds.stream().filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); if (ids.isEmpty()) { return map; } try { List users = userApi.getUserName(ids); if (users != null) { for (UserEntity user : users) { if (user == null || StringUtil.isEmpty(user.getId())) { continue; } map.put(user.getId(), StringUtil.isNotEmpty(user.getRealName()) ? user.getRealName() : user.getId()); } } } catch (Exception ignored) { // 用户服务不可用时回退 userId } return map; } private String passFlag(BigDecimal score, BigDecimal passScore) { if (passScore == null) { return null; } return nvl(score).compareTo(passScore) >= 0 ? "1" : "0"; } private String fmt(Date date) { return date == null ? null : DateUtil.dateToString(date, FMT); } private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private String trim(String value) { return value == null || "null".equalsIgnoreCase(value) ? "" : value.trim(); } private boolean contains(String text, String kw) { return text != null && text.toLowerCase().contains(kw); } @Data @JsonIgnoreProperties(ignoreUnknown = true) private static class OptSnap { private String optionLabel; private String optionContent; } }