package com.ruoyi.web.service; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.github.pagehelper.PageHelper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.bean.BeanUtils; import com.ruoyi.enums.PaperStatus; import com.ruoyi.enums.PaperType; import com.ruoyi.enums.QuestionType; import com.ruoyi.learn.domain.*; import com.ruoyi.learn.mapper.*; import com.ruoyi.learn.service.ILearnQuestionsService; import com.ruoyi.syzy.domain.BBusiWishUniversities; import com.ruoyi.syzy.mapper.BBusiWishUniversitiesMapper; import com.ruoyi.syzy.service.IBBusiWishUniversitiesService; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 考卷服务 */ @Service @Slf4j public class PaperService { Set chooseTypes = Sets.newHashSet(QuestionType.Judgment, QuestionType.Multiple, QuestionType.Single); private final LearnPaperMapper paperMapper; private final LearnPaperQuestionMapper paperQuestionMapper; private final LearnQuestionsMapper questionsMapper; private final ILearnQuestionsService learnQuestionsService; private final LearnDirectedKnowledgeMapper learnDirectedKnowledgeMapper; private final IBBusiWishUniversitiesService wishUniversitiesService; private final BBusiWishUniversitiesMapper bBusiWishUniversitiesMapper; private final LearnKnowledgeTreeMapper learnKnowledgeTreeMapper; private final LearnKnowledgeCourseMapper learnKnowledgeCourseMapper; PaperService(LearnPaperMapper paperMapper, LearnPaperQuestionMapper paperQuestionMapper, LearnQuestionsMapper questionsMapper, ILearnQuestionsService learnQuestionsService, LearnDirectedKnowledgeMapper learnDirectedKnowledgeMapper, IBBusiWishUniversitiesService wishUniversitiesService, BBusiWishUniversitiesMapper bBusiWishUniversitiesMapper, LearnKnowledgeTreeMapper learnKnowledgeTreeMapper, LearnKnowledgeCourseMapper learnKnowledgeCourseMapper) { this.paperMapper = paperMapper; this.paperQuestionMapper = paperQuestionMapper; this.questionsMapper = questionsMapper; this.learnQuestionsService = learnQuestionsService; this.learnDirectedKnowledgeMapper = learnDirectedKnowledgeMapper; this.wishUniversitiesService = wishUniversitiesService; this.bBusiWishUniversitiesMapper = bBusiWishUniversitiesMapper; this.learnKnowledgeTreeMapper = learnKnowledgeTreeMapper; this.learnKnowledgeCourseMapper = learnKnowledgeCourseMapper; // buildAllPapers(2); // buildSimulatedPaperForUniversity(20950L, 11L, 78L, 2); // buildSimulatedPaperForUniversity(20962L, 11L, 156L, 2); // test2(); // testCulture(); // corrQuestions(); } public static String convertImg2Html(String text) { String urlPattern = "!\\[\\]\\((https?://[\\w.-]+\\.[a-zA-Z]{2,}[/\\w.-]*\\??[/\\w.=&%-]*)\\)"; Pattern pattern = Pattern.compile(urlPattern); Matcher matcher = pattern.matcher(text); StringBuffer result = new StringBuffer(); while (matcher.find()) { String url = matcher.group(1); String replacement = String.format("", url); matcher.appendReplacement(result, replacement); } matcher.appendTail(result); return result.toString(); } public void corrQuestions() { LearnQuestions cond = new LearnQuestions(); Long id = 0L; Integer pageSize = 0; List questionsList; String newValue; do { cond.setId(id); PageHelper.startPage(1, pageSize, null); questionsList = questionsMapper.selectLearnQuestionsList(cond); for(LearnQuestions q : questionsList) { boolean updated = false; LearnQuestions up = new LearnQuestions(); up.setId(q.getId()); if(!q.getTitle().equals((newValue = convertImg2Html(q.getTitle())))) { up.setTitle(newValue); updated = true; } if(StringUtils.isNotBlank(q.getTitle0())) { if(!q.getTitle0().equals((newValue = convertImg2Html(q.getTitle0())))) { up.setTitle0(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getAnswer2())) { if(!q.getAnswer2().equals(newValue = convertImg2Html(q.getAnswer2()))) { up.setAnswer2(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getParse())) { if(!q.getParse().equals(newValue = convertImg2Html(q.getParse()))) { up.setParse(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionA())) { if(!q.getOptionA().equals(newValue = convertImg2Html(q.getOptionA()))) { up.setOptionA(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionB())) { if(!q.getOptionB().equals(newValue = convertImg2Html(q.getOptionB()))) { up.setOptionB(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionC())) { if(!q.getOptionC().equals(newValue = convertImg2Html(q.getOptionC()))) { up.setOptionC(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionD())) { if(!q.getOptionD().equals(newValue = convertImg2Html(q.getOptionD()))) { up.setOptionD(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionE())) { if(!q.getOptionE().equals(newValue = convertImg2Html(q.getOptionE()))) { up.setOptionE(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionF())) { if(!q.getOptionF().equals(newValue = convertImg2Html(q.getOptionF()))) { up.setOptionF(newValue); updated = true; } } if(StringUtils.isNotBlank(q.getOptionG())) { if(!q.getOptionG().equals(newValue = convertImg2Html(q.getOptionG()))) { up.setOptionG(newValue); updated = true; } } if(updated) { up.setIsUpdate(1L); questionsMapper.updateLearnQuestions(up); } id = q.getId(); } } while(questionsList.size() != pageSize); } public void buildAllPapers(Integer seq) { LearnDirectedKnowledge dkCond = new LearnDirectedKnowledge(); dkCond.setYear(2025); Map> universityDirectedMap = learnDirectedKnowledgeMapper.selectLearnDirectedKnowledgeList(dkCond).stream().collect(Collectors.groupingBy(LearnDirectedKnowledge::getUniversityId)); Map uCond = new HashMap(); uCond.put("ids", universityDirectedMap.keySet()); Map universityMap = bBusiWishUniversitiesMapper.selectBBusiWishUniversitiesListSimpleByIds(uCond).stream().collect(Collectors.toMap(BBusiWishUniversities::getId, Function.identity())); for(Long universityId : universityDirectedMap.keySet()) { BBusiWishUniversities universities = universityMap.get(universityId); if(null == universities) { continue; } for(LearnDirectedKnowledge dk : universityDirectedMap.get(universityId)) { buildSimulatedPaperForKnowledge(11L, seq, universities, dk); } } } public void testCulture() { LearnPaper paper = new LearnPaper(); // TestPaperVO.PaperDef2 paperDef = new TestPaperVO.PaperDef2("", "[{\"score\":40,\"knowledges\":[2001,2002,2003,2004,2005,2006,2007,2010,2013],\"types\":{\"judgment\":\"0\",\"single\":\"10/4\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}},{\"score\":20,\"knowledges\":[2012],\"types\":{\"judgment\":\"0\",\"single\":\"5/4\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}},{\"score\":40,\"knowledges\":[2011],\"types\":{\"judgment\":\"0\",\"single\":\"8/5\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}}]"); // paperDef.setFillExclude(false); // paper.setSubjectId(1010L); // paper.setRelateId(1010L); // 定向ID // paper.setPaperName("语文"); // TestPaperVO.PaperDef2 paperDef = new TestPaperVO.PaperDef2("", "[{\"score\":80,\"knowledges\":[1005,1006,1007,1008,1009,1010,1011,1013,1015,2044,2043],\"types\":{\"judgment\":\"10/2\",\"single\":\"15/4\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}},{\"score\":10,\"knowledges\":[2043],\"types\":{\"judgment\":\"0\",\"single\":\"1/10\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}},{\"score\":10,\"knowledges\":[2043],\"types\":{\"judgment\":\"0\",\"single\":\"1/10\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}}]"); // paperDef.setFillExclude(false); // paper.setSubjectId(1011L); // paper.setRelateId(1011L); // paper.setPaperName("数学"); TestPaperVO.PaperDef2 paperDef = new TestPaperVO.PaperDef2("", "[{\"score\":60,\"knowledges\":[1022,1193,1023],\"types\":{\"judgment\":\"0\",\"single\":\"15/4\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}},{\"score\":20,\"knowledges\":[1022,1193,1023],\"types\":{\"judgment\":\"0\",\"single\":\"5/4\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}},{\"score\":20,\"knowledges\":[1022,1193,1023],\"types\":{\"judgment\":\"0\",\"single\":\"10/2\",\"multiple\":\"0\",\"subjective\":\"0\",\"fill\":\"0\",\"essay\":\"0\",\"short\":\"0\"}}]"); paperDef.setFillExclude(false); paper.setSubjectId(1012L); paper.setRelateId(1012L); paper.setPaperName("英语"); paper.setPaperType(PaperType.Simulated.name()); paper.setYear(2025); paper.setPaperSource(1); paper.setDirectKey(""); paper.setStatus(PaperStatus.Valid.getVal()); paper.setNumber(paperDef.getTotal()); paper.setFenshu(paperDef.getScore().intValue()); AnswerSheet.PaperCond info = new AnswerSheet.PaperCond(); info.setScore(paper.getFenshu()); info.setTime(60 * 60); info.setTypes(paperDef.getTypes().stream().map(t -> new AnswerSheet.PaperCondType(t.getType().getTitle(), t.getCount(), t.getScore())).collect(Collectors.toList())); paper.setPaperInfo(JSONObject.toJSONString(info)); try { Pair> paperResult = buildPaper2(null, paper, paperDef); savePaper(paperResult.getKey(), paperResult.getValue()); return; } catch(Exception e) { log.error(e.getMessage()); } } public void test2() { TestPaperVO.PaperDef paperDef = new TestPaperVO.PaperDef(); paperDef.setFillExclude(true); paperDef.setKnowIds(Lists.newArrayList(1016L,1101L,1091L, 1103L)); List typeDefList= Lists.newArrayList(); typeDefList.add(new TestPaperVO.TypeDef("判断题", "判断题", 20, 2)); typeDefList.add(new TestPaperVO.TypeDef("单选题", "单选题", 10, 1)); typeDefList.add(new TestPaperVO.TypeDef("多选题", "多选题", 10, 2)); paperDef.setTypes(typeDefList); paperDef.setTotal(40L); List questionList = getQuestionsByType(null, paperDef); return; } 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); } public PaperVO loadPaper(Long paperId) { LearnPaper learnPaper = paperMapper.selectLearnPaperById(paperId); PaperVO result = new PaperVO(); BeanUtils.copyProperties(learnPaper, result); result.setQuestions(loadPaperQuestions(paperId)); return result; } /** * 加载试卷 * @param paperId * @return */ public List loadPaperQuestions(Long paperId) { List questions = questionsMapper.selectQuestionByPaperId(paperId); Map gropuMap = Maps.newHashMap(); List paperQuestionList = Lists.newArrayList(); for(LearnQuestions lqs : questions) { PaperVO.QuestionSeq qs = new PaperVO.QuestionSeq(); BeanUtils.copyProperties(lqs, qs, "options", "parse", "answer1", "answer2"); QuestionType qt = QuestionType.of(lqs.getQtpye()); qs.setTypeId(qt.getVal()); qs.setType(qt.getTitle()); if (!chooseTypes.contains(qt)) { qs.setOptions(Lists.newArrayList("会", "不会")); } else { qs.setOptions(StringUtils.getOptions(lqs.getOptionA(), lqs.getOptionB(), lqs.getOptionC(), lqs.getOptionD(), lqs.getOptionE(), lqs.getOptionF(), lqs.getOptionG())); } if(StringUtils.isNotBlank(lqs.getTitle0())) { // 大题 PaperVO.QuestionSeq qg = gropuMap.get(lqs.getTitle0()); if(qg == null) { qg = new PaperVO.QuestionSeq(); qg.setTypeId(99); qg.setSubQuestions(Lists.newArrayList()); qg.setTitle(lqs.getTitle0()); gropuMap.put(lqs.getTitle0(), qg); paperQuestionList.add(qg); } qg.getSubQuestions().add(qs); } else { paperQuestionList.add(qs); } } return paperQuestionList; } public List loadPaperQuestionAnswers(Long userId, Long paperId, Map answerMap, boolean withParse) { List questions = questionsMapper.selectQuestionByPaperId(paperId); learnQuestionsService.fillCollectInfo(userId, questions); Map gropuMap = Maps.newHashMap(); List paperQuestionList = Lists.newArrayList(); LearnAnswer answer; for(LearnQuestions lqs : questions) { PaperVO.QuestionAnswer qs = new PaperVO.QuestionAnswer(); QuestionType qt = QuestionType.of(lqs.getQtpye()); if(withParse || !chooseTypes.contains(qt)) { BeanUtils.copyProperties(lqs, qs, "title", "options"); } else { BeanUtils.copyProperties(lqs, qs, "title", "options", "parse", "answer1", "answer2"); } qs.setTotalScore(lqs.getScore()); qs.setScore(null); qs.setTypeId(qt.getVal()); qs.setIsFavorite(lqs.isCollect()); if(null != answerMap && null != (answer = answerMap.get(lqs.getId()))) { qs.setAnswerId(answer.getAnswerId()); qs.setAnswers(Arrays.asList(answer.getAnswer().split(","))); qs.setState(answer.getState()); qs.setIsMark(answer.getMark()); qs.setIsNotKnow(answer.getNotKnow()); qs.setScore(answer.getScore()); } if(StringUtils.isNotBlank(lqs.getTitle0())) { // 大题 PaperVO.QuestionAnswer qg = gropuMap.get(lqs.getTitle0()); if(qg == null) { qg = new PaperVO.QuestionAnswer(); qg.setTypeId(99); qg.setSubQuestions(Lists.newArrayList()); qg.setTitle(lqs.getTitle0()); gropuMap.put(lqs.getTitle0(), qg); paperQuestionList.add(qg); } qg.getSubQuestions().add(qs); } else { paperQuestionList.add(qs); } } return paperQuestionList; } public int buildSimulatedPaperForUniversity(Long universityId, Long subjectId, Long directedId, Integer seq) { LearnDirectedKnowledge dkCond = new LearnDirectedKnowledge(); dkCond.setUniversityId(universityId); List directedKnowledgeList = learnDirectedKnowledgeMapper.selectLearnDirectedKnowledgeList(dkCond); BBusiWishUniversities universities = wishUniversitiesService.selectBBusiWishUniversitiesById(universityId); for(LearnDirectedKnowledge dk : directedKnowledgeList) { if(null == directedId || directedId.equals(dk.getId())) { buildSimulatedPaperForKnowledge(subjectId, seq, universities, dk); } } return 0; } /** * 根据院校专业要求生成模拟试卷 * @return */ public int buildSimulatedPaperForKnowledge(Long subjectId, Integer seq, BBusiWishUniversities universities, LearnDirectedKnowledge dk) { if(StringUtils.isBlank(dk.getConditions())) { return 0; } TestPaperVO.PaperDef2 paperDef = new TestPaperVO.PaperDef2(dk.getKnowledges(), dk.getConditions()); paperDef.setFillExclude(false); LearnPaper paper = new LearnPaper(); paper.setSubjectId(subjectId); paper.setPaperType(PaperType.Simulated.name()); paper.setRelateId(dk.getId()); // 定向ID paper.setYear(dk.getYear()); paper.setPaperSource(seq); paper.setStatus(1); if(CollectionUtils.isNotEmpty(paperMapper.selectLearnPaperList(paper))) { log.warn("已经生成: {}:{}", dk.getId(), seq); return 0; } paper.setPaperName(StringUtils.isNotBlank(dk.getDirectKey()) ? universities.getName() + "(" + dk.getDirectKey() + ")" : universities.getName()); paper.setDirectKey(universities.getId() + "_" + dk.getExamineeTypes() + "_" + dk.getDirectKey()); paper.setStatus(PaperStatus.Valid.getVal()); paper.setNumber(paperDef.getTotal()); paper.setFenshu(paperDef.getScore().intValue()); AnswerSheet.PaperCond info = new AnswerSheet.PaperCond(); info.setScore(paper.getFenshu()); info.setTime(dk.getTime() * 60); info.setTypes(paperDef.getTypes().stream().map(t -> new AnswerSheet.PaperCondType(t.getType().getTitle(), t.getCount(), t.getScore())).collect(Collectors.toList())); paper.setPaperInfo(JSONObject.toJSONString(info)); try { Pair> paperResult = buildPaper2(null, paper, paperDef); savePaper(paperResult.getKey(), paperResult.getValue()); return 0; } catch(Exception e) { log.error(e.getMessage()); } return 0; } /** * 原版本,新的是2 * @param directedKnowledge * @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); } public Pair> buildPaper2(Long studentId, LearnPaper paper, TestPaperVO.PaperDef2 paperDef) { paperDef.setFillExclude(null != studentId); List pqList = getQuestions2(studentId, paperDef); return Pair.of(paper, pqList); } /** * 根据试卷定义生成试卷 * @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()); paperMapper.insertLearnPaper(paper); Long paperId = paper.getId(); pqList.stream().forEach(t -> t.setPaperId(paperId)); paperQuestionMapper.batchInsert(pqList); return paper; } /** * 按类型,知识点平均分配组卷 * @param studentId * @param paperDef * @return */ public List getQuestionsByType(Long studentId, TestPaperVO.PaperDef paperDef) { // 统计知识点+类型的有效数量 TODO 总量可以缓存 Map knowTypeAssignMap = buildKnowTypeAssignMap(studentId, paperDef); assignTypeFirstWithCount(paperDef, knowTypeAssignMap); // 知识优先,类型可变 return getQuestions(studentId, paperDef, knowTypeAssignMap); } public List getQuestions2(Long studentId, TestPaperVO.PaperDef2 paperDef) { List pqList = Lists.newArrayList(); Set existQuestionIdSet = Sets.newHashSet(); for(TestPaperVO.KnowledgeTypeDef2 ktd : paperDef.getKnowTypes()) { Map> ktSubMap = Maps.newHashMap(); Integer maxSubCount = 6; List newKnownList = Lists.newArrayList(); List tailKnownList = Lists.newArrayList(); List ktList = learnKnowledgeTreeMapper.selectLearnKnowledgeTreeByParentIds(ktd.getKnowledges()); for(LearnKnowledgeTree kt : ktList) { List subIdList = ktSubMap.computeIfAbsent(kt.getPid(), k -> Lists.newArrayList()); subIdList.add(kt.getId()); } for(Long knownId : ktd.getKnowledges()) { List subList = ktSubMap.get(knownId); if(null == subList) { newKnownList.add(knownId); } else if(subList.size() > maxSubCount) { newKnownList.addAll(subList.subList(0, maxSubCount)); tailKnownList.addAll(subList.subList(maxSubCount, subList.size())); } else { newKnownList.addAll(subList); } } newKnownList.addAll(tailKnownList); ktd.setKnowledges(newKnownList); Map knowTypeAssignMap = buildKnowTypeAssignMap(studentId, "0", ktd.getTypes().stream().map(t -> t.getType().getTitle()).collect(Collectors.toList()), newKnownList, paperDef.getFillExclude()); assignTypeFirst(paperDef.getFillExclude(), ktd, knowTypeAssignMap); pqList.addAll(getQuestions2(studentId, ktd.getCount(), pqList.size(), newKnownList, ktd.getTypes(), knowTypeAssignMap, existQuestionIdSet)); } reSort(pqList); return pqList; } public List getQuestionsByRandom(Long studentId, Integer total, Collection knowledgeIds, List types) { Map knowTypeAssignMap = buildKnowTypeAssignMap(studentId, "1", types, knowledgeIds, false); List knowTypeAssignList = Lists.newArrayList(knowTypeAssignMap.values()); List pqList = Lists.newArrayList(); Set existQuestionIdSet = Sets.newHashSet(); Random random = new Random(); Map> typeQuestionMap = Maps.newHashMap(); LearnQuestions qCond = new LearnQuestions(); do { if(knowTypeAssignList.isEmpty()) { break; } int idx = random.nextInt(knowTypeAssignList.size()); KnowTypeAssign knowTypeAssign = knowTypeAssignList.get(idx); if(knowTypeAssign.getExclCount() > knowTypeAssign.getExclAssign()) { List questions = typeQuestionMap.get(knowTypeAssign.getType()); if(null == questions) { qCond.setKnowledgeId(knowTypeAssign.getKnowId()); qCond.setQtpye(knowTypeAssign.getType()); qCond.setId(studentId); qCond.setNumber(knowTypeAssign.exclCount > 500 ? (long) random.nextInt(knowTypeAssign.exclCount.intValue() - 500) : 0L); qCond.setIsSubType("1"); questions = questionsMapper.selectQuestionsForPaper(qCond); typeQuestionMap.put(knowTypeAssign.getType(), questions); } if(!questions.isEmpty()) { int oldSize = pqList.size(); addRandomList(knowTypeAssign.getKnowId(), QuestionType.of(knowTypeAssign.getType()), questions, random, total.longValue(), 1L, 1.0, existQuestionIdSet, 1, pqList); if(oldSize != pqList.size()) { knowTypeAssign.exclAssign++; } } } else { knowTypeAssignList.remove(idx); } } while(pqList.size() < total); reSort(pqList); return pqList; } /** * // diff(type), paperId(parentId), seq(type), questionId(id) * @param pqList */ private void reSort(List pqList) { Collections.sort(pqList, new Comparator() { @Override public int compare(LearnPaperQuestion o1, LearnPaperQuestion o2) { int iRet; if(0 != (iRet = o1.getDiff().compareTo(o2.getDiff()))) { return iRet; } if(0 != (iRet = o1.getPaperId().compareTo(o2.getPaperId()))) { return iRet; } if(0 != (iRet = o1.getSeq().compareTo(o2.getSeq()))) { return iRet; } return o1.getQuestionId().compareTo(o2.getQuestionId()); } }); Integer[] idx = {1}; pqList.forEach(t -> { t.setSeq(idx[0]++); t.setPaperId(null); t.setDiff(null); }); } /** * 按知识点,题型平均分配组卷 * @param studentId * @param paperDef * @return */ public List getQuestions(Long studentId, TestPaperVO.PaperDef paperDef) { // 题型分布定义, 知识点列表, 分值定义 // 统计知识点+类型的有效数量 TODO 总量可以缓存 Map knowTypeAssignMap = buildKnowTypeAssignMap(studentId, paperDef); assignKnowFirst(paperDef, knowTypeAssignMap); // 知识优先,类型可变 return getQuestions(studentId, paperDef, knowTypeAssignMap); } private void assignTypeFirstWithCount(TestPaperVO.PaperDef paperDef, Map knowTypeAssignMap) { AtomicLong assignCount = new AtomicLong(0); Map knownAdjMap = Maps.newHashMap(); Map> typeKnowIdMap = Maps.newHashMap(); Map typeFillCntMap = Maps.newHashMap(); Map typeMinCntMap = Maps.newHashMap(); // 所有知识点,正常补全一次,哪个知识点差多少先记录下 for(TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { Long avgKnowTypeCount = typeDef.getCount().longValue() / paperDef.getKnowIds().size(); if(avgKnowTypeCount.equals(0L)) { avgKnowTypeCount = 1L; } assignCount.set(0); for(Long knowId : paperDef.getKnowIds()) { Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, avgKnowTypeCount, paperDef.getFillExclude(), assignCount); if (tmpMinKnowTypeCount > 0) { // 记录最小数 Long minKnowTypeCount = typeMinCntMap.get(typeDef.getType()); typeMinCntMap.put(typeDef.getType(), null == minKnowTypeCount ? tmpMinKnowTypeCount : Math.min(minKnowTypeCount, tmpMinKnowTypeCount)); // 记录有效知识点 Set knownIdSet = typeKnowIdMap.get(typeDef.getType()); if(null == knownIdSet) { typeKnowIdMap.put(typeDef.getType(), Sets.newHashSet(knowId)); } else { knownIdSet.add(knowId); } } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型 // 记录知识点差额 Long lackCount = knownAdjMap.get(knowId); knownAdjMap.put(knowId, (null != lackCount ? lackCount : 0L) - tmpMinKnowTypeCount); } } typeFillCntMap.put(typeDef.getType(), assignCount.get()); } knownAdjMap.clear(); // 优先补充差的知识点 for(TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { assignCount.set(typeFillCntMap.get(typeDef.getType())); Long lackTotal = typeDef.getCount().longValue(); Long minCount = typeMinCntMap.get(typeDef.getType()); Set knownIdSet = typeKnowIdMap.get(typeDef.getType()); do { if(lackTotal <= assignCount.get()) { break; } Long needCount = lackTotal - assignCount.get(); Long avgKnowTypeCount = knownIdSet.size() > 0 ? Math.min(minCount, needCount / knownIdSet.size()) : 1; if (avgKnowTypeCount <= 0L) { avgKnowTypeCount = 1L; } minCount = Long.MAX_VALUE; Set validIdSet = Sets.newHashSet(knownIdSet); knownIdSet.clear(); for(Long knowId : paperDef.getKnowIds()) { if(knownIdSet.size() > 0 && !validIdSet.contains(knowId)) { continue; } // Long lastLack = null; knownAdjMap.remove(knowId); null != lastLack ? lastLack + avgKnowTypeCount : Long knowTypeCount = avgKnowTypeCount; if (knowTypeCount > 0) { // Long oldCnt = assignCount.get(); Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount); // Long fillCnt = oldCnt - assignCount.get(); // if(fillCnt < 0) { // Long oldFill = knownAdjMap.get(knowId); // knownAdjMap.put(knowId, null == oldFill ? fillCnt : oldFill - fillCnt); // } if (tmpMinKnowTypeCount > 0) { minCount = Math.min(minCount, tmpMinKnowTypeCount); knownIdSet.add(knowId); } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型 // knownAdjMap.put(knowId, -tmpMinKnowTypeCount); } } else if(knowTypeCount < 0) { // knownAdjMap.put(knowId, knowTypeCount); } if(lackTotal <= assignCount.get()) { break; } } } while(true); } } private void assignTypeFirstWithCount2(TestPaperVO.PaperDef paperDef, Map knowTypeAssignMap) { Map> typeKnownIdsMap = knowTypeAssignMap.values().stream().collect(Collectors.groupingBy(KnowTypeAssign::getType, Collectors.mapping(KnowTypeAssign::getKnowId, Collectors.toSet()))); Map knownAdjMap = Maps.newHashMap(); AtomicLong assignCount = new AtomicLong(0); Integer needCount = paperDef.getTypes().size(); Set doneSet = Sets.newHashSet(); do { for(TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { // 每个类型,让所有知识先平均补充,补充的由后面填充 Long minKnowTypeCount = Long.MAX_VALUE; // 首先按平均数量填充 assignCount.set(0L); Long lackTotal = typeDef.getCount().longValue(); Set knownIdSet; if(knownAdjMap.size() > 0) { // 先补充之前的同类型 knownIdSet = Sets.newHashSet(); for(Long knowId : paperDef.getKnowIds()) { Long lastLack = knownAdjMap.remove(knowId); Long knowTypeCount = null != lastLack ? lastLack : 0; if(knowTypeCount >= 0) { Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount); if (tmpMinKnowTypeCount > 0) { minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount); knownIdSet.add(knowId); } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型 knownAdjMap.put(knowId, -tmpMinKnowTypeCount); } } else if(knowTypeCount < 0) { knownAdjMap.put(knowId, knowTypeCount); } } } else { knownIdSet = typeKnownIdsMap.get(typeDef.getType()); } if(lackTotal <= assignCount.get()) { doneSet.add(typeDef.getType()); continue; } Long avgKnowTypeCount = (lackTotal - assignCount.get()) / paperDef.getKnowIds().size(); if (avgKnowTypeCount <= 0L) { avgKnowTypeCount = 1L; } for(Long knowId : paperDef.getKnowIds()) { Long lastLack = knownAdjMap.remove(knowId); Long knowTypeCount = null != lastLack ? lastLack + avgKnowTypeCount : avgKnowTypeCount; if(knowTypeCount > 0) { Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount); if (tmpMinKnowTypeCount > 0) { minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount); knownIdSet.add(knowId); } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型 knownAdjMap.put(knowId, -tmpMinKnowTypeCount); } } else if(knowTypeCount < 0) { knownAdjMap.put(knowId, knowTypeCount); } } // 然后进行多级补充,多退少补 do { if(lackTotal <= assignCount.get()) { doneSet.add(typeDef.getType()); break; } avgKnowTypeCount = Math.min(minKnowTypeCount, (lackTotal - assignCount.get()) / knownIdSet.size()); if (avgKnowTypeCount <= 0L) { avgKnowTypeCount = 1L; } minKnowTypeCount = Long.MAX_VALUE; Set validIdSet = Sets.newHashSet(knownIdSet); knownIdSet.clear(); for(Long knowId : paperDef.getKnowIds()) { if(!validIdSet.contains(knowId)) { continue; } Long lastLack = knownAdjMap.remove(knowId); Long knowTypeCount = null != lastLack ? lastLack + avgKnowTypeCount : avgKnowTypeCount; if (knowTypeCount > 0) { Long oldCnt = assignCount.get(); Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, knowTypeCount, paperDef.getFillExclude(), assignCount); Long fillCnt = oldCnt - assignCount.get(); if(fillCnt < 0) { Long oldFill = knownAdjMap.get(knowId); knownAdjMap.put(knowId, null == oldFill ? fillCnt : oldFill - fillCnt); } if (tmpMinKnowTypeCount > 0) { minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount); knownIdSet.add(knowId); } else if(tmpMinKnowTypeCount < 0) { // 差的个数移到下一类型,补充的类型也移到下一类型 knownAdjMap.put(knowId, -tmpMinKnowTypeCount); } } else if(knowTypeCount < 0) { knownAdjMap.put(knowId, knowTypeCount); } } } while(true); } } while(doneSet.size() < needCount); } private void assignTypeFirst(Boolean fillExclude, TestPaperVO.KnowledgeTypeDef2 knowledgeTypeDef, Map knowTypeAssignMap) { Map> typeKnownIdsMap = knowTypeAssignMap.values().stream().collect(Collectors.groupingBy(KnowTypeAssign::getType, Collectors.mapping(KnowTypeAssign::getKnowId, Collectors.toSet()))); AtomicLong assignCount = new AtomicLong(0); for(TestPaperVO.TypeDef2 typeDef : knowledgeTypeDef.getTypes()) { // 每个类型,让所有知识先平均补充,补充的由后面填充 Long minKnowTypeCount = Long.MAX_VALUE; Long lackTotal = typeDef.getCount().longValue(); String typeTitle = typeDef.getType().getTitle(); Set knownIdSet = typeKnownIdsMap.get(typeTitle); if(CollectionUtils.isEmpty(knownIdSet)) { log.error("Invalid knowledge type: " + typeTitle); return; } assignCount.set(0L); do { Long avgKnowTypeCount = (lackTotal - assignCount.get()) / knownIdSet.size(); if (avgKnowTypeCount <= 0L) { avgKnowTypeCount = 1L; } knownIdSet.clear(); for(Long knowId : knowledgeTypeDef.getKnowledges()) { if(avgKnowTypeCount > 0) { Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeTitle, knowTypeAssignMap, avgKnowTypeCount, fillExclude, assignCount); if (tmpMinKnowTypeCount > 0) { minKnowTypeCount = Math.min(minKnowTypeCount, tmpMinKnowTypeCount); knownIdSet.add(knowId); } if(assignCount.get() >= lackTotal) { break; } } } } while(lackTotal != assignCount.get() && knownIdSet.size() > 0); if(lackTotal < assignCount.get()) { throw new RuntimeException("题数不足: " + typeTitle + "差" + (lackTotal - assignCount.get())); } } } private void assignKnowFirst(TestPaperVO.PaperDef paperDef, Map knowTypeAssignMap) { // 循环补充未做+已做,如果知识点总数不够时才填充其他知识点的 Long lackTotal = paperDef.getTotal(); AtomicLong assignCount = new AtomicLong(0); Map knowTypesMap = Maps.newHashMap(); int typeCount = paperDef.getTypes().size(); Set knowSet = knowTypeAssignMap.values().stream().map(KnowTypeAssign::getKnowId).collect(Collectors.toSet()); for (Long knowId : paperDef.getKnowIds()) { knowTypesMap.put(knowId, knowSet.contains(knowId) ? typeCount : 0); } 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); } /** * 根据计划的分配数生成题关系 * @param studentId * @param paperDef * @param knowTypeAssignMap * @return */ public List getQuestions(Long studentId, TestPaperVO.PaperDef paperDef, Map knowTypeAssignMap) { // 知识点已经分配,准备题型分配 LearnQuestions qCond = new LearnQuestions(); Random random = new Random(); List pqList = Lists.newArrayList(); Set existQuestionIdSet = Sets.newHashSet(); int total = paperDef.getTotal().intValue(); for (TestPaperVO.TypeDef typeDef : paperDef.getTypes()) { for (Long knowId : paperDef.getKnowIds()) { String key = knowId + "_" + typeDef.getType(); KnowTypeAssign ktc = knowTypeAssignMap.get(key); if(null == ktc) { continue; } qCond.setKnowledgeId(ktc.getKnowId()); qCond.setQtpye(ktc.getType()); qCond.setIsSubType("0"); QuestionType qt = QuestionType.of(ktc.getType()); if(ktc.exclAssign > 0){ qCond.setId(studentId); qCond.setNumber(ktc.exclAssign > 500 ? (long) random.nextInt(ktc.exclAssign.intValue() - 500) : 0L); List questions = questionsMapper.selectQuestionsForPaper(qCond); ktc.exclAssign = addRandomList(knowId, qt, questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore().doubleValue(), existQuestionIdSet, 1, pqList); if(pqList.size() == total) { break; } } 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); ktc.assign = addRandomList(knowId, qt, questions, random, paperDef.getTotal(), ktc.assign, typeDef.getScore().doubleValue(), existQuestionIdSet, 1, pqList); if(pqList.size() == total) { break; } } } if(pqList.size() == total) { break; } } if(CollectionUtils.isEmpty(pqList)) { throw new RuntimeException("题数不足"); } reSort(pqList); return pqList; } public List getQuestions2(Long studentId, Integer total, Integer seqId, Collection knownIds, List types, Map knowTypeAssignMap, Set existQuestionIdSet) { // 知识点已经分配,准备题型分配 LearnQuestions qCond = new LearnQuestions(); Random random = new Random(); List pqList = Lists.newArrayList(); for (TestPaperVO.TypeDef2 typeDef : types) { String typeTitle = typeDef.getType().getTitle(); for (Long knowId : knownIds) { String key = knowId + "_" + typeTitle; KnowTypeAssign ktc = knowTypeAssignMap.get(key); if(null == ktc) { continue; } qCond.setKnowledgeId(ktc.getKnowId()); qCond.setQtpye(typeTitle); qCond.setIsSubType("0"); if(ktc.exclAssign > 0){ qCond.setId(studentId); qCond.setNumber(ktc.exclAssign > 500 ? (long) random.nextInt(ktc.exclAssign.intValue() - 500) : 0L); List questions = questionsMapper.selectQuestionsForPaper(qCond); ktc.exclAssign = addRandomList(knowId, typeDef.getType(), questions, random, total.longValue(), ktc.exclAssign, typeDef.getScore(), existQuestionIdSet, seqId, pqList); if(pqList.size() == total) { break; } } 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); ktc.assign = addRandomList(knowId, typeDef.getType(), questions, random, total.longValue(), ktc.assign, typeDef.getScore(), existQuestionIdSet, seqId, pqList); if(pqList.size() == total) { break; } } } if(pqList.size() == total) { break; } } if(pqList.size() < total) { throw new RuntimeException("题数不足 " + types.stream().map( t -> t.getType().getTitle()).collect(Collectors.joining(",")) + ":" + StringUtils.join(knownIds, ",")); } return pqList; } /** * 初始化当前用户卷情况 * @param studentId * @param paperDef * @return */ private Map buildKnowTypeAssignMap(Long studentId, TestPaperVO.PaperDef paperDef) { return buildKnowTypeAssignMap(studentId, "0", paperDef.getTypes().stream().map(TestPaperVO.TypeDef::getType).collect(Collectors.toList()), paperDef.getKnowIds(), paperDef.getFillExclude()); } /** * 初始化当前用户知识点的情况 * @param studentId * @param types * @param knownIds * @param fillExclude * @return */ private Map buildKnowTypeAssignMap(Long studentId, String isSubType, List types, Collection knownIds, Boolean fillExclude) { Map knowTypeAssignMap = Maps.newHashMap(); Map cond = Maps.newHashMap(); cond.put("studentId", studentId); cond.put("knowIds", knownIds); cond.put("types", types); cond.put("isSubType", isSubType); setValue(knowTypeAssignMap, cond, 1); // 填充排除后总量 if (null != studentId && fillExclude) { cond.remove("studentId"); setValue(knowTypeAssignMap, cond, 2); // 按需填充已做总量 } return knowTypeAssignMap; } /** * 随机从 knowId 题池中提取需要个数的题 * @param knowId 知识点 * @param type 类型 * @param questions 题池 * @param random 随机数 * @param totalCount 总题数 * @param count 本池分配数 * @param score 题分 * @param existQuestionIdSet 不能使用的题 * @param pqList 卷题关系 diff(type),paperId(parentId),seq,questionId(id) */ private Long addRandomList(Long knowId, QuestionType type, List questions, Random random, Long totalCount, Long count, Double score, Set existQuestionIdSet, Integer baseSeq, List pqList) { while(count > 0L && !questions.isEmpty()) { LearnQuestions q = questions.size() > 1 ? questions.remove(random.nextInt(questions.size() - 1)) : questions.remove(0); if(existQuestionIdSet.add(q.getId())) { if("1".equals(q.getIsSubType())) { LearnQuestions subCond = new LearnQuestions(); subCond.setKnowId(q.getId()); for(LearnQuestions sq : questionsMapper.selectLearnQuestionsList(subCond)) { LearnPaperQuestion pq = new LearnPaperQuestion(); pq.setSeq(baseSeq + pqList.size()); pq.setKnowledgeId(knowId); pq.setScore(score); pq.setQuestionId(sq.getId()); pq.setType(type.getTitle()); pq.setDiff(type.getVal()); pq.setPaperId(q.getId()); pqList.add(pq); count--; } } else { LearnPaperQuestion pq = new LearnPaperQuestion(); pq.setSeq(baseSeq + pqList.size()); pq.setKnowledgeId(knowId); pq.setScore(score); pq.setQuestionId(q.getId()); pq.setType(type.getTitle()); pq.setDiff(type.getVal()); pq.setPaperId(0L); pqList.add(pq); count--; } if(pqList.size() >= totalCount) { break; } } } return count; } /** * 给指定类型分配 * @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 (null != knowTypeAssign && 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 && null != knowTypeAssign && 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 -lack; } /** * 合并 未用总量和已有总量, 分配数置0 * @param knowTypeAssignMap key=knowId +"_" + qtype * @param cond * @param index 1 free 2 used */ 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()); knowTypeAssign.total = 0L; knowTypeAssign.exclAssign = 0L; knowTypeAssign.assign = 0L; knowTypeAssign.exclCount = 0L; knowTypeAssignMap.put(key, knowTypeAssign); } if (1 == index) { 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; // 已用总量 } }