Browse Source

feat: 增加批量获取学习学习进度接口

过卫栋 6 ngày trước cách đây
mục cha
commit
cbc465404b

+ 20 - 100
app/Http/Controllers/Api/StudentProgressController.php

@@ -4,137 +4,57 @@ namespace App\Http\Controllers\Api;
 
 use App\Http\Controllers\Controller;
 use App\Services\StudentProgressService;
-use Illuminate\Http\Request;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Log;
+use Illuminate\Http\Request;
 
 class StudentProgressController extends Controller
 {
-    protected StudentProgressService $progressService;
-
-    public function __construct(StudentProgressService $progressService)
-    {
-        $this->progressService = $progressService;
-    }
+    public function __construct(
+        private StudentProgressService $service
+    ) {}
 
     /**
-     * 获取学生学习进度
-     *
-     * @param string $studentId
-     * @return JsonResponse
+     * 获取单个学生的学习进度
+     * GET /api/students/{studentId}/learning-progress
      */
-    public function getLearningProgress(string $studentId): JsonResponse
+    public function show(string $studentId): JsonResponse
     {
         try {
-            Log::info('获取学生学习进度', ['student_id' => $studentId]);
+            $result = $this->service->getProgress($studentId);
 
-            $result = $this->progressService->calculateLearningProgress($studentId);
-
-            if ($result['success']) {
-                return response()->json([
-                    'success' => true,
-                    'data' => $result['data'],
-                    'message' => '学习进度计算成功'
-                ]);
+            if (!$result['success']) {
+                return response()->json($result, 400);
             }
 
-            return response()->json([
-                'success' => false,
-                'message' => $result['error'] ?? '计算学习进度失败'
-            ], 400);
-
+            return response()->json($result);
         } catch (\Exception $e) {
-            Log::error('获取学生学习进度失败', [
-                'student_id' => $studentId,
-                'error' => $e->getMessage(),
-                'trace' => $e->getTraceAsString()
-            ]);
-
             return response()->json([
                 'success' => false,
-                'message' => '获取学习进度失败: ' . $e->getMessage()
-            ], 500);
-        }
-    }
-
-    /**
-     * 获取学生知识点掌握度详情
-     *
-     * @param string $studentId
-     * @return JsonResponse
-     */
-    public function getKnowledgePointDetails(string $studentId): JsonResponse
-    {
-        try {
-            Log::info('获取学生知识点掌握度详情', ['student_id' => $studentId]);
-
-            $details = $this->progressService->getKnowledgePointDetails($studentId);
-
-            return response()->json([
-                'success' => true,
-                'data' => $details,
-                'message' => '获取知识点详情成功'
-            ]);
-
-        } catch (\Exception $e) {
-            Log::error('获取学生知识点掌握度详情失败', [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
-
-            return response()->json([
-                'success' => false,
-                'message' => '获取知识点详情失败: ' . $e->getMessage()
+                'message' => '计算学习进度失败: ' . $e->getMessage()
             ], 500);
         }
     }
 
     /**
      * 批量获取学生学习进度
-     *
-     * @param Request $request
-     * @return JsonResponse
+     * POST /api/students/learning-progress/batch
      */
-    public function batchGetLearningProgress(Request $request): JsonResponse
+    public function batch(Request $request): JsonResponse
     {
         try {
             $studentIds = $request->input('student_ids', []);
-            $studentIds = array_filter(array_map('trim', $studentIds));
-
-            if (empty($studentIds)) {
-                return response()->json([
-                    'success' => false,
-                    'message' => '学生ID列表不能为空'
-                ], 400);
-            }
+            $result = $this->service->getBatchProgress($studentIds);
 
-            if (count($studentIds) > 100) {
-                return response()->json([
-                    'success' => false,
-                    'message' => '单次最多查询100个学生'
-                ], 400);
+            if (!$result['success']) {
+                return response()->json($result, 400);
             }
 
-            Log::info('批量获取学生学习进度', ['student_ids' => $studentIds]);
-
-            $results = $this->progressService->batchCalculateLearningProgress($studentIds);
-
-            return response()->json([
-                'success' => true,
-                'data' => $results,
-                'message' => '批量获取学习进度成功'
-            ]);
-
+            return response()->json($result);
         } catch (\Exception $e) {
-            Log::error('批量获取学生学习进度失败', [
-                'error' => $e->getMessage(),
-                'student_ids' => $request->input('student_ids', [])
-            ]);
-
             return response()->json([
                 'success' => false,
-                'message' => '批量获取学习进度失败: ' . $e->getMessage()
+                'message' => '批量计算学习进度失败: ' . $e->getMessage()
             ], 500);
         }
     }
-}
+}

+ 224 - 260
app/Services/StudentProgressService.php

@@ -4,225 +4,264 @@ namespace App\Services;
 
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
 
 class StudentProgressService
 {
     /**
-     * 计算学生学习进度(总体掌握度)
-     * 公式:所有子知识点掌握度累加值 / 所有子知识点掌握度的累加值
-     *
-     * @param string $studentId
-     * @return array
+     * 获取单个学生的学习进度
      */
-    public function calculateLearningProgress(string $studentId): array
+    public function getProgress(string $studentId): array
     {
-        try {
-            // 1. 获取所有父知识点代码(需要排除的)
-            $parentKpCodes = $this->getParentKnowledgePointCodes();
+        Log::info('获取学生学习进度', ['student_id' => $studentId]);
 
-            // 2. 获取学生所有知识点的掌握度数据(合并两个表)
-            $allMasteryData = $this->getMergedMasteryData($studentId);
+        // 获取知识图谱结构
+        $graphData = $this->getKnowledgeGraphStructure();
+        $leafKpCodes = $graphData['leafKpCodes'];
+        $leafKpCodesSet = $graphData['leafKpCodesSet'];
+        $kpCodes = $graphData['kpCodes'];
+        $maxChildScore = count($leafKpCodes) * 1.0;
 
-            // 3. 过滤掉父知识点,只保留子知识点
-            $childMasteryData = $allMasteryData->filter(function ($item) use ($parentKpCodes) {
-                return !in_array($item['kp_code'], $parentKpCodes);
-            });
+        // 获取学生掌握度数据
+        $mergedData = $this->getStudentMasteryData($studentId);
 
-            if ($childMasteryData->isEmpty()) {
-                return [
-                    'success' => false,
-                    'error' => '该学生没有子知识点的掌握度数据'
-                ];
-            }
+        // 如果没有数据,返回空结构(和批量接口行为一致)
+        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']]);
+        });
 
-            // 4. 计算总体掌握度
-            $totalMasterySum = $childMasteryData->sum('mastery_level');
-            $masteryCount = $childMasteryData->count();
-            $overallMastery = $masteryCount > 0 ? $totalMasterySum / $masteryCount : 0.0;
-
-            // 5. 统计信息
-            $statistics = [
-                'total_child_kps' => $masteryCount,
-                'average_mastery' => round($overallMastery, 4),
-                'max_mastery' => round($childMasteryData->max('mastery_level'), 4),
-                'min_mastery' => round($childMasteryData->min('mastery_level'), 4),
-                'data_source' => $this->getDataSourceInfo($allMasteryData),
-                'parent_kp_excluded' => count($parentKpCodes),
-                'mastery_distribution' => $this->getMasteryDistribution($childMasteryData)
+        // 如果没有子知识点数据,也返回空结构
+        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' => '该学生暂无子知识点学习数据'
             ];
+        }
 
-            $result = [
+        // 计算学习进度
+        $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,
-                'overall_mastery' => round($overallMastery, 4),
+                '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()
-            ];
-
-            Log::info('学生学习进度计算成功', [
-                'student_id' => $studentId,
-                'overall_mastery' => $overallMastery,
-                'child_kp_count' => $masteryCount,
-                'parent_kp_excluded' => count($parentKpCodes)
-            ]);
+            ],
+            'message' => '学习进度计算成功'
+        ];
+    }
 
+    /**
+     * 批量获取学生学习进度
+     */
+    public function getBatchProgress(array $studentIds): array
+    {
+        if (empty($studentIds)) {
             return [
-                'success' => true,
-                'data' => $result
+                'success' => false,
+                'error' => 'student_ids 不能为空'
             ];
+        }
 
-        } catch (\Exception $e) {
-            Log::error('计算学生学习进度失败', [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
-
+        if (count($studentIds) > 100) {
             return [
                 'success' => false,
-                'error' => '计算学习进度时发生错误: ' . $e->getMessage()
+                'error' => '单次最多查询 100 个学生'
             ];
         }
-    }
 
-    /**
-     * 获取学生知识点掌握度详情
-     *
-     * @param string $studentId
-     * @return array
-     */
-    public function getKnowledgePointDetails(string $studentId): array
-    {
-        try {
-            // 获取合并的掌握度数据
-            $masteryData = $this->getMergedMasteryData($studentId);
+        Log::info('批量获取学生学习进度', ['count' => count($studentIds)]);
 
-            // 获取父知识点列表
-            $parentKpCodes = $this->getParentKnowledgePointCodes();
+        // 获取知识图谱结构(所有学生共用)
+        $graphData = $this->getKnowledgeGraphStructure();
+        $leafKpCodes = $graphData['leafKpCodes'];
+        $leafKpCodesSet = $graphData['leafKpCodesSet'];
+        $maxChildScore = count($leafKpCodes) * 1.0;
 
-            // 分类数据
-            $childData = [];
-            $parentData = [];
+        // 批量获取掌握度数据
+        $batchData = $this->getBatchStudentMasteryData($studentIds);
+        $detailedData = $batchData['detailed'];
+        $simpleData = $batchData['simple'];
 
-            foreach ($masteryData as $item) {
-                if (in_array($item['kp_code'], $parentKpCodes)) {
-                    $parentData[] = $item;
-                } else {
-                    $childData[] = $item;
+        // 为每个学生计算学习进度
+        $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,
+                    ];
                 }
             }
 
-            // 获取知识点详细信息
-            $knowledgePointDetails = $this->getKnowledgePointDetailsByCodes(
-                array_column($masteryData->toArray(), 'kp_code')
-            );
-
-            // 合并知识点信息
-            $enhancedData = [];
-            foreach ($masteryData as $item) {
-                $kpCode = $item['kp_code'];
-                $detail = $knowledgePointDetails[$kpCode] ?? null;
-
-                $enhancedData[] = array_merge($item, [
-                    'knowledge_point_name' => $detail['name'] ?? '未知知识点',
-                    'subject' => $detail['subject'] ?? null,
-                    'grade' => $detail['grade'] ?? null,
-                    'is_parent' => in_array($kpCode, $parentKpCodes),
-                    'parent_kp_code' => $detail['parent_kp_code'] ?? null
-                ]);
+            // 从 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,
+                        ];
+                    }
+                }
             }
 
-            return [
-                'student_id' => $studentId,
-                'mastery_data' => $enhancedData,
-                'summary' => [
-                    'total_kps' => count($enhancedData),
-                    'child_kps' => count($childData),
-                    'parent_kps' => count($parentData),
-                    'overall_child_mastery' => count($childData) > 0 ?
-                        round(array_sum(array_column($childData, 'mastery_level')) / count($childData), 4) : 0.0
-                ]
-            ];
+            // 计算学习进度
+            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;
+            }
 
-        } catch (\Exception $e) {
-            Log::error('获取学生知识点掌握度详情失败', [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
+            // 筛选叶子节点并计算
+            $childMasterySum = 0;
+            $masteredChildCount = 0;
 
-            throw $e;
-        }
-    }
+            foreach ($mergedData as $item) {
+                if (isset($leafKpCodesSet[$item['kp_code']])) {
+                    $childMasterySum += $item['mastery_level'];
+                    $masteredChildCount++;
+                }
+            }
 
-    /**
-     * 批量计算学生学习进度
-     *
-     * @param array $studentIds
-     * @return array
-     */
-    public function batchCalculateLearningProgress(array $studentIds): array
-    {
-        $results = [];
+            $learningProgress = $maxChildScore > 0 ? ($childMasterySum / $maxChildScore) : 0.0;
 
-        foreach ($studentIds as $studentId) {
-            $result = $this->calculateLearningProgress($studentId);
-            $results[] = [
+            $results[$studentId] = [
                 'student_id' => $studentId,
-                'success' => $result['success'],
-                'data' => $result['success'] ? $result['data'] : null,
-                'error' => $result['success'] ? null : $result['error']
+                '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,
             ];
         }
 
-        return $results;
+        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(),
+            ]
+        ];
     }
 
     /**
-     * 获取所有父知识点代码
-     *
-     * @return array
+     * 获取知识图谱结构(缓存 5 分钟)
      */
-    private function getParentKnowledgePointCodes(): array
+    private function getKnowledgeGraphStructure(): array
     {
-        try {
-            $parentCodes = DB::connection('remote_mysql')
+        return Cache::remember('knowledge_graph_structure', 300, function () {
+            $allKps = DB::connection('remote_mysql')
                 ->table('knowledge_points')
-                ->whereNotNull('parent_kp_code')
-                ->distinct()
-                ->pluck('parent_kp_code')
-                ->toArray();
+                ->select(['kp_code', 'parent_kp_code'])
+                ->get();
 
-            return array_filter($parentCodes);
-        } catch (\Exception $e) {
-            Log::error('获取父知识点代码失败', ['error' => $e->getMessage()]);
-            return [];
-        }
+            $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),
+            ];
+        });
     }
 
     /**
-     * 获取合并的学生掌握度数据(从两个表)
-     *
-     * @param string $studentId
-     * @return Collection
+     * 获取单个学生的掌握度数据
      */
-    private function getMergedMasteryData(string $studentId): Collection
+    private function getStudentMasteryData(string $studentId): array
     {
         $mergedData = [];
 
         try {
-            // 从 student_knowledge_mastery 表获取数据
             $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()
-                ->toArray();
+                ->select(['kp_code', 'mastery_level', 'total_attempts', 'correct_attempts', 'updated_at'])
+                ->get();
 
             foreach ($detailedData as $item) {
                 $mergedData[$item->kp_code] = [
@@ -234,34 +273,21 @@ class StudentProgressService
                     'updated_at' => $item->updated_at
                 ];
             }
-
         } catch (\Exception $e) {
-            Log::warning('从 student_knowledge_mastery 表获取数据失败', [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
+            Log::warning('从 student_knowledge_mastery 获取数据失败', ['error' => $e->getMessage()]);
         }
 
         try {
-            // 从 student_mastery 表获取数据(补充或覆盖)
             $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()
-                ->toArray();
+                ->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;
 
-                // 如果已存在,优先使用 mastery_level 更高的数据
                 if (isset($mergedData[$kpCode])) {
                     if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
                         $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
@@ -278,97 +304,35 @@ class StudentProgressService
                     ];
                 }
             }
-
-        } catch (\Exception $e) {
-            Log::warning('从 student_mastery 表获取数据失败', [
-                'student_id' => $studentId,
-                'error' => $e->getMessage()
-            ]);
-        }
-
-        return collect($mergedData)->values();
-    }
-
-    /**
-     * 获取知识点详细信息
-     *
-     * @param array $kpCodes
-     * @return array
-     */
-    private function getKnowledgePointDetailsByCodes(array $kpCodes): array
-    {
-        if (empty($kpCodes)) {
-            return [];
-        }
-
-        try {
-            $details = DB::connection('remote_mysql')
-                ->table('knowledge_points')
-                ->whereIn('kp_code', $kpCodes)
-                ->select(['kp_code', 'name', 'subject', 'grade', 'parent_kp_code'])
-                ->get()
-                ->keyBy('kp_code')
-                ->toArray();
-
-            return $details;
         } catch (\Exception $e) {
-            Log::warning('获取知识点详细信息失败', [
-                'kp_codes' => $kpCodes,
-                'error' => $e->getMessage()
-            ]);
-            return [];
+            Log::warning('从 student_mastery 获取数据失败', ['error' => $e->getMessage()]);
         }
-    }
-
-    /**
-     * 获取数据源信息
-     *
-     * @param Collection $masteryData
-     * @return string
-     */
-    private function getDataSourceInfo(Collection $masteryData): string
-    {
-        $sourceTables = $masteryData->pluck('source_table')->unique()->toArray();
 
-        if (count($sourceTables) === 1) {
-            return $sourceTables[0];
-        }
-
-        return 'merged (' . implode(', ', $sourceTables) . ')';
+        return $mergedData;
     }
 
     /**
-     * 获取掌握度分布
-     *
-     * @param Collection $masteryData
-     * @return array
+     * 批量获取学生掌握度数据
      */
-    private function getMasteryDistribution(Collection $masteryData): array
+    private function getBatchStudentMasteryData(array $studentIds): array
     {
-        $distribution = [
-            'excellent' => 0,  // >= 0.9
-            'good' => 0,       // 0.7 - 0.89
-            'fair' => 0,       // 0.5 - 0.69
-            'poor' => 0,       // < 0.5
-            'unknown' => 0     // 无数据
+        $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(),
         ];
-
-        foreach ($masteryData as $item) {
-            $mastery = $item['mastery_level'];
-
-            if ($mastery >= 0.9) {
-                $distribution['excellent']++;
-            } elseif ($mastery >= 0.7) {
-                $distribution['good']++;
-            } elseif ($mastery >= 0.5) {
-                $distribution['fair']++;
-            } elseif ($mastery > 0) {
-                $distribution['poor']++;
-            } else {
-                $distribution['unknown']++;
-            }
-        }
-
-        return $distribution;
     }
-}
+}

+ 179 - 5
routes/api.php

@@ -1124,14 +1124,30 @@ Route::get('/health', [HealthCheckController::class, 'index'])
 
 /*
 |--------------------------------------------------------------------------
-| 学生学习进度 API 路由(测试版)
+| 学生学习进度 API 路由
+|--------------------------------------------------------------------------
+*/
+
+use App\Http\Controllers\Api\StudentProgressController;
+
+// 获取单个学生学习进度
+Route::get('/students/{studentId}/learning-progress', [StudentProgressController::class, 'show'])
+    ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
+    ->name('api.students.learning-progress.show');
+
+// 批量获取学生学习进度
+Route::post('/students/learning-progress/batch', [StudentProgressController::class, 'batch'])
+    ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
+    ->name('api.students.learning-progress.batch');
+
+/*
+|--------------------------------------------------------------------------
+| 以下为旧代码(已迁移到 Controller,保留注释供参考)
 |--------------------------------------------------------------------------
-|
-| 使用内联闭包避免类加载问题
-|
 */
 
-// 获取学生学习进度(测试版)
+// [已迁移] 获取学生学习进度
+/*
 Route::get('/students/{studentId}/learning-progress', function (string $studentId) {
     try {
         Log::info('获取学生学习进度', ['student_id' => $studentId]);
@@ -1328,6 +1344,164 @@ Route::get('/students/{studentId}/learning-progress', function (string $studentI
 ])
 ->name('api.students.learning-progress.get');
 
+// 批量获取学生学习进度(解决 N+1 问题)
+Route::post('/students/learning-progress/batch', function (\Illuminate\Http\Request $request) {
+    try {
+        $studentIds = $request->input('student_ids', []);
+
+        if (empty($studentIds)) {
+            return response()->json([
+                'success' => false,
+                'error' => 'student_ids 不能为空'
+            ], 400);
+        }
+
+        if (count($studentIds) > 100) {
+            return response()->json([
+                'success' => false,
+                'error' => '单次最多查询 100 个学生'
+            ], 400);
+        }
+
+        Log::info('批量获取学生学习进度', ['student_ids' => $studentIds, 'count' => count($studentIds)]);
+
+        // 1. 获取知识图谱结构(所有学生共用)
+        $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));
+        $leafKpCodesSet = array_flip($leafKpCodes); // 用于快速查找
+        $maxChildScore = count($leafKpCodes) * 1.0;
+
+        // 2. 批量获取 student_knowledge_mastery 数据
+        $detailedData = DB::connection('remote_mysql')
+            ->table('student_knowledge_mastery')
+            ->whereIn('student_id', $studentIds)
+            ->select(['student_id', 'kp_code', 'mastery_level', 'total_attempts', 'correct_attempts', 'updated_at'])
+            ->get()
+            ->groupBy('student_id');
+
+        // 3. 批量获取 student_mastery 数据
+        $simpleData = DB::connection('remote_mysql')
+            ->table('student_mastery')
+            ->whereIn('student_id', $studentIds)
+            ->select(['student_id', 'kp as kp_code', 'mastery', 'attempts as total_attempts', 'correct as correct_attempts', 'updated_at'])
+            ->get()
+            ->groupBy('student_id');
+
+        // 4. 为每个学生计算学习进度
+        $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 response()->json([
+            'success' => true,
+            'data' => $results,
+            'meta' => [
+                'total_students' => count($studentIds),
+                'total_child_knowledge_points' => count($leafKpCodes),
+                'calculated_at' => now()->toISOString(),
+            ]
+        ]);
+
+    } catch (\Exception $e) {
+        Log::error('批量计算学习进度失败', [
+            'error' => $e->getMessage(),
+            'trace' => $e->getTraceAsString()
+        ]);
+
+        return response()->json([
+            'success' => false,
+            'message' => '批量计算学习进度失败: ' . $e->getMessage()
+        ], 500);
+    }
+})
+->withoutMiddleware([
+    Authenticate::class,
+    'auth',
+    'auth:sanctum',
+    'auth:api',
+])
+->name('api.students.learning-progress.batch');
+*/
+
 /*
 |--------------------------------------------------------------------------
 | 注意:知识点详情API已被注释