package com.ruoyi.web.service; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.ruoyi.common.utils.bean.BeanUtils; import com.ruoyi.enums.PaperStatus; import com.ruoyi.enums.PaperType; import com.ruoyi.learn.domain.*; import com.ruoyi.learn.mapper.*; import lombok.Data; import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 考卷服务 */ @Service public class PaperService { private final LearnPaperMapper paperMapper; private final LearnPaperQuestionMapper paperQuestionMapper; private final LearnQuestionsMapper questionsMapper; PaperService(LearnPaperMapper paperMapper, LearnPaperQuestionMapper paperQuestionMapper, LearnQuestionsMapper questionsMapper) { this.paperMapper = paperMapper; this.paperQuestionMapper = paperQuestionMapper; this.questionsMapper = questionsMapper; } public void test() { // PaperDef paperDef = new PaperDef(); // paperDef.setFillExclude(true); // paperDef.setKnowIds(Lists.newArrayList(133614L,130166L,130187L)); // List typeDefList= Lists.newArrayList(); // typeDefList.add(new TypeDef("单选题", "单选题", 80, 1)); // typeDefList.add(new TypeDef("判断题", "判断题", 10, 2)); // paperDef.setTypes(typeDefList); // getQuestions(1L, 100L, paperDef); } /** * 加载试卷 * @param paperId * @return */ public PaperVO loadPaper(Long paperId) { PaperVO result = new PaperVO(); LearnPaper learnPaper = paperMapper.selectLearnPaperById(paperId); BeanUtils.copyProperties(learnPaper, result); List questions = questionsMapper.selectQuestionByPaperId(paperId); result.setQuestions(questions.stream().map(t -> { PaperVO.QuestionSeq qs = new PaperVO.QuestionSeq(); BeanUtils.copyProperties(t, qs); return qs; }).collect(Collectors.toList())); return result; } /** * 根据院校专业要求生成模拟试卷 * @return */ public Pair> buildSimulatedPaper(LearnDirectedKnowledge directedKnowledge) { LearnPaper paper = new LearnPaper(); paper.setPaperType(PaperType.Simulated.name()); paper.setRelateId(directedKnowledge.getId()); // 定向ID paper.setYear(directedKnowledge.getYear()); paper.setStatus(PaperStatus.Valid.getVal()); paper.setDirectKey(directedKnowledge.getDirectKey()); TestPaperVO.PaperDef paperDef = JSONObject.parseObject(directedKnowledge.getConditions(), TestPaperVO.PaperDef.class); paperDef.setKnowIds(Stream.of(directedKnowledge.getKnowledges().split(",")).map(Long::valueOf).collect(Collectors.toList())); paperDef.setTypes(JSONArray.parseArray(directedKnowledge.getQuestionTypes(), TestPaperVO.TypeDef.class)); return buildPaper(null, paper, paperDef); } /** * 根据试卷定义生成试卷 * @param studentId * @param paper * @param paperDef * @return */ public Pair> buildPaper(Long studentId, LearnPaper paper, TestPaperVO.PaperDef paperDef) { if(null == studentId){ paperDef.setFillExclude(false); } List pqList = getQuestions(studentId, paperDef); return Pair.of(paper, pqList); } /** * 保存试卷 * @param paper * @param pqList * @return */ public LearnPaper savePaper(LearnPaper paper, List pqList) { paper.setNumber(pqList.size()); paper.setFenshu(0); paperMapper.insertLearnPaper(paper); Long paperId = paper.getId(); pqList.stream().forEach(t -> { t.setPaperId(paperId); paperQuestionMapper.insertLearnPaperQuestion(t); }); return paper; } /** * 动态组卷查题 * @param studentId * @param paperDef * @return */ public List getQuestions(Long studentId, TestPaperVO.PaperDef paperDef) { // 题型分布定义, 知识点列表, 分值定义 // 统计知识点+类型的有效数量 TODO 总量可以缓存 Map knowTypeAssignMap = Maps.newHashMap(); List typeSet = paperDef.getTypes().stream().map(TestPaperVO.TypeDef::getType).collect(Collectors.toList()); Map cond = Maps.newHashMap(); cond.put("studentId", studentId); cond.put("knowIds", paperDef.getKnowIds()); cond.put("types", typeSet); setValue(knowTypeAssignMap, cond, 1); // 填充排除总量 if (paperDef.getFillExclude()) { cond.remove("studentId"); setValue(knowTypeAssignMap, cond, 2); // 按需填充总量 } // 循环补充未做+已做,如果知识点总数不够时才填充其他知识点的 Long lackTotal = paperDef.getTotal(); AtomicLong assignCount = new AtomicLong(0); Map knowTypesMap = Maps.newHashMap(); int typeCount = paperDef.getTypes().size(); for (Long knowId : paperDef.getKnowIds()) { knowTypesMap.put(knowId, typeCount); } Set knowSet = Sets.newHashSet(paperDef.getKnowIds()); Long minKnowTypeCount = Long.MAX_VALUE; do { Integer knowCount = knowSet.size(); knowSet.clear(); for (Long knowId : paperDef.getKnowIds()) { typeCount = knowTypesMap.get(knowId); if (0 == typeCount) { continue; } Long avgKnowTypeCount = Math.min(minKnowTypeCount, lackTotal / knowCount / typeCount); if(avgKnowTypeCount == 0 && lackTotal > 0) { avgKnowTypeCount = 1L; } typeCount = 0; for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, avgKnowTypeCount, paperDef.getFillExclude(), assignCount); if (tmpMinKnowTypeCount > 0) { minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount); knowSet.add(knowId); typeCount++; } } knowTypesMap.put(knowId, typeCount); } lackTotal = paperDef.getTotal() - assignCount.get(); if (lackTotal <= 0 || knowSet.isEmpty()) { break; } } while (true); // 知识点已经分配,准备题型分配 LearnQuestions qCond = new LearnQuestions(); Random random = new Random(); List pqList = Lists.newArrayList(); Set existQuestionIdSet = Sets.newHashSet(); for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { for (Long knowId : paperDef.getKnowIds()) { String key = knowId + "_" + typeDef.getType(); KnowTypeAssign ktc = knowTypeAssignMap.get(key); qCond.setKnowledgeId(ktc.getKnowId()); qCond.setQtpye(ktc.getType()); if(ktc.exclAssign > 0){ qCond.setId(studentId); qCond.setNumber(ktc.assign > 500 ? (long) random.nextInt(ktc.exclAssign.intValue() - 500) : 0L); List questions = questionsMapper.selectQuestionsForPaper(qCond); addRandomList(questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore(), existQuestionIdSet, pqList); } if(ktc.assign > 0L) { qCond.setId(null); qCond.setNumber(ktc.assign > 500 ? (long) random.nextInt(ktc.assign.intValue() - 500) : 0L); List questions = questionsMapper.selectQuestionsForPaper(qCond); addRandomList(questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore(), existQuestionIdSet, pqList); } } } return pqList; } private void addRandomList(List questions, Random random, Long totalCount, Long count, Integer score, Set existQuestionIdSet, List pqList) { while(count > 0 && questions.size() > 0) { LearnQuestions q = questions.size() > 1 ? questions.remove(random.nextInt(questions.size() - 1)) : questions.remove(0); if(existQuestionIdSet.add(q.getId())) { LearnPaperQuestion pq = new LearnPaperQuestion(); pq.setSeq(pqList.size() + 1); pq.setScore(score); pq.setQuestionId(q.getId()); pqList.add(pq); count--; if(pqList.size() == totalCount) { break; } } } } /** * 给指定类型分配 * @param knowId * @param qtype * @param knowTypeAssignMap * @param knowTypeCount * @param fillExclude * @param assignCount */ private Long assignKnownCount(Long knowId, String qtype, Map knowTypeAssignMap, Long knowTypeCount, Boolean fillExclude, AtomicLong assignCount) { String key = knowId + "_" + qtype; KnowTypeAssign knowTypeAssign = knowTypeAssignMap.get(key); long lackCount; if (knowTypeAssign.exclCount > 0) { lackCount = knowTypeCount - knowTypeAssign.exclCount; if (lackCount <= 0) { // 足量 assignCount.getAndAdd(knowTypeCount); knowTypeAssign.exclCount -= knowTypeCount; knowTypeAssign.exclAssign += knowTypeCount; } else { // 不足时且还有时全转 assignCount.getAndAdd(knowTypeAssign.exclCount); knowTypeAssign.exclAssign += knowTypeAssign.exclCount; knowTypeAssign.exclCount = 0L; } } else { lackCount = knowTypeCount; } long lack = 0; if (lackCount > 0) { // 差额优先补充已做过的 if(fillExclude && knowTypeAssign.total > 0) { lack = lackCount - knowTypeAssign.total; if (lack <= 0) { // 足量 assignCount.getAndAdd(lackCount); knowTypeAssign.total -= lackCount; knowTypeAssign.assign += lackCount; } else { // 不足时全转 assignCount.getAndAdd(knowTypeAssign.total); knowTypeAssign.assign += knowTypeAssign.total; knowTypeAssign.total = 0L; } } else { lack = lackCount; } } if(lackCount < 0) { return -lackCount; } else if(lack < 0) { return -lack; } return 0L; } /** * 合并 排除总量和总量 * @param knowTypeAssignMap * @param cond * @param index 1 free 2 total */ private void setValue(Map knowTypeAssignMap, Map cond, Integer index) { for (LearnQuestions q : questionsMapper.statByKnowledgeType(cond)) { String key = q.getKnowledgeId() + "_" + q.getQtpye(); KnowTypeAssign knowTypeAssign = knowTypeAssignMap.get(key); if (null == knowTypeAssign) { knowTypeAssign = new KnowTypeAssign(); knowTypeAssign.setKnowId(q.getKnowledgeId()); knowTypeAssign.setType(q.getQtpye()); knowTypeAssignMap.put(key, knowTypeAssign); } if (1 == index) { knowTypeAssign.total = 0L; knowTypeAssign.exclAssign = 0L; knowTypeAssign.assign = 0L; knowTypeAssign.exclCount = q.getNumber(); } else { knowTypeAssign.total = q.getNumber() - knowTypeAssign.exclCount; } } } @Data public static class KnowTypeAssign { Long knowId; String type; Long exclAssign; // 分配个数 Long assign; // 全量分配个数 Long exclCount; // 排除总数 Long total; // 全量总数 } }