$studentId]); // 获取知识图谱结构 $graphData = $this->getKnowledgeGraphStructure(); $leafKpCodes = $graphData['leafKpCodes']; $leafKpCodesSet = $graphData['leafKpCodesSet']; $kpCodes = $graphData['kpCodes']; $maxChildScore = count($leafKpCodes) * 1.0; // 获取学生掌握度数据 $mergedData = $this->getStudentMasteryData($studentId); // 如果没有数据,返回空结构(和批量接口行为一致) 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)->filter(function ($item) use ($leafKpCodesSet) { return isset($leafKpCodesSet[$item['kp_code']]); }); // 如果没有子知识点数据,也返回空结构 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' => implode(', ', collect($mergedData)->pluck('source_table')->unique()->toArray()), '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)]); // 获取知识图谱结构(所有学生共用) $graphData = $this->getKnowledgeGraphStructure(); $leafKpCodes = $graphData['leafKpCodes']; $leafKpCodesSet = $graphData['leafKpCodesSet']; $maxChildScore = count($leafKpCodes) * 1.0; // 批量获取掌握度数据 $batchData = $this->getBatchStudentMasteryData($studentIds); $detailedData = $batchData['detailed']; $simpleData = $batchData['simple']; // 为每个学生计算学习进度 $results = []; foreach ($studentIds as $studentId) { $studentId = (string) $studentId; $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($leafKpCodes), 'has_data' => false, ]; continue; } // 筛选叶子节点并计算 $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($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), 'total_child_knowledge_points' => count($leafKpCodes), 'calculated_at' => now()->toISOString(), ] ]; } /** * 获取知识图谱结构(缓存 5 分钟) */ private function getKnowledgeGraphStructure(): array { return Cache::remember('knowledge_graph_structure', 300, function () { $allKps = DB::connection('remote_mysql') ->table('knowledge_points') ->select(['kp_code', 'parent_kp_code']) ->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)); return [ 'kpCodes' => $kpCodes, 'leafKpCodes' => $leafKpCodes, 'leafKpCodesSet' => array_flip($leafKpCodes), ]; }); } /** * 获取单个学生的掌握度数据 */ 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()]); } try { $simpleData = DB::connection('remote_mysql') ->table('student_mastery') ->where('student_id', $studentId) ->select(['kp as kp_code', 'mastery', 'attempts as total_attempts', 'correct as correct_attempts', 'updated_at']) ->get(); foreach ($simpleData 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; $mergedData[$kpCode]['source_table'] = 'student_mastery (updated)'; } } else { $mergedData[$kpCode] = [ 'kp_code' => $kpCode, 'mastery_level' => $masteryLevel, 'total_attempts' => $item->total_attempts ?? 0, 'correct_attempts' => $item->correct_attempts ?? 0, 'source_table' => 'student_mastery', 'updated_at' => $item->updated_at ?? null ]; } } } catch (\Exception $e) { Log::warning('从 student_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'); $simpleData = DB::connection('remote_mysql') ->table('student_mastery') ->whereIn('student_id', $studentIds) ->select(['student_id', 'kp as kp_code', 'mastery']) ->get() ->groupBy('student_id'); return [ 'detailed' => $detailedData->toArray(), 'simple' => $simpleData->toArray(), ]; } }