| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Cache;
- use App\Models\Student;
- class StudentProgressService
- {
- /**
- * 获取单个学生的学习进度
- */
- public function getProgress(string $studentId): array
- {
- Log::info('获取学生学习进度', ['student_id' => $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' => [],
- ];
- }
- }
|