$studentId]); $studentGrade = Student::query() ->where('student_id', $studentId) ->value('grade'); // 获取知识图谱结构 $graphData = $this->getKnowledgeGraphStructure($studentGrade ? (int) $studentGrade : null); $leafKpCodes = $graphData['leafKpCodes']; $leafKpCodesSet = $graphData['leafKpCodesSet']; $kpCodes = $graphData['kpCodes']; $maxChildScore = count($leafKpCodes) * 1.0; // 获取学生掌握度数据 $mergedData = $this->getStudentMasteryData($studentId); $mergedCountBefore = count($mergedData); // 仅保留叶子节点数据,避免父节点干扰进度 $mergedData = array_filter($mergedData, function ($item) use ($leafKpCodesSet) { return isset($leafKpCodesSet[$item['kp_code']]); }); if ($mergedCountBefore > 0 && count($mergedData) !== $mergedCountBefore) { Log::info('学习进度过滤父节点', [ 'student_id' => $studentId, 'before' => $mergedCountBefore, 'after' => count($mergedData), ]); } // 如果没有数据,返回空结构(和批量接口行为一致) if (empty($mergedData)) { return [ 'success' => true, 'data' => [ 'student_id' => $studentId, 'learning_progress' => 0, 'learning_progress_percentage' => 0, 'mastered_child_count' => 0, 'total_child_count' => count($leafKpCodes), 'child_mastery_sum' => 0, 'has_data' => false, 'calculated_at' => now()->toISOString() ], 'message' => '该学生暂无学习数据' ]; } // 筛选叶子节点 $childMasteryData = collect($mergedData); // 如果没有子知识点数据,也返回空结构 if ($childMasteryData->isEmpty()) { return [ 'success' => true, 'data' => [ 'student_id' => $studentId, 'learning_progress' => 0, 'learning_progress_percentage' => 0, 'mastered_child_count' => 0, 'total_child_count' => count($leafKpCodes), 'child_mastery_sum' => 0, 'has_data' => false, 'calculated_at' => now()->toISOString() ], 'message' => '该学生暂无子知识点学习数据' ]; } // 计算学习进度 $totalChildMasterySum = $childMasteryData->sum('mastery_level'); $learningProgress = $maxChildScore > 0 ? ($totalChildMasterySum / $maxChildScore) : 0.0; // 统计信息 $statistics = [ 'total_knowledge_points' => count($kpCodes), 'child_knowledge_points' => count($leafKpCodes), 'student_mastered_child_count' => $childMasteryData->count(), 'student_mastered_child_percentage' => round(($childMasteryData->count() / count($leafKpCodes)) * 100, 2), 'child_mastery_sum' => round($totalChildMasterySum, 4), 'max_child_score' => round($maxChildScore, 4), 'learning_progress_percentage' => round($learningProgress * 100, 2), 'data_source' => 'student_knowledge_mastery', 'child_mastery_max' => round($childMasteryData->max('mastery_level'), 4), 'child_mastery_min' => round($childMasteryData->min('mastery_level'), 4), 'child_mastery_avg' => round($childMasteryData->avg('mastery_level'), 4), ]; Log::info('学生学习进度计算成功', [ 'student_id' => $studentId, 'learning_progress' => $learningProgress, ]); return [ 'success' => true, 'data' => [ 'student_id' => $studentId, 'learning_progress' => round($learningProgress, 6), 'learning_progress_percentage' => round($learningProgress * 100, 2), 'mastered_child_count' => $childMasteryData->count(), 'total_child_count' => count($leafKpCodes), 'child_mastery_sum' => round($totalChildMasterySum, 4), 'has_data' => true, 'child_knowledge_points' => $childMasteryData->values()->toArray(), 'statistics' => $statistics, 'calculated_at' => now()->toISOString() ], 'message' => '学习进度计算成功' ]; } /** * 批量获取学生学习进度 */ public function getBatchProgress(array $studentIds): array { if (empty($studentIds)) { return [ 'success' => false, 'error' => 'student_ids 不能为空' ]; } if (count($studentIds) > 100) { return [ 'success' => false, 'error' => '单次最多查询 100 个学生' ]; } Log::info('批量获取学生学习进度', ['count' => count($studentIds)]); $gradeMap = Student::query() ->whereIn('student_id', $studentIds) ->pluck('grade', 'student_id') ->toArray(); $stageGraph = []; $stageStats = []; // 批量获取掌握度数据 $batchData = $this->getBatchStudentMasteryData($studentIds); $detailedData = $batchData['detailed']; $simpleData = $batchData['simple']; // 为每个学生计算学习进度 $results = []; foreach ($studentIds as $studentId) { $studentId = (string) $studentId; $grade = $gradeMap[$studentId] ?? null; $stageKey = $this->getStageKey($grade ? (int) $grade : null); if (!isset($stageGraph[$stageKey])) { $stageGraph[$stageKey] = $this->getKnowledgeGraphStructure($grade ? (int) $grade : null); $stageStats[$stageKey] = [ 'total_child_knowledge_points' => count($stageGraph[$stageKey]['leafKpCodes']), 'student_count' => 0, ]; } $stageStats[$stageKey]['student_count']++; $leafKpCodesSet = $stageGraph[$stageKey]['leafKpCodesSet']; $maxChildScore = count($stageGraph[$stageKey]['leafKpCodes']) * 1.0; $mergedData = []; // 从 student_knowledge_mastery 获取 if (isset($detailedData[$studentId])) { foreach ($detailedData[$studentId] as $item) { $mergedData[$item->kp_code] = [ 'kp_code' => $item->kp_code, 'mastery_level' => (float) $item->mastery_level, ]; } } // 从 student_mastery 补充或更新 if (isset($simpleData[$studentId])) { foreach ($simpleData[$studentId] as $item) { $kpCode = $item->kp_code; $masteryLevel = (float) $item->mastery; if (isset($mergedData[$kpCode])) { if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) { $mergedData[$kpCode]['mastery_level'] = $masteryLevel; } } else { $mergedData[$kpCode] = [ 'kp_code' => $kpCode, 'mastery_level' => $masteryLevel, ]; } } } // 计算学习进度 if (empty($mergedData)) { $results[$studentId] = [ 'student_id' => $studentId, 'learning_progress' => 0, 'learning_progress_percentage' => 0, 'mastered_child_count' => 0, 'total_child_count' => count($stageGraph[$stageKey]['leafKpCodes']), 'has_data' => false, ]; continue; } $mergedCountBefore = count($mergedData); // 仅保留叶子节点数据,避免父节点干扰进度 $mergedData = array_filter($mergedData, function ($item) use ($leafKpCodesSet) { return isset($leafKpCodesSet[$item['kp_code']]); }); if ($mergedCountBefore > 0 && count($mergedData) !== $mergedCountBefore) { Log::info('批量学习进度过滤父节点', [ 'student_id' => $studentId, 'before' => $mergedCountBefore, 'after' => count($mergedData), ]); } // 筛选叶子节点并计算 $childMasterySum = 0; $masteredChildCount = 0; foreach ($mergedData as $item) { if (isset($leafKpCodesSet[$item['kp_code']])) { $childMasterySum += $item['mastery_level']; $masteredChildCount++; } } $learningProgress = $maxChildScore > 0 ? ($childMasterySum / $maxChildScore) : 0.0; $results[$studentId] = [ 'student_id' => $studentId, 'learning_progress' => round($learningProgress, 6), 'learning_progress_percentage' => round($learningProgress * 100, 2), 'mastered_child_count' => $masteredChildCount, 'total_child_count' => count($stageGraph[$stageKey]['leafKpCodes']), 'child_mastery_sum' => round($childMasterySum, 4), 'has_data' => true, ]; } Log::info('批量学习进度计算完成', ['count' => count($results)]); return [ 'success' => true, 'data' => $results, 'meta' => [ 'total_students' => count($studentIds), 'stage_summary' => $stageStats, 'calculated_at' => now()->toISOString(), ] ]; } /** * 获取知识图谱结构(缓存 5 分钟) */ private function getKnowledgeGraphStructure(?int $grade = null): array { $stageKey = $this->getStageKey($grade); $cacheKey = 'knowledge_graph_structure_' . $stageKey; return Cache::remember($cacheKey, 300, function () use ($grade) { $query = DB::connection('remote_mysql') ->table('knowledge_points') ->select(['kp_code', 'parent_kp_code', 'grade']); $stageLabel = $this->getStageLabel($grade); if ($stageLabel) { $query->where('grade', $stageLabel); } $allKps = $query->get(); $kpCodes = $allKps->pluck('kp_code')->toArray(); $parentCodes = $allKps->whereNotNull('parent_kp_code')->pluck('parent_kp_code')->unique()->toArray(); $leafKpCodes = array_values(array_diff($kpCodes, $parentCodes)); $maxDepth = $this->calculateMaxDepth($allKps); return [ 'kpCodes' => $kpCodes, 'leafKpCodes' => $leafKpCodes, 'leafKpCodesSet' => array_flip($leafKpCodes), 'maxDepth' => $maxDepth, ]; }); } private function getStageLabel(?int $grade): ?string { if ($grade === null || $grade <= 0) { return null; } if ($grade <= 6) { return '小学'; } if ($grade <= 9) { return '初中'; } return '高中'; } private function getStageKey(?int $grade): string { $label = $this->getStageLabel($grade); return $label ?: 'all'; } private function calculateMaxDepth($knowledgePoints): int { $children = []; foreach ($knowledgePoints as $kp) { if (!empty($kp->parent_kp_code)) { $children[$kp->parent_kp_code][] = $kp->kp_code; } } $depthCache = []; $maxDepth = 1; $visit = function ($kpCode) use (&$visit, &$children, &$depthCache, &$maxDepth): int { if (isset($depthCache[$kpCode])) { return $depthCache[$kpCode]; } if (empty($children[$kpCode])) { $depthCache[$kpCode] = 1; return 1; } $childDepths = array_map(fn ($child) => $visit($child), $children[$kpCode]); $depthCache[$kpCode] = 1 + max($childDepths); $maxDepth = max($maxDepth, $depthCache[$kpCode]); return $depthCache[$kpCode]; }; foreach ($knowledgePoints as $kp) { $visit($kp->kp_code); } return $maxDepth; } /** * 获取单个学生的掌握度数据 */ private function getStudentMasteryData(string $studentId): array { $mergedData = []; try { $detailedData = DB::connection('remote_mysql') ->table('student_knowledge_mastery') ->where('student_id', $studentId) ->select(['kp_code', 'mastery_level', 'total_attempts', 'correct_attempts', 'updated_at']) ->get(); foreach ($detailedData as $item) { $mergedData[$item->kp_code] = [ 'kp_code' => $item->kp_code, 'mastery_level' => (float) $item->mastery_level, 'total_attempts' => $item->total_attempts, 'correct_attempts' => $item->correct_attempts, 'source_table' => 'student_knowledge_mastery', 'updated_at' => $item->updated_at ]; } } catch (\Exception $e) { Log::warning('从 student_knowledge_mastery 获取数据失败', ['error' => $e->getMessage()]); } return $mergedData; } /** * 批量获取学生掌握度数据 */ private function getBatchStudentMasteryData(array $studentIds): array { $detailedData = DB::connection('remote_mysql') ->table('student_knowledge_mastery') ->whereIn('student_id', $studentIds) ->select(['student_id', 'kp_code', 'mastery_level']) ->get() ->groupBy('student_id'); return [ 'detailed' => $detailedData->toArray(), 'simple' => [], ]; } }