diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java index 467f966..4e2d135 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java @@ -188,7 +188,8 @@ public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDo scoringPoints.add(spDTO); } detailDTO.setScoringPoints(scoringPoints); - }else if (shortAnswerNum > 0){ + } else if (Objects.equals(parsed.getQuestionType(), QuestionTypeEnum.SHORT_ANSWER.getValue())) { + // 仅当前题为简答题且缺少得分点时才报错,其他题型无得分点属正常 throw new BusinessErrorException("导入简答题没有得分点,请补充"); } diff --git a/src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java b/src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java index 0f93789..5a018b6 100644 --- a/src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java +++ b/src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java @@ -26,7 +26,13 @@ public class WordQuestionParser { private static final Pattern OPTION_PATTERN = Pattern.compile("^[A-Z][..\\u3001::、]\\s*"); private static final Pattern ANSWER_PATTERN = Pattern.compile("^答案[::\\u3001]\\s*(.*)"); private static final Pattern ANALYSIS_PATTERN = Pattern.compile("^解析[::\\u3001]\\s*(.*)"); + // 难度行:如"难度:中",不属于题干内容,忽略 + private static final Pattern DIFFICULTY_PATTERN = Pattern.compile("^难度[::\\u3001]"); private static final Pattern SCORING_POINT_PATTERN = Pattern.compile("^得分点[::\\u3001]\\s*(.*)"); + // 单条得分点行:得分点N. 内容(N 后可为英文点/中文点/顿号) + private static final Pattern SCORING_POINT_ITEM_PATTERN = Pattern.compile("^得分点[0-9]+[..、]"); + // 题型标签:[单选题] [多选题] [判断题] [简答题] + private static final Pattern TYPE_TAG_PATTERN = Pattern.compile("\\[[^\\]]*题\\]"); private final MinIoUtils minIoUtils; @@ -36,6 +42,8 @@ public class WordQuestionParser { private int imageCounter = 0; /** 是否正在解析得分点段落(得分点触发后,后续段落归入得分点内容) */ private boolean parsingScoringPoints = false; + /** 是否正在解析简答题参考答案正文(得分点结束后到下一道真题之间的内容) */ + private boolean inReferenceAnswer = false; public WordQuestionParser(MinIoUtils minIoUtils) { this.minIoUtils = minIoUtils; @@ -81,9 +89,15 @@ public class WordQuestionParser { // 题号匹配 if (QUESTION_PATTERN.matcher(text).find()) { - // 如果正在解析得分点,且题干极短,说明是得分点子项(如"1、两块纳米黑板") - // 归入最后一个得分点而非新题目 - if (parsingScoringPoints && currentQuestion != null + // 带题型标签(如 40、[简答题])的一定是新题目,即使处于得分点/参考答案状态 + boolean isRealQuestion = TYPE_TAG_PATTERN.matcher(text).find(); + // 参考答案正文中的编号行(如"1、LED高清显示系统")不是新题,归入当前简答题参考答案 + if (!isRealQuestion && inReferenceAnswer && currentQuestion != null) { + appendReferenceAnswer(currentQuestion, text); + return; + } + // 处于得分点解析状态,且不是真题目,归入得分点子项 + if (!isRealQuestion && parsingScoringPoints && currentQuestion != null && !currentQuestion.scoringPoints.isEmpty()) { accumulateScoringPointContent(text, currentQuestion); return; @@ -93,6 +107,7 @@ public class WordQuestionParser { } currentQuestion = new ParsedQuestion(); parsingScoringPoints = false; // 新题目,重置得分点状态 + inReferenceAnswer = false; // 新题目,重置参考答案状态 // 去掉题号 currentQuestion.stem = QUESTION_PATTERN.matcher(text).replaceFirst("").trim(); currentQuestion.questionType = detectQuestionType(currentQuestion.stem); @@ -109,6 +124,12 @@ public class WordQuestionParser { return; } + // 正在解析参考答案正文:全部归入 referenceAnswer,不污染题干/选项 + if (inReferenceAnswer) { + appendReferenceAnswer(currentQuestion, text); + return; + } + // 答案行 Matcher answerMatcher = ANSWER_PATTERN.matcher(text); if (answerMatcher.matches()) { @@ -126,6 +147,11 @@ public class WordQuestionParser { return; } + // 难度行(如"难度:中"):元数据,不属于题干,直接忽略 + if (DIFFICULTY_PATTERN.matcher(text).find()) { + return; + } + // 得分点行(格式:得分点:得分点1. xxx 得分点2. xxx) Matcher scoringPointMatcher = SCORING_POINT_PATTERN.matcher(text); if (scoringPointMatcher.matches()) { @@ -250,27 +276,32 @@ public class WordQuestionParser { /** * 将后续段落内容归入得分点(得分点跨段落时使用) + * 只接受以"得分点N."开头的行;遇到不以"得分点"开头的普通正文 + * (参考答案)则结束得分点收集,避免参考答案污染得分点 */ private void accumulateScoringPointContent(String text, ParsedQuestion question) { - // 检查是否是新得分点标题(如:得分点3. xxx) - Matcher m = Pattern.compile("得分点([0-9]+)[..、]\\s*(.*)").matcher(text); - if (m.matches()) { - // 新得分点:通过 parseScoringPoints 处理(它会处理保存逻辑) - // 这里把单条"得分点N. 内容"包装后调用 + // 新得分点行(如:得分点3. xxx):交由 parseScoringPoints 处理(它会追加到已有列表) + if (SCORING_POINT_ITEM_PATTERN.matcher(text).find()) { parseScoringPoints(text, question); return; } - // 普通内容:追加到最后一个得分点 - if (!question.scoringPoints.isEmpty()) { - ParsedScoringPoint last = question.scoringPoints.get(question.scoringPoints.size() - 1); - if (last.getContent() != null && !last.getContent().isEmpty()) { - last.setContent(last.getContent() + " " + text); - } else { - last.setContent(text); - } + // 不是得分点行:视为参考答案正文开始,结束得分点收集,不再污染得分点 + parsingScoringPoints = false; + inReferenceAnswer = true; // 进入参考答案正文,后续编号行不再误判为新题 + appendReferenceAnswer(question, text); + } + + /** 追加参考答案正文(单独存放,不污染题干) */ + private void appendReferenceAnswer(ParsedQuestion question, String text) { + if (question == null) { + return; + } + if (question.referenceAnswer == null || question.referenceAnswer.isEmpty()) { + question.referenceAnswer = text; + } else { + question.referenceAnswer += "\n" + text; } - // 如果还没有得分点(异常情况),忽略 } /** @@ -339,6 +370,8 @@ public class WordQuestionParser { List options = new ArrayList<>(); String answer; String analysis; + /** 简答题参考答案正文(得分点之后、下一道题之前的内容) */ + String referenceAnswer; /** 简答题得分点列表 */ List scoringPoints = new ArrayList<>(); } diff --git a/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java b/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java index 98a923f..7f21e82 100644 --- a/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java +++ b/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java @@ -83,15 +83,28 @@ public class QuestionInventoryDomainServiceImpl implements QuestionInventoryDoma List clusterList = clusterMapper.selectList( new LambdaQueryWrapper().eq(TaskKnowledgeClusterEntity::getTaskId, taskId)); + // 多选簇专用摊薄系数:u * (多选题数 / 多选簇总数) + // 说明:原逻辑复用了按“全部知识点数”摊薄的 watermark/targetLine(multiplier=总题数/知识点数), + // 在知识点数远大于题数(如 1000+ 知识点)时该系数 << 1,导致每个多选簇目标库存被压到个位数, + // 无法覆盖 u 名考生的多选题实际消耗。多选消耗只应按“多选簇”摊薄,而非全部知识点。 + long mcClusterCount = clusterList.stream() + .filter(c -> c.getClusterSize() != null && c.getClusterSize() >= 2) + .count(); + double mcClusterMultiplier = mcClusterCount > 0 + ? task.getMultipleChoiceNum() * 1.0 / mcClusterCount + : 1.0; + int mcWatermark = (int) Math.ceil(getUnpassedCount(task) * mcClusterMultiplier); + int mcTargetLine = (int) Math.ceil(2 * getUnpassedCount(task) * mcClusterMultiplier); + for (TaskKnowledgeClusterEntity cluster : clusterList) { if (cluster.getClusterSize() < 2) { continue; } int w = cluster.getClusterSize(); - // 簇水位线: 1.0 * U * W - int clusterWatermark = (int) Math.ceil(watermark * w * questionTypeMap.get(QuestionTypeEnum.MULTIPLE_CHOICE.name())); - // 簇目标线: 2 * U * W - int clusterTargetLine = (int) Math.ceil(targetLine * w * questionTypeMap.get(QuestionTypeEnum.MULTIPLE_CHOICE.name())); + // 簇水位线: mcWatermark * W(分子已是多选题数,占比已隐含,不再乘题型占比二次摊薄) + int clusterWatermark = (int) Math.ceil(mcWatermark * w); + // 簇目标线: mcTargetLine * W + int clusterTargetLine = (int) Math.ceil(mcTargetLine * w); checkClusterProduce(cluster.getId(), clusterWatermark, clusterTargetLine); } diff --git a/src/main/java/com/project/task/controller/TaskController.java b/src/main/java/com/project/task/controller/TaskController.java index edd6ba4..ff95fcd 100644 --- a/src/main/java/com/project/task/controller/TaskController.java +++ b/src/main/java/com/project/task/controller/TaskController.java @@ -50,6 +50,12 @@ public class TaskController { return questionInventoryDomainService.checkAndReplenishBoolean(taskId); } + @GetMapping("/doCheckAndReplenish") + public Result doCheckAndReplenish(Long taskId) throws Exception { + questionInventoryDomainService.checkAndReplenish(taskId); + return Result.success("ok"); + } + @PostMapping("/batchDelete") @OperationLog(module = "考试任务")