where('textbook_id', $textbookId) ->where('node_type', 'chapter') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->get(); if ($chapters->isEmpty()) { Log::warning('DiagnosticChapterService: 未找到章节节点', [ 'textbook_id' => $textbookId, ]); return []; } $orderedCodes = []; $seen = []; foreach ($chapters as $chapter) { $sectionIds = TextbookCatalog::query() ->where('parent_id', $chapter->id) ->where('node_type', 'section') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->pluck('id') ->toArray(); if (empty($sectionIds)) { continue; } $kpCodes = TextbookChapterKnowledgeRelation::query() ->whereIn('catalog_chapter_id', $sectionIds) ->pluck('kp_code') ->filter() ->unique() ->values() ->toArray(); $kpCodes = $this->expandWithChildKnowledgePoints($kpCodes); foreach ($kpCodes as $kpCode) { if (isset($seen[$kpCode])) { continue; } $seen[$kpCode] = true; $orderedCodes[] = $kpCode; } } Log::info('DiagnosticChapterService: 获取教材知识点顺序列表', [ 'textbook_id' => $textbookId, 'kp_count' => count($orderedCodes), ]); return $orderedCodes; } public function getInitialChapterKnowledgePoints(int $textbookId): array { $chapter = TextbookCatalog::query() ->where('textbook_id', $textbookId) ->where('node_type', 'chapter') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->first(); if (!$chapter) { Log::warning('DiagnosticChapterService: 未找到章节节点', [ 'textbook_id' => $textbookId, ]); return []; } $sectionIds = TextbookCatalog::query() ->where('parent_id', $chapter->id) ->where('node_type', 'section') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->pluck('id') ->toArray(); if (empty($sectionIds)) { Log::warning('DiagnosticChapterService: 章节下未找到section节点', [ 'textbook_id' => $textbookId, 'chapter_id' => $chapter->id, ]); return []; } $kpCodes = TextbookChapterKnowledgeRelation::query() ->whereIn('catalog_chapter_id', $sectionIds) ->pluck('kp_code') ->filter() ->unique() ->values() ->toArray(); $kpCodes = $this->expandWithChildKnowledgePoints($kpCodes); Log::info('DiagnosticChapterService: 获取首章知识点', [ 'textbook_id' => $textbookId, 'chapter_id' => $chapter->id, 'section_count' => count($sectionIds), 'kp_count' => count($kpCodes), ]); return [ 'chapter_id' => $chapter->id, 'section_ids' => $sectionIds, 'kp_codes' => $kpCodes, ]; } public function getFirstUnmasteredChapterKnowledgePoints(int $textbookId, int $studentId, float $threshold = 0.9): array { $chapters = TextbookCatalog::query() ->where('textbook_id', $textbookId) ->where('node_type', 'chapter') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->get(); if ($chapters->isEmpty()) { Log::warning('DiagnosticChapterService: 未找到章节节点', [ 'textbook_id' => $textbookId, ]); return []; } foreach ($chapters as $chapter) { $sectionIds = TextbookCatalog::query() ->where('parent_id', $chapter->id) ->where('node_type', 'section') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->pluck('id') ->toArray(); if (empty($sectionIds)) { continue; } $kpCodes = TextbookChapterKnowledgeRelation::query() ->whereIn('catalog_chapter_id', $sectionIds) ->pluck('kp_code') ->filter() ->unique() ->values() ->toArray(); $kpCodes = $this->expandWithChildKnowledgePoints($kpCodes); if (empty($kpCodes)) { continue; } $allMastered = StudentKnowledgeMastery::allAtLeast($studentId, $kpCodes, $threshold); Log::info('DiagnosticChapterService: 章节掌握度评估', [ 'student_id' => $studentId, 'textbook_id' => $textbookId, 'chapter_id' => $chapter->id, 'section_count' => count($sectionIds), 'kp_count' => count($kpCodes), 'all_mastered' => $allMastered, 'threshold' => $threshold, ]); if (!$allMastered) { return [ 'chapter_id' => $chapter->id, 'section_ids' => $sectionIds, 'kp_codes' => $kpCodes, 'all_mastered' => $allMastered, ]; } } Log::info('DiagnosticChapterService: 所有章节均达到掌握度阈值', [ 'student_id' => $studentId, 'textbook_id' => $textbookId, 'threshold' => $threshold, ]); return []; } private function expandWithChildKnowledgePoints(array $kpCodes): array { if (empty($kpCodes)) { return []; } $baseCodes = collect($kpCodes)->filter()->values()->all(); $children = KnowledgePoint::query() ->whereIn('parent_kp_code', $baseCodes) ->orderBy('kp_code') ->get(['parent_kp_code', 'kp_code']); $childrenMap = []; foreach ($children as $child) { $childrenMap[$child->parent_kp_code][] = $child->kp_code; } $ordered = []; $seen = []; foreach ($baseCodes as $kpCode) { if (!isset($seen[$kpCode])) { $seen[$kpCode] = true; $ordered[] = $kpCode; } foreach ($childrenMap[$kpCode] ?? [] as $childCode) { if (isset($seen[$childCode])) { continue; } $seen[$childCode] = true; $ordered[] = $childCode; } } return $ordered; } // ========== 以下是新增的方法(不扩展子知识点)========== /** * 获取章节的知识点(不扩展子知识点) * 直接返回 section 绑定的知识点 */ public function getChapterKnowledgePointsSimple(int $chapterId): array { $sectionIds = TextbookCatalog::query() ->where('parent_id', $chapterId) ->where('node_type', 'section') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->pluck('id') ->toArray(); if (empty($sectionIds)) { return [ 'section_ids' => [], 'kp_codes' => [], ]; } $kpCodes = TextbookChapterKnowledgeRelation::query() ->whereIn('catalog_chapter_id', $sectionIds) ->pluck('kp_code') ->filter() ->unique() ->values() ->toArray(); // 不扩展子知识点,直接返回 return [ 'section_ids' => $sectionIds, 'kp_codes' => $kpCodes, ]; } /** * 判断章节是否已经摸底过 */ public function hasChapterDiagnostic(int $studentId, int $chapterId): bool { return \App\Models\Paper::query() ->where('student_id', $studentId) ->where('paper_type', 0) // 摸底类型 ->where('diagnostic_chapter_id', $chapterId) ->exists(); } /** * 获取第一个未摸底的章节 * 用于章节摸底流程 */ public function getFirstUndiagnosedChapter(int $textbookId, int $studentId): ?array { $chapters = TextbookCatalog::query() ->where('textbook_id', $textbookId) ->where('node_type', 'chapter') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->get(); if ($chapters->isEmpty()) { return null; } foreach ($chapters as $chapter) { // 检查是否已摸底 if (!$this->hasChapterDiagnostic($studentId, $chapter->id)) { $chapterData = $this->getChapterKnowledgePointsSimple($chapter->id); // 过滤掉没有题目的知识点 $kpCodesWithQuestions = $this->filterKpCodesWithQuestions($chapterData['kp_codes']); if (empty($kpCodesWithQuestions)) { // 这个章节没有题目,跳过 continue; } Log::info('DiagnosticChapterService: 找到第一个未摸底的章节', [ 'textbook_id' => $textbookId, 'student_id' => $studentId, 'chapter_id' => $chapter->id, 'chapter_name' => $chapter->name ?? '', 'kp_count' => count($kpCodesWithQuestions), ]); return [ 'chapter_id' => $chapter->id, 'chapter_name' => $chapter->name ?? '', 'section_ids' => $chapterData['section_ids'], 'kp_codes' => $kpCodesWithQuestions, ]; } } // 所有章节都已摸底,返回第一章(重新开始) $firstChapter = $chapters->first(); $chapterData = $this->getChapterKnowledgePointsSimple($firstChapter->id); $kpCodesWithQuestions = $this->filterKpCodesWithQuestions($chapterData['kp_codes']); Log::info('DiagnosticChapterService: 所有章节都已摸底,返回第一章', [ 'textbook_id' => $textbookId, 'student_id' => $studentId, 'chapter_id' => $firstChapter->id, ]); return [ 'chapter_id' => $firstChapter->id, 'chapter_name' => $firstChapter->name ?? '', 'section_ids' => $chapterData['section_ids'], 'kp_codes' => $kpCodesWithQuestions, 'is_restart' => true, // 标记是重新开始 ]; } /** * 获取当前应该学习的章节(第一个有未达标知识点的章节) * 用于智能组卷流程 */ public function getCurrentLearningChapter(int $textbookId, int $studentId, float $threshold = 0.9): ?array { $chapters = TextbookCatalog::query() ->where('textbook_id', $textbookId) ->where('node_type', 'chapter') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->get(); if ($chapters->isEmpty()) { return null; } foreach ($chapters as $chapter) { $chapterData = $this->getChapterKnowledgePointsSimple($chapter->id); $kpCodes = $chapterData['kp_codes']; if (empty($kpCodes)) { continue; } // 使用新方法判断是否达标(跳过无题知识点) $allMastered = StudentKnowledgeMastery::allAtLeastSkipNoQuestions($studentId, $kpCodes, $threshold); if (!$allMastered) { // 检查是否已摸底 $hasDiagnostic = $this->hasChapterDiagnostic($studentId, $chapter->id); Log::info('DiagnosticChapterService: 找到当前学习章节', [ 'textbook_id' => $textbookId, 'student_id' => $studentId, 'student_id_type' => gettype($studentId), 'chapter_id' => $chapter->id, 'chapter_name' => $chapter->name ?? '', 'has_diagnostic' => $hasDiagnostic, 'kp_codes' => $kpCodes, // 显示实际的知识点列表 'kp_count' => count($kpCodes), ]); return [ 'chapter_id' => $chapter->id, 'chapter_name' => $chapter->name ?? '', 'section_ids' => $chapterData['section_ids'], 'kp_codes' => $kpCodes, 'has_diagnostic' => $hasDiagnostic, ]; } } // 所有章节都达标 Log::info('DiagnosticChapterService: 所有章节都达标', [ 'textbook_id' => $textbookId, 'student_id' => $studentId, ]); return null; } /** * 获取下一个章节 */ public function getNextChapter(int $textbookId, int $currentChapterId): ?array { $chapters = TextbookCatalog::query() ->where('textbook_id', $textbookId) ->where('node_type', 'chapter') ->orderBy('display_no') ->orderBy('sort_order') ->orderBy('id') ->get(); $foundCurrent = false; foreach ($chapters as $chapter) { if ($foundCurrent) { $chapterData = $this->getChapterKnowledgePointsSimple($chapter->id); $kpCodesWithQuestions = $this->filterKpCodesWithQuestions($chapterData['kp_codes']); if (!empty($kpCodesWithQuestions)) { return [ 'chapter_id' => $chapter->id, 'chapter_name' => $chapter->name ?? '', 'section_ids' => $chapterData['section_ids'], 'kp_codes' => $kpCodesWithQuestions, ]; } } if ($chapter->id === $currentChapterId) { $foundCurrent = true; } } return null; // 没有下一章 } /** * 过滤出有题目的知识点 */ public function filterKpCodesWithQuestions(array $kpCodes): array { if (empty($kpCodes)) { return []; } $kpCodesWithQuestions = \App\Models\Question::query() ->whereIn('kp_code', $kpCodes) ->distinct() ->pluck('kp_code') ->toArray(); // 保持原有顺序 return array_values(array_intersect($kpCodes, $kpCodesWithQuestions)); } /** * 按顺序获取未达标的知识点(用于智能组卷) * * @param int $studentId 学生ID * @param array $kpCodes 知识点列表(按顺序) * @param float $threshold 达标阈值 * @param int $maxCount 最多返回几个知识点 * @param int $minQuestions 每个知识点最少需要的题目数 * @return array 未达标的知识点列表 */ public function getUnmasteredKpCodesInOrder( int $studentId, array $kpCodes, float $threshold = 0.9, int $maxCount = 2, int $minQuestions = 20 ): array { if (empty($kpCodes)) { return []; } // 获取掌握度(使用 DB::table 避免 Eloquent accessor 把数字转成文字标签) // 【新增】同时获取 direct_mastery_level 和 mastery_level,判断时优先使用 direct_mastery_level $masteryRecords = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->whereIn('kp_code', $kpCodes) ->get(['kp_code', 'mastery_level', 'direct_mastery_level']) ->keyBy('kp_code'); // 构建有效掌握度映射:优先使用 direct_mastery_level,其次使用 mastery_level $levels = []; foreach ($masteryRecords as $kpCode => $record) { // 优先使用 direct_mastery_level(直接学习掌握度) if ($record->direct_mastery_level !== null) { $levels[$kpCode] = (float) $record->direct_mastery_level; } else { $levels[$kpCode] = (float) $record->mastery_level; } } // 【调试】记录查询到的掌握度 Log::info('DiagnosticChapterService: 查询掌握度结果', [ 'student_id' => $studentId, 'student_id_type' => gettype($studentId), 'input_kp_codes' => $kpCodes, 'found_levels' => $levels, 'raw_records' => $masteryRecords->toArray(), ]); // 获取每个知识点的题目数量 $questionCounts = \App\Models\Question::query() ->whereIn('kp_code', $kpCodes) ->selectRaw('kp_code, COUNT(*) as count') ->groupBy('kp_code') ->pluck('count', 'kp_code') ->toArray(); $result = []; $totalQuestions = 0; foreach ($kpCodes as $kpCode) { // 跳过没有题目的知识点 $count = $questionCounts[$kpCode] ?? 0; if ($count === 0) { continue; } // 检查是否达标 $level = isset($levels[$kpCode]) ? (float) $levels[$kpCode] : 0.0; $isMastered = $level >= $threshold; Log::info("DiagnosticChapterService: 检查知识点", [ 'kp_code' => $kpCode, 'level' => $level, 'threshold' => $threshold, 'is_mastered' => $isMastered, 'level_found_in_db' => isset($levels[$kpCode]), ]); if ($level >= $threshold) { continue; } // 添加到结果 $result[] = $kpCode; $totalQuestions += $count; // 检查是否达到最大数量 if (count($result) >= $maxCount) { break; } // 检查题目数量是否足够 if ($totalQuestions >= $minQuestions) { break; } } Log::info('DiagnosticChapterService: 获取未达标知识点', [ 'student_id' => $studentId, 'input_kp_codes' => $kpCodes, 'result_kp_codes' => $result, 'levels_found' => $levels, 'total_questions' => $totalQuestions, 'threshold' => $threshold, ]); return $result; } }