Ver Fonte

feat: 非摸底组卷按比例分配题型,避免解答题独占缺口

之前选题逻辑:每种题型保底1题,剩余从混合池按权重填充,
导致解答题(题库数量最多)吃掉所有剩余名额。

新逻辑:
1. 按 question_type_ratio 计算每种题型的目标数量
2. 从各题型池中按目标选题
3. 某题型不足时,缺口按 填空>解答>选择 优先级补到其他题型

摸底测试(type 0)的知识点优先逻辑不受影响。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gwd há 1 mês atrás
pai
commit
3b848dfa0e
1 ficheiros alterados com 81 adições e 17 exclusões
  1. 81 17
      app/Services/LearningAnalyticsService.php

+ 81 - 17
app/Services/LearningAnalyticsService.php

@@ -2231,10 +2231,40 @@ class LearningAnalyticsService
         $useKnowledgePointPriority = ($assembleType === 0); // 摸底测试需要知识点优先
         $kpSelected = []; // 已选知识点记录
 
+        // 【新增】非摸底类型:按比例计算每种题型的目标数量
+        $typeTargets = ['choice' => 0, 'fill' => 0, 'answer' => 0];
+        if (!$useKnowledgePointPriority) {
+            $ratioMap = ['choice' => '选择题', 'fill' => '填空题', 'answer' => '解答题'];
+            $totalRatio = array_sum($questionTypeRatio) ?: 100;
+            $allocated = 0;
+            $fractions = [];
+            foreach ($ratioMap as $type => $ratioKey) {
+                $ratio = $questionTypeRatio[$ratioKey] ?? 0;
+                $exact = $totalQuestions * $ratio / $totalRatio;
+                $typeTargets[$type] = (int) floor($exact);
+                $fractions[$type] = $exact - floor($exact);
+                $allocated += $typeTargets[$type];
+            }
+            // 余数分配给小数部分最大的题型
+            $remainder = $totalQuestions - $allocated;
+            arsort($fractions);
+            foreach ($fractions as $type => $frac) {
+                if ($remainder <= 0) break;
+                $typeTargets[$type]++;
+                $remainder--;
+            }
+
+            Log::info('selectQuestionsByMastery: 题型目标分配', [
+                'total_questions' => $totalQuestions,
+                'ratio' => $questionTypeRatio,
+                'targets' => $typeTargets,
+            ]);
+        }
+
         Log::info('selectQuestionsByMastery: 知识点优先策略', [
             'assemble_type' => $assembleType,
             'use_knowledge_point_priority' => $useKnowledgePointPriority,
-            'note' => $useKnowledgePointPriority ? '摸底测试:需要均衡分配知识点' : '知识点组卷:允许同一知识点选多题'
+            'note' => $useKnowledgePointPriority ? '摸底测试:需要均衡分配知识点' : '按比例分配题型'
         ]);
 
         // 确保每种题型至少选1题
@@ -2295,14 +2325,26 @@ class LearningAnalyticsService
                     'note' => $selectedInThisType > 0 ? '成功选择' : '无可用知识点'
                 ]);
             } else {
-                // 【修复】知识点组卷:随机选择该题型的一道题,避免固定选择第一个导致知识点分布不均
-                $randomIndex = array_rand($questionsByType[$type]);
-                $selectedQuestions[] = $questionsByType[$type][$randomIndex];
-                Log::debug('题型基础分配(随机选择)', [
+                // 非摸底:按目标数量从该题型池中选题
+                $target = $typeTargets[$type] ?? 0;
+                $selectedIds = array_column($selectedQuestions, 'id');
+                $taken = 0;
+                foreach ($questionsByType[$type] as $q) {
+                    if ($taken >= $target) break;
+                    $qid = $q['id'] ?? null;
+                    if ($qid && !in_array($qid, $selectedIds)) {
+                        $selectedQuestions[] = $q;
+                        $selectedIds[] = $qid;
+                        $taken++;
+                    }
+                }
+                $typeTargets[$type . '_actual'] = $taken;
+
+                Log::info('题型按比例分配', [
                     'type' => $type,
-                    'kp' => $questionsByType[$type][$randomIndex]['kp_code'] ?? 'unknown',
-                    'random_index' => $randomIndex,
-                    'total_in_type' => count($questionsByType[$type])
+                    'target' => $target,
+                    'actual' => $taken,
+                    'available' => count($questionsByType[$type]),
                 ]);
             }
         }
@@ -2440,17 +2482,39 @@ class LearningAnalyticsService
                 ]);
             }
         } else {
-            // 知识点组卷:选择未选过的题目(不要求知识点不重复)
-            $selectedIds = array_column($selectedQuestions, 'id');
-            foreach ($allQuestions as $q) {
-                if (count($selectedQuestions) >= $totalQuestions) break;
+            // 非摸底:题型不足时,缺口按优先级补到其他题型
+            $totalSelected = count($selectedQuestions);
+            if ($totalSelected < $totalQuestions) {
+                $deficit = $totalQuestions - $totalSelected;
+                $selectedIds = array_column($selectedQuestions, 'id');
 
-                $qid = $q['id'] ?? null;
-                if ($qid && !in_array($qid, $selectedIds)) {
-                    $selectedQuestions[] = $q;
-                    $selectedIds[] = $qid;
-                    Log::debug('继续选择题目(无知识点限制)', ['kp' => $q['kp_code'] ?? 'unknown', 'id' => $qid]);
+                // 按补充优先级:填空 > 解答 > 选择(避免解答题独占缺口)
+                $supplementOrder = ['fill', 'answer', 'choice'];
+
+                Log::info('题型缺口补充开始', [
+                    'deficit' => $deficit,
+                    'current_count' => $totalSelected,
+                    'target' => $totalQuestions,
+                    'supplement_order' => $supplementOrder,
+                ]);
+
+                foreach ($supplementOrder as $type) {
+                    if ($deficit <= 0) break;
+                    foreach ($questionsByType[$type] as $q) {
+                        if ($deficit <= 0) break;
+                        $qid = $q['id'] ?? null;
+                        if ($qid && !in_array($qid, $selectedIds)) {
+                            $selectedQuestions[] = $q;
+                            $selectedIds[] = $qid;
+                            $deficit--;
+                        }
+                    }
                 }
+
+                Log::info('题型缺口补充完成', [
+                    'final_count' => count($selectedQuestions),
+                    'target' => $totalQuestions,
+                ]);
             }
         }