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<TmsExamGradeSessionVO> getSessionList(TmsExamGradeQuery query) {
|
List<TmsExamEntity> exams = loadGradeExams(null);
|
Map<String, List<TmsExamEntity>> byPaper = groupByPaper(exams);
|
Map<String, TmsPaperEntity> papers = loadPapers(new ArrayList<>(byPaper.keySet()));
|
|
List<TmsExamGradeSessionVO> all = new ArrayList<>();
|
for (Map.Entry<String, List<TmsExamEntity>> 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<TmsExamGradeSessionVO> 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<TmsExamEntity> exams = loadGradeExams(sessionId);
|
if (exams.isEmpty()) {
|
throw new DataException("阅卷场次不存在");
|
}
|
return toSession(sessionId, paperMapper.selectById(sessionId), exams);
|
}
|
|
@Override
|
public List<TmsExamGradeExamVO> getExamList(String sessionId) {
|
if (StringUtil.isEmpty(sessionId)) {
|
return Collections.emptyList();
|
}
|
List<TmsExamEntity> exams = loadGradeExams(sessionId);
|
exams.sort(Comparator.comparing(TmsExamEntity::getSubmitTime, Comparator.nullsLast(Date::compareTo)).reversed());
|
Map<String, String> names = loadUserNames(exams.stream().map(TmsExamEntity::getUserId).collect(Collectors.toList()));
|
List<TmsExamGradeExamVO> 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<TmsExamItemEntity> items = loadItems(exam.getId());
|
Map<String, String> 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<TmsExamItemEntity> items = loadItems(exam.getId());
|
Map<String, BigDecimal> 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<TmsExamEntity> 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<TmsExamEntity> loadGradeExams(String paperId) {
|
LambdaQueryWrapper<TmsExamEntity> 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<String, List<TmsExamEntity>> groupByPaper(List<TmsExamEntity> exams) {
|
Map<String, List<TmsExamEntity>> 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<String, TmsPaperEntity> loadPapers(List<String> ids) {
|
if (ids == null || ids.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<TmsPaperEntity> list = paperMapper.selectBatchIds(ids);
|
Map<String, TmsPaperEntity> map = new HashMap<>();
|
for (TmsPaperEntity paper : list) {
|
map.put(paper.getId(), paper);
|
}
|
return map;
|
}
|
|
private TmsExamGradeSessionVO toSession(String paperId, TmsPaperEntity paper, List<TmsExamEntity> 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<TmsExamItemEntity> loadItems(String examId) {
|
LambdaQueryWrapper<TmsExamItemEntity> 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<TmsExamGradeOptionVO> parseOptions(String json) {
|
if (StringUtil.isEmpty(json)) {
|
return Collections.emptyList();
|
}
|
try {
|
List<OptSnap> list = OBJECT_MAPPER.readValue(json, new TypeReference<List<OptSnap>>() {});
|
List<TmsExamGradeOptionVO> 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<String, String> loadUserNames(List<String> userIds) {
|
Map<String, String> map = new HashMap<>();
|
List<String> ids = userIds == null ? Collections.emptyList()
|
: userIds.stream().filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList());
|
if (ids.isEmpty()) {
|
return map;
|
}
|
try {
|
List<UserEntity> 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;
|
}
|
}
|