|
|
@@ -1004,20 +1004,152 @@ class ExamTypeStrategy
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 根据章节ID列表获取知识点代码列表
|
|
|
+ * 根据章节ID列表获取知识点代码列表(均等抽样版本)
|
|
|
+ * 使用"知识点均等抽样"算法,确保覆盖全书章节
|
|
|
+ *
|
|
|
+ * @param array $chapterIds 章节ID列表
|
|
|
+ * @param int $limit 目标题目数量(默认25)
|
|
|
+ * @return array 抽样后的知识点代码列表
|
|
|
*/
|
|
|
private function getKnowledgePointsFromChapters(array $chapterIds, int $limit = 25): array
|
|
|
{
|
|
|
try {
|
|
|
- // 查询 textbook_chapter_knowledge_relation 表
|
|
|
- $kpCodes = DB::table('textbook_chapter_knowledge_relation')
|
|
|
- ->whereIn('catalog_chapter_id', $chapterIds)
|
|
|
+ // 如果章节数较少,使用原有逻辑
|
|
|
+ if (count($chapterIds) <= 5) {
|
|
|
+ Log::info('ExamTypeStrategy: 章节数较少,使用原有逻辑', [
|
|
|
+ 'chapter_count' => count($chapterIds)
|
|
|
+ ]);
|
|
|
+ return $this->getKnowledgePointsFromChaptersLegacy($chapterIds, $limit);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 步骤1: 构建全量有序池 ==========
|
|
|
+ // 查询所有知识点,按章节顺序排列,去重后得到有序数组
|
|
|
+ $allKpData = DB::table('textbook_chapter_knowledge_relation as tckr')
|
|
|
+ ->join('textbook_catalog_nodes as tcn', 'tckr.catalog_chapter_id', '=', 'tcn.id')
|
|
|
+ ->whereIn('tckr.catalog_chapter_id', $chapterIds)
|
|
|
+ ->select('tckr.kp_code', 'tcn.sort_order', 'tcn.id as chapter_id', 'tcn.title as chapter_title')
|
|
|
+ ->orderBy('tcn.sort_order', 'asc')
|
|
|
+ ->orderBy('tckr.kp_code', 'asc')
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ // 去重并构建有序数组
|
|
|
+ $allKpCodes = [];
|
|
|
+ $chapterInfo = [];
|
|
|
+ foreach ($allKpData as $row) {
|
|
|
+ if (!in_array($row->kp_code, $allKpCodes)) {
|
|
|
+ $allKpCodes[] = $row->kp_code;
|
|
|
+ $chapterInfo[$row->kp_code] = [
|
|
|
+ 'chapter_id' => $row->chapter_id,
|
|
|
+ 'chapter_title' => $row->chapter_title,
|
|
|
+ 'sort_order' => $row->sort_order
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $totalKps = count($allKpCodes);
|
|
|
+
|
|
|
+ Log::info('ExamTypeStrategy: 构建全量有序池完成', [
|
|
|
+ 'chapter_count' => count($chapterIds),
|
|
|
+ 'total_knowledge_points' => $totalKps,
|
|
|
+ 'target_questions' => $limit
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // ========== 步骤2: 计算抽样步长 ==========
|
|
|
+ $step = $totalKps > 0 ? $totalKps / $limit : 1;
|
|
|
+ Log::info('ExamTypeStrategy: 计算抽样步长', [
|
|
|
+ 'step' => $step
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // ========== 步骤3: 等距取样 ==========
|
|
|
+ $selectedKps = [];
|
|
|
+ $selectedCount = 0;
|
|
|
+ $currentIndex = 0;
|
|
|
+
|
|
|
+ while ($selectedCount < $limit && $selectedCount < $totalKps) {
|
|
|
+ $index = (int) floor($currentIndex);
|
|
|
+ if ($index < $totalKps && isset($allKpCodes[$index])) {
|
|
|
+ $kpCode = $allKpCodes[$index];
|
|
|
+ if (!in_array($kpCode, $selectedKps)) {
|
|
|
+ $selectedKps[] = $kpCode;
|
|
|
+ $selectedCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $currentIndex += $step;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果数量不足,循环取样
|
|
|
+ if (count($selectedKps) < $limit && $totalKps > 0) {
|
|
|
+ Log::info('ExamTypeStrategy: 第一次抽样不足,进行循环取样', [
|
|
|
+ 'selected_count' => count($selectedKps),
|
|
|
+ 'target_count' => $limit
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $needed = $limit - count($selectedKps);
|
|
|
+ $startIndex = 0;
|
|
|
+
|
|
|
+ for ($i = 0; $i < $needed && count($selectedKps) < $limit; $i++) {
|
|
|
+ $index = ($startIndex + $i) % $totalKps;
|
|
|
+ $kpCode = $allKpCodes[$index];
|
|
|
+ if (!in_array($kpCode, $selectedKps)) {
|
|
|
+ $selectedKps[] = $kpCode;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 步骤4: 记录抽样统计 ==========
|
|
|
+ $chapterDistribution = [];
|
|
|
+ foreach ($selectedKps as $kpCode) {
|
|
|
+ if (isset($chapterInfo[$kpCode])) {
|
|
|
+ $chapterId = $chapterInfo[$kpCode]['chapter_id'];
|
|
|
+ if (!isset($chapterDistribution[$chapterId])) {
|
|
|
+ $chapterDistribution[$chapterId] = [
|
|
|
+ 'count' => 0,
|
|
|
+ 'title' => $chapterInfo[$kpCode]['chapter_title']
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ $chapterDistribution[$chapterId]['count']++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('ExamTypeStrategy: 等距抽样完成', [
|
|
|
+ 'total_kps' => $totalKps,
|
|
|
+ 'selected_kps' => count($selectedKps),
|
|
|
+ 'step' => $step,
|
|
|
+ 'chapter_distribution' => $chapterDistribution,
|
|
|
+ 'sample_kp_codes' => array_slice($selectedKps, 0, 10)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return array_filter($selectedKps);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('ExamTypeStrategy: 均等抽样获取章节知识点失败,使用原有逻辑', [
|
|
|
+ 'chapter_ids' => $chapterIds,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 失败时回退到原有逻辑
|
|
|
+ return $this->getKnowledgePointsFromChaptersLegacy($chapterIds, $limit);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 原有逻辑(保留作为回退方案)
|
|
|
+ */
|
|
|
+ private function getKnowledgePointsFromChaptersLegacy(array $chapterIds, int $limit = 25): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 查询 textbook_chapter_knowledge_relation 表(修复:添加排序避免数据偏向)
|
|
|
+ $kpCodes = DB::table('textbook_chapter_knowledge_relation as tckr')
|
|
|
+ ->leftJoin('textbook_catalog_nodes as tcn', 'tckr.catalog_chapter_id', '=', 'tcn.id')
|
|
|
+ ->whereIn('tckr.catalog_chapter_id', $chapterIds)
|
|
|
+ ->orderBy('tcn.sort_order', 'asc')
|
|
|
+ ->orderBy('tckr.kp_code', 'asc')
|
|
|
->limit($limit)
|
|
|
->distinct()
|
|
|
- ->pluck('kp_code')
|
|
|
+ ->pluck('tckr.kp_code')
|
|
|
->toArray();
|
|
|
|
|
|
- Log::debug('ExamTypeStrategy: 查询章节知识点', [
|
|
|
+ Log::debug('ExamTypeStrategy: 查询章节知识点(原有逻辑)', [
|
|
|
'chapter_count' => count($chapterIds),
|
|
|
'found_kp_count' => count($kpCodes)
|
|
|
]);
|