package jnpf.tmsService.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.exception.DataException; import jnpf.tmsEntity.TmsQuestionBankEntity; import jnpf.tmsEntity.TmsQuestionEntity; import jnpf.tmsEntity.TmsQuestionOptionEntity; import jnpf.tmsEntity.TmsSelfTestEntity; import jnpf.tmsEntity.TmsSelfTestItemEntity; import jnpf.tmsEntity.selftest.TmsSelfTestOptionVO; import jnpf.tmsEntity.selftest.TmsSelfTestPaperVO; import jnpf.tmsEntity.selftest.TmsSelfTestQuery; import jnpf.tmsEntity.selftest.TmsSelfTestQuestionVO; import jnpf.tmsEntity.selftest.TmsSelfTestRecordVO; import jnpf.tmsEntity.selftest.TmsSelfTestStartForm; import jnpf.tmsEntity.selftest.TmsSelfTestSubmitForm; import jnpf.tmsEntity.selftest.TmsSelfTestSubmitResultVO; import jnpf.tmsEntity.selftest.TmsSelfTestTypeSetting; import jnpf.tmsMapper.TmsQuestionBankMapper; import jnpf.tmsMapper.TmsQuestionMapper; import jnpf.tmsMapper.TmsQuestionOptionMapper; import jnpf.tmsMapper.TmsSelfTestItemMapper; import jnpf.tmsMapper.TmsSelfTestMapper; import jnpf.tmsService.TmsSelfTestService; 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; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; 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; @Service @RequiredArgsConstructor public class TmsSelfTestServiceImpl implements TmsSelfTestService { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** 自我检测 UI 难度 → 试题 difficulty 值 */ private static final Map> DIFFICULTY_MAP = new HashMap<>(); static { DIFFICULTY_MAP.put("veryEasy", Arrays.asList("easy", "veryEasy")); DIFFICULTY_MAP.put("easier", Arrays.asList("easy", "easier")); DIFFICULTY_MAP.put("normal", Collections.singletonList("normal")); DIFFICULTY_MAP.put("harder", Arrays.asList("hard", "harder")); DIFFICULTY_MAP.put("veryHard", Arrays.asList("hard", "veryHard")); } private final TmsSelfTestMapper selfTestMapper; private final TmsSelfTestItemMapper selfTestItemMapper; private final TmsQuestionMapper questionMapper; private final TmsQuestionOptionMapper optionMapper; private final TmsQuestionBankMapper bankMapper; @Override @Transactional(rollbackFor = Exception.class) public TmsSelfTestPaperVO start(TmsSelfTestStartForm form) { if (form == null || StringUtil.isEmpty(form.getBankId())) { throw new DataException("请选择题库"); } TmsQuestionBankEntity bank = bankMapper.selectById(form.getBankId()); if (bank == null) { throw new DataException("题库不存在或已删除"); } int singleCnt = countOf(form.getSingle()); int multiCnt = countOf(form.getMulti()); int judgeCnt = countOf(form.getJudge()); if (singleCnt + multiCnt + judgeCnt <= 0) { throw new DataException("请至少设置一种题型的抽题数量"); } List drawn = new ArrayList<>(); drawn.addAll(drawQuestions(form.getBankId(), "single", form.getSingle())); drawn.addAll(drawQuestions(form.getBankId(), "multi", form.getMulti())); drawn.addAll(drawQuestions(form.getBankId(), "judge", form.getJudge())); if (drawn.isEmpty()) { throw new DataException("当前条件下没有可抽题目,请调整数量或难度"); } Map> optionMap = loadOptions( drawn.stream().map(TmsQuestionEntity::getId).collect(Collectors.toList())); String userId = UserProvider.getLoginUserId(); Date now = new Date(); String paperId = RandomUtil.uuId(); TmsSelfTestEntity entity = new TmsSelfTestEntity(); entity.setId(paperId); entity.setBankId(bank.getId()); entity.setBankName(StringUtil.isNotEmpty(form.getBankName()) ? form.getBankName() : bank.getBankName()); entity.setUserId(userId); entity.setSingleCount(singleCnt); entity.setMultiCount(multiCnt); entity.setJudgeCount(judgeCnt); entity.setSingleDifficulty(diffOf(form.getSingle())); entity.setMultiDifficulty(diffOf(form.getMulti())); entity.setJudgeDifficulty(diffOf(form.getJudge())); entity.setTotalCount(drawn.size()); entity.setStartTime(now); entity.setTestStatus("doing"); entity.setCreatorUserId(userId); entity.setCreatorTime(now); selfTestMapper.insert(entity); List questions = new ArrayList<>(drawn.size()); int sort = 0; for (TmsQuestionEntity q : drawn) { List opts = optionMap.getOrDefault(q.getId(), Collections.emptyList()); List optionVos = toOptionVos(opts); String correct = opts.stream() .filter(o -> "1".equals(o.getIsCorrect())) .map(TmsQuestionOptionEntity::getOptionLabel) .sorted() .collect(Collectors.joining(",")); TmsSelfTestItemEntity item = new TmsSelfTestItemEntity(); item.setId(RandomUtil.uuId()); item.setForeignId(paperId); item.setQuestionId(q.getId()); item.setQuestionNo(q.getQuestionNo()); item.setQuestionType(q.getQuestionType()); item.setDifficulty(q.getDifficulty()); item.setStem(q.getStem()); item.setOptionsJson(toJson(optionVos)); item.setCorrectAnswer(correct); item.setSortNo(++sort); item.setCreatorUserId(userId); item.setCreatorTime(now); selfTestItemMapper.insert(item); TmsSelfTestQuestionVO vo = new TmsSelfTestQuestionVO(); vo.setId(q.getId()); vo.setQuestionNo(q.getQuestionNo()); vo.setQuestionType(q.getQuestionType()); vo.setDifficulty(q.getDifficulty()); vo.setStem(q.getStem()); vo.setOptions(optionVos); questions.add(vo); } TmsSelfTestPaperVO paper = new TmsSelfTestPaperVO(); paper.setPaperId(paperId); paper.setBankId(entity.getBankId()); paper.setBankName(entity.getBankName()); paper.setQuestions(questions); return paper; } @Override @Transactional(rollbackFor = Exception.class) public void saveAnswers(String id, TmsSelfTestSubmitForm form) { TmsSelfTestEntity entity = requireDoingPaper(id, "无权暂存该检测"); String userId = UserProvider.getLoginUserId(); List items = listItems(id); Map answers = form != null && form.getAnswers() != null ? form.getAnswers() : Collections.emptyMap(); Date now = new Date(); for (TmsSelfTestItemEntity item : items) { // 仅更新前端已作答的题,未传的保持原值,方便增量暂存 if (!answers.containsKey(item.getQuestionId())) { continue; } String userAnswer = normalizeAnswer(answers.get(item.getQuestionId()), item.getQuestionType()); item.setUserAnswer(userAnswer); item.setIsRight(null); item.setLastModifyUserId(userId); item.setLastModifyTime(now); selfTestItemMapper.updateById(item); } entity.setLastModifyUserId(userId); entity.setLastModifyTime(now); selfTestMapper.updateById(entity); } @Override @Transactional(rollbackFor = Exception.class) public TmsSelfTestSubmitResultVO submit(String id, TmsSelfTestSubmitForm form) { TmsSelfTestEntity entity = requireDoingPaper(id, "无权提交该检测"); String userId = UserProvider.getLoginUserId(); List items = listItems(id); Map answers = form != null && form.getAnswers() != null ? form.getAnswers() : Collections.emptyMap(); Date now = new Date(); int correct = 0; for (TmsSelfTestItemEntity item : items) { String userAnswer = normalizeAnswer(answers.get(item.getQuestionId()), item.getQuestionType()); boolean right = Objects.equals(userAnswer, normalizeCorrect(item.getCorrectAnswer())); item.setUserAnswer(userAnswer); item.setIsRight(right ? "1" : "0"); item.setLastModifyUserId(userId); item.setLastModifyTime(now); selfTestItemMapper.updateById(item); if (right) { correct++; } } int total = items.size(); BigDecimal rate = total <= 0 ? BigDecimal.ZERO : BigDecimal.valueOf(correct * 100.0 / total).setScale(2, RoundingMode.HALF_UP); entity.setCorrectCount(correct); entity.setTotalCount(total); entity.setScoreRate(rate); entity.setSubmitTime(now); entity.setTestStatus("submitted"); entity.setLastModifyUserId(userId); entity.setLastModifyTime(now); selfTestMapper.updateById(entity); TmsSelfTestSubmitResultVO result = new TmsSelfTestSubmitResultVO(); result.setPaperId(id); result.setTotalCount(total); result.setCorrectCount(correct); result.setScoreRate(rate); return result; } private TmsSelfTestEntity requireDoingPaper(String id, String denyMsg) { TmsSelfTestEntity entity = selfTestMapper.selectById(id); if (entity == null) { throw new DataException("检测记录不存在"); } String userId = UserProvider.getLoginUserId(); if (!Objects.equals(userId, entity.getUserId())) { throw new DataException(denyMsg); } if ("submitted".equals(entity.getTestStatus())) { throw new DataException("已交卷,请勿重复操作"); } return entity; } private List listItems(String paperId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsSelfTestItemEntity::getForeignId, paperId); qw.orderByAsc(TmsSelfTestItemEntity::getSortNo); return selfTestItemMapper.selectList(qw); } @Override public List getList(TmsSelfTestQuery query) { String userId = UserProvider.getLoginUserId(); LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsSelfTestEntity::getUserId, userId); if (StringUtil.isNotEmpty(query.getBankId())) { qw.eq(TmsSelfTestEntity::getBankId, query.getBankId()); } if (StringUtil.isNotEmpty(query.getTestStatus())) { qw.eq(TmsSelfTestEntity::getTestStatus, query.getTestStatus()); } if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) { qw.like(TmsSelfTestEntity::getBankName, query.getKeyword().trim()); } if (StringUtil.isNotEmpty(query.getStartTimeBegin())) { Date begin = DateUtil.stringToDate(query.getStartTimeBegin()); if (begin != null) { qw.ge(TmsSelfTestEntity::getStartTime, begin); } } if (StringUtil.isNotEmpty(query.getStartTimeEnd())) { Date end = DateUtil.stringToDate(query.getStartTimeEnd()); if (end != null) { qw.le(TmsSelfTestEntity::getStartTime, end); } } qw.orderByDesc(TmsSelfTestEntity::getCreatorTime); Page page = new Page<>(query.getCurrentPage(), query.getPageSize()); Page result = selfTestMapper.selectPage(page, qw); List list = new ArrayList<>(); for (TmsSelfTestEntity row : result.getRecords()) { TmsSelfTestRecordVO vo = new TmsSelfTestRecordVO(); vo.setId(row.getId()); vo.setBankId(row.getBankId()); vo.setBankName(row.getBankName()); vo.setTotalCount(row.getTotalCount()); vo.setCorrectCount(row.getCorrectCount()); vo.setScoreRate(row.getScoreRate()); vo.setTestStatus(row.getTestStatus()); vo.setStartTime(row.getStartTime() == null ? null : DateUtil.dateToString(row.getStartTime(), "yyyy-MM-dd HH:mm:ss")); vo.setSubmitTime(row.getSubmitTime() == null ? null : DateUtil.dateToString(row.getSubmitTime(), "yyyy-MM-dd HH:mm:ss")); list.add(vo); } query.setData(list, result.getTotal()); return list; } @Override public TmsSelfTestPaperVO getInfo(String id) { TmsSelfTestEntity entity = selfTestMapper.selectById(id); if (entity == null) { throw new DataException("检测记录不存在"); } if (!Objects.equals(UserProvider.getLoginUserId(), entity.getUserId())) { throw new DataException("无权查看该检测"); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsSelfTestItemEntity::getForeignId, id); qw.orderByAsc(TmsSelfTestItemEntity::getSortNo); List items = selfTestItemMapper.selectList(qw); List questions = new ArrayList<>(items.size()); for (TmsSelfTestItemEntity item : items) { TmsSelfTestQuestionVO vo = new TmsSelfTestQuestionVO(); vo.setId(item.getQuestionId()); vo.setQuestionNo(item.getQuestionNo()); vo.setQuestionType(item.getQuestionType()); vo.setDifficulty(item.getDifficulty()); vo.setStem(item.getStem()); vo.setOptions(parseOptions(item.getOptionsJson())); vo.setCorrectAnswer(item.getCorrectAnswer()); vo.setUserAnswer(item.getUserAnswer()); vo.setIsRight(item.getIsRight()); vo.setSortNo(item.getSortNo()); questions.add(vo); } TmsSelfTestPaperVO paper = new TmsSelfTestPaperVO(); paper.setPaperId(entity.getId()); paper.setBankId(entity.getBankId()); paper.setBankName(entity.getBankName()); paper.setTestStatus(entity.getTestStatus()); paper.setTotalCount(entity.getTotalCount()); paper.setCorrectCount(entity.getCorrectCount()); paper.setScoreRate(entity.getScoreRate()); paper.setStartTime(entity.getStartTime() == null ? null : DateUtil.dateToString(entity.getStartTime(), "yyyy-MM-dd HH:mm:ss")); paper.setSubmitTime(entity.getSubmitTime() == null ? null : DateUtil.dateToString(entity.getSubmitTime(), "yyyy-MM-dd HH:mm:ss")); paper.setQuestions(questions); return paper; } private List drawQuestions(String bankId, String type, TmsSelfTestTypeSetting setting) { int count = countOf(setting); if (count <= 0) { return Collections.emptyList(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsQuestionEntity::getBankId, bankId); qw.eq(TmsQuestionEntity::getQuestionType, type); qw.eq(TmsQuestionEntity::getBizStatus, "open"); List diffs = resolveDifficulties(diffOf(setting)); if (!diffs.isEmpty()) { qw.in(TmsQuestionEntity::getDifficulty, diffs); } List pool = questionMapper.selectList(qw); if (pool.isEmpty()) { return Collections.emptyList(); } Collections.shuffle(pool); if (pool.size() <= count) { return pool; } return new ArrayList<>(pool.subList(0, count)); } private List resolveDifficulties(String uiDifficulty) { if (StringUtil.isEmpty(uiDifficulty)) { return Collections.emptyList(); } return DIFFICULTY_MAP.getOrDefault(uiDifficulty, Collections.singletonList(uiDifficulty)); } 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); List all = optionMapper.selectList(qw); return all.stream().collect(Collectors.groupingBy(TmsQuestionOptionEntity::getForeignId)); } private List toOptionVos(List opts) { List list = new ArrayList<>(opts.size()); for (TmsQuestionOptionEntity opt : opts) { TmsSelfTestOptionVO vo = new TmsSelfTestOptionVO(); vo.setOptionLabel(opt.getOptionLabel()); vo.setOptionContent(opt.getOptionContent()); vo.setIsCorrect(opt.getIsCorrect()); list.add(vo); } return list; } private String toJson(Object obj) { try { return OBJECT_MAPPER.writeValueAsString(obj); } catch (Exception e) { throw new DataException("选项序列化失败"); } } private List parseOptions(String json) { if (StringUtil.isEmpty(json)) { return Collections.emptyList(); } try { return OBJECT_MAPPER.readValue(json, new TypeReference>() { }); } catch (Exception e) { return Collections.emptyList(); } } private int countOf(TmsSelfTestTypeSetting setting) { if (setting == null || setting.getCount() == null) { return 0; } return Math.max(0, setting.getCount()); } private String diffOf(TmsSelfTestTypeSetting setting) { return setting == null ? null : setting.getDifficulty(); } /** 归一化用户答案:多选排序后逗号拼接 */ private String normalizeAnswer(Object raw, String questionType) { if (raw == null) { return ""; } if ("multi".equals(questionType)) { Set labels = new LinkedHashSet<>(); if (raw instanceof List) { for (Object o : (List) raw) { if (o != null && StringUtil.isNotEmpty(String.valueOf(o))) { labels.add(String.valueOf(o).trim()); } } } else { String s = String.valueOf(raw).trim(); if (StringUtil.isNotEmpty(s)) { for (String part : s.split("[,,]")) { if (StringUtil.isNotEmpty(part.trim())) { 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(",")); } }