|
@@ -0,0 +1,332 @@
|
|
|
|
|
+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.AllArgsConstructor;
|
|
|
|
|
+import lombok.Data;
|
|
|
|
|
+import lombok.NoArgsConstructor;
|
|
|
|
|
+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<TypeDef> 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<LearnQuestions> 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<LearnPaper, List<LearnPaperQuestion>> 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());
|
|
|
|
|
+
|
|
|
|
|
+ PaperService.PaperDef paperDef = JSONObject.parseObject(directedKnowledge.getConditions(), PaperDef.class);
|
|
|
|
|
+ paperDef.setKnowIds(Stream.of(directedKnowledge.getKnowledges().split(",")).map(Long::valueOf).collect(Collectors.toList()));
|
|
|
|
|
+ paperDef.setTypes(JSONArray.parseArray(directedKnowledge.getQuestionTypes(), TypeDef.class));
|
|
|
|
|
+ return buildPaper(null, paper, paperDef);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据试卷定义生成试卷
|
|
|
|
|
+ * @param studentId
|
|
|
|
|
+ * @param paper
|
|
|
|
|
+ * @param paperDef
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ public Pair<LearnPaper, List<LearnPaperQuestion>> buildPaper(Long studentId, LearnPaper paper, PaperService.PaperDef paperDef) {
|
|
|
|
|
+ if(null == studentId){
|
|
|
|
|
+ paperDef.setFillExclude(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ List<LearnPaperQuestion> pqList = getQuestions(studentId, paperDef);
|
|
|
|
|
+ return Pair.of(paper, pqList);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存试卷
|
|
|
|
|
+ * @param paper
|
|
|
|
|
+ * @param pqList
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ public LearnPaper savePaper(LearnPaper paper, List<LearnPaperQuestion> 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<LearnPaperQuestion> getQuestions(Long studentId, PaperDef paperDef) {
|
|
|
|
|
+ // 题型分布定义, 知识点列表, 分值定义
|
|
|
|
|
+ // 统计知识点+类型的有效数量 TODO 总量可以缓存
|
|
|
|
|
+ Map<String, KnowTypeAssign> knowTypeAssignMap = Maps.newHashMap();
|
|
|
|
|
+ List<String> typeSet = paperDef.getTypes().stream().map(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.fillExclude) {
|
|
|
|
|
+ cond.remove("studentId");
|
|
|
|
|
+ setValue(knowTypeAssignMap, cond, 2); // 按需填充总量
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 循环补充未做+已做,如果知识点总数不够时才填充其他知识点的
|
|
|
|
|
+ Long lackTotal = paperDef.getTotal();
|
|
|
|
|
+ AtomicLong assignCount = new AtomicLong(0);
|
|
|
|
|
+ Map<Long, Integer> knowTypesMap = Maps.newHashMap();
|
|
|
|
|
+ int typeCount = paperDef.getTypes().size();
|
|
|
|
|
+ for (Long knowId : paperDef.getKnowIds()) {
|
|
|
|
|
+ knowTypesMap.put(knowId, typeCount);
|
|
|
|
|
+ }
|
|
|
|
|
+ Set<Long> 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 (TypeDef typeDef : paperDef.getTypes()) {
|
|
|
|
|
+ Long tmpMinKnowTypeCount = assignKnownCount(knowId, typeDef.getType(), knowTypeAssignMap, avgKnowTypeCount, paperDef.fillExclude, 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<LearnPaperQuestion> pqList = Lists.newArrayList();
|
|
|
|
|
+ Set<Long> existQuestionIdSet = Sets.newHashSet();
|
|
|
|
|
+ for (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<LearnQuestions> 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<LearnQuestions> questions = questionsMapper.selectQuestionsForPaper(qCond);
|
|
|
|
|
+ addRandomList(questions, random, paperDef.getTotal(), ktc.exclAssign, typeDef.getScore(), existQuestionIdSet, pqList);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return pqList;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void addRandomList(List<LearnQuestions> questions, Random random, Long totalCount, Long count, Integer score, Set<Long> existQuestionIdSet, List<LearnPaperQuestion> 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<String, KnowTypeAssign> 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<String, KnowTypeAssign> 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; // 全量总数
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Data
|
|
|
|
|
+ @AllArgsConstructor
|
|
|
|
|
+ @NoArgsConstructor
|
|
|
|
|
+ public static class TypeDef {
|
|
|
|
|
+ String title;
|
|
|
|
|
+ String type;
|
|
|
|
|
+ Integer count;
|
|
|
|
|
+ Integer score;
|
|
|
|
|
+ }
|
|
|
|
|
+ @Data
|
|
|
|
|
+ public static class PaperDef {
|
|
|
|
|
+ Long total;
|
|
|
|
|
+ List<Long> knowIds;
|
|
|
|
|
+ Boolean fillExclude; // 不足时是否填充排除的
|
|
|
|
|
+ List<TypeDef> types;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|