Browse Source

学习进度 api【new】

yemeishu 11 hours ago
parent
commit
5d359bb89d

+ 140 - 0
app/Http/Controllers/Api/StudentProgressController.php

@@ -0,0 +1,140 @@
+<?php
+
+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;
+
+class StudentProgressController extends Controller
+{
+    protected StudentProgressService $progressService;
+
+    public function __construct(StudentProgressService $progressService)
+    {
+        $this->progressService = $progressService;
+    }
+
+    /**
+     * 获取学生学习进度
+     *
+     * @param string $studentId
+     * @return JsonResponse
+     */
+    public function getLearningProgress(string $studentId): JsonResponse
+    {
+        try {
+            Log::info('获取学生学习进度', ['student_id' => $studentId]);
+
+            $result = $this->progressService->calculateLearningProgress($studentId);
+
+            if ($result['success']) {
+                return response()->json([
+                    'success' => true,
+                    'data' => $result['data'],
+                    'message' => '学习进度计算成功'
+                ]);
+            }
+
+            return response()->json([
+                'success' => false,
+                'message' => $result['error'] ?? '计算学习进度失败'
+            ], 400);
+
+        } 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()
+            ], 500);
+        }
+    }
+
+    /**
+     * 批量获取学生学习进度
+     *
+     * @param Request $request
+     * @return JsonResponse
+     */
+    public function batchGetLearningProgress(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);
+            }
+
+            if (count($studentIds) > 100) {
+                return response()->json([
+                    'success' => false,
+                    'message' => '单次最多查询100个学生'
+                ], 400);
+            }
+
+            Log::info('批量获取学生学习进度', ['student_ids' => $studentIds]);
+
+            $results = $this->progressService->batchCalculateLearningProgress($studentIds);
+
+            return response()->json([
+                'success' => true,
+                'data' => $results,
+                'message' => '批量获取学习进度成功'
+            ]);
+
+        } catch (\Exception $e) {
+            Log::error('批量获取学生学习进度失败', [
+                'error' => $e->getMessage(),
+                'student_ids' => $request->input('student_ids', [])
+            ]);
+
+            return response()->json([
+                'success' => false,
+                'message' => '批量获取学习进度失败: ' . $e->getMessage()
+            ], 500);
+        }
+    }
+}

+ 374 - 0
app/Services/StudentProgressService.php

@@ -0,0 +1,374 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Collection;
+
+class StudentProgressService
+{
+    /**
+     * 计算学生学习进度(总体掌握度)
+     * 公式:所有子知识点掌握度累加值 / 所有子知识点掌握度的累加值
+     *
+     * @param string $studentId
+     * @return array
+     */
+    public function calculateLearningProgress(string $studentId): array
+    {
+        try {
+            // 1. 获取所有父知识点代码(需要排除的)
+            $parentKpCodes = $this->getParentKnowledgePointCodes();
+
+            // 2. 获取学生所有知识点的掌握度数据(合并两个表)
+            $allMasteryData = $this->getMergedMasteryData($studentId);
+
+            // 3. 过滤掉父知识点,只保留子知识点
+            $childMasteryData = $allMasteryData->filter(function ($item) use ($parentKpCodes) {
+                return !in_array($item['kp_code'], $parentKpCodes);
+            });
+
+            if ($childMasteryData->isEmpty()) {
+                return [
+                    'success' => false,
+                    'error' => '该学生没有子知识点的掌握度数据'
+                ];
+            }
+
+            // 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)
+            ];
+
+            $result = [
+                'student_id' => $studentId,
+                'overall_mastery' => round($overallMastery, 4),
+                '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)
+            ]);
+
+            return [
+                'success' => true,
+                'data' => $result
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('计算学生学习进度失败', [
+                'student_id' => $studentId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'error' => '计算学习进度时发生错误: ' . $e->getMessage()
+            ];
+        }
+    }
+
+    /**
+     * 获取学生知识点掌握度详情
+     *
+     * @param string $studentId
+     * @return array
+     */
+    public function getKnowledgePointDetails(string $studentId): array
+    {
+        try {
+            // 获取合并的掌握度数据
+            $masteryData = $this->getMergedMasteryData($studentId);
+
+            // 获取父知识点列表
+            $parentKpCodes = $this->getParentKnowledgePointCodes();
+
+            // 分类数据
+            $childData = [];
+            $parentData = [];
+
+            foreach ($masteryData as $item) {
+                if (in_array($item['kp_code'], $parentKpCodes)) {
+                    $parentData[] = $item;
+                } else {
+                    $childData[] = $item;
+                }
+            }
+
+            // 获取知识点详细信息
+            $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
+                ]);
+            }
+
+            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
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('获取学生知识点掌握度详情失败', [
+                'student_id' => $studentId,
+                'error' => $e->getMessage()
+            ]);
+
+            throw $e;
+        }
+    }
+
+    /**
+     * 批量计算学生学习进度
+     *
+     * @param array $studentIds
+     * @return array
+     */
+    public function batchCalculateLearningProgress(array $studentIds): array
+    {
+        $results = [];
+
+        foreach ($studentIds as $studentId) {
+            $result = $this->calculateLearningProgress($studentId);
+            $results[] = [
+                'student_id' => $studentId,
+                'success' => $result['success'],
+                'data' => $result['success'] ? $result['data'] : null,
+                'error' => $result['success'] ? null : $result['error']
+            ];
+        }
+
+        return $results;
+    }
+
+    /**
+     * 获取所有父知识点代码
+     *
+     * @return array
+     */
+    private function getParentKnowledgePointCodes(): array
+    {
+        try {
+            $parentCodes = DB::connection('remote_mysql')
+                ->table('knowledge_points')
+                ->whereNotNull('parent_kp_code')
+                ->distinct()
+                ->pluck('parent_kp_code')
+                ->toArray();
+
+            return array_filter($parentCodes);
+        } catch (\Exception $e) {
+            Log::error('获取父知识点代码失败', ['error' => $e->getMessage()]);
+            return [];
+        }
+    }
+
+    /**
+     * 获取合并的学生掌握度数据(从两个表)
+     *
+     * @param string $studentId
+     * @return Collection
+     */
+    private function getMergedMasteryData(string $studentId): Collection
+    {
+        $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();
+
+            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 表获取数据失败', [
+                'student_id' => $studentId,
+                '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();
+
+            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;
+                        $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 表获取数据失败', [
+                '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 [];
+        }
+    }
+
+    /**
+     * 获取数据源信息
+     *
+     * @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) . ')';
+    }
+
+    /**
+     * 获取掌握度分布
+     *
+     * @param Collection $masteryData
+     * @return array
+     */
+    private function getMasteryDistribution(Collection $masteryData): array
+    {
+        $distribution = [
+            'excellent' => 0,  // >= 0.9
+            'good' => 0,       // 0.7 - 0.89
+            'fair' => 0,       // 0.5 - 0.69
+            'poor' => 0,       // < 0.5
+            'unknown' => 0     // 无数据
+        ];
+
+        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;
+    }
+}

+ 229 - 0
routes/api.php

@@ -1121,3 +1121,232 @@ Route::get('/health', [HealthCheckController::class, 'index'])
         'auth:api',
     ])
     ->name('api.health.index');
+
+/*
+|--------------------------------------------------------------------------
+| 学生学习进度 API 路由(测试版)
+|--------------------------------------------------------------------------
+|
+| 使用内联闭包避免类加载问题
+|
+*/
+
+// 获取学生学习进度(测试版)
+Route::get('/students/{studentId}/learning-progress', function (string $studentId) {
+    try {
+        Log::info('获取学生学习进度', ['student_id' => $studentId]);
+
+        // 1. 获取所有父知识点代码(需要排除的)
+        $parentKpCodes = [];
+        try {
+            $parentCodes = DB::connection('remote_mysql')
+                ->table('knowledge_points')
+                ->whereNotNull('parent_kp_code')
+                ->distinct()
+                ->pluck('parent_kp_code')
+                ->toArray();
+            $parentKpCodes = array_filter($parentCodes);
+        } catch (\Exception $e) {
+            Log::warning('获取父知识点代码失败', ['error' => $e->getMessage()]);
+        }
+
+        // 2. 获取学生所有知识点的掌握度数据(合并两个表)
+        $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();
+
+            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 表获取数据失败', [
+                'student_id' => $studentId,
+                '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();
+
+            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;
+                        $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 表获取数据失败', [
+                'student_id' => $studentId,
+                'error' => $e->getMessage()
+            ]);
+        }
+
+        // 3. 获取学生所有知识点掌握度数据(只使用student_knowledge_mastery表,因为student_mastery表为空)
+        $allMasteryData = collect($mergedData);
+
+        if ($allMasteryData->isEmpty()) {
+            return response()->json([
+                'success' => false,
+                'error' => '该学生没有掌握度数据'
+            ], 400);
+        }
+
+        // 4. 获取知识图谱中所有知识点,找出真正的子知识点(叶子节点)
+        $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));
+
+        // 5. 筛选出学生掌握的子知识点数据
+        $childMasteryData = $allMasteryData->filter(function($item) use ($leafKpCodes) {
+            return in_array($item['kp_code'], $leafKpCodes);
+        });
+
+        if ($childMasteryData->isEmpty()) {
+            return response()->json([
+                'success' => false,
+                'error' => '该学生没有子知识点的掌握度数据'
+            ], 400);
+        }
+
+        // 6. 计算分子:学生已掌握的子知识点掌握度总和
+        $totalChildMasterySum = $childMasteryData->sum('mastery_level');
+
+        // 7. 计算分母:知识图谱中所有子知识点的最大可能总分
+        $maxChildScore = count($leafKpCodes) * 1.0; // 每个子知识点最高1分
+        $learningProgress = $maxChildScore > 0 ? ($totalChildMasterySum / $maxChildScore) : 0.0;
+
+        // 8. 统计信息
+        $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(', ', $allMasteryData->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),
+        ];
+
+        $result = [
+            'student_id' => $studentId,
+            'learning_progress' => round($learningProgress, 6),
+            'learning_progress_percentage' => round($learningProgress * 100, 2),
+            'child_mastery_sum' => round($totalChildMasterySum, 4),
+            'child_knowledge_points' => $childMasteryData->values()->toArray(),
+            'statistics' => $statistics,
+            'calculated_at' => now()->toISOString()
+        ];
+
+        Log::info('学生学习进度计算成功', [
+            'student_id' => $studentId,
+            'learning_progress' => $learningProgress,
+            'child_mastery_sum' => $totalChildMasterySum,
+            'child_knowledge_points_count' => count($leafKpCodes),
+            'student_mastered_child_count' => $childMasteryData->count()
+        ]);
+
+        return response()->json([
+            'success' => true,
+            'data' => $result,
+            'message' => '学习进度计算成功'
+        ]);
+
+    } catch (\Exception $e) {
+        Log::error('计算学生学习进度失败', [
+            'student_id' => $studentId,
+            '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.get');
+
+/*
+|--------------------------------------------------------------------------
+| 注意:知识点详情API已被注释
+|--------------------------------------------------------------------------
+|
+| 由于路由冲突问题,此API暂时被注释。可使用以下替代接口:
+| - api/mathrecsys/students/{studentId}/knowledge-points/detail
+| - api/students/{studentId}/knowledge-points/detail
+|
+*/
+
+// 获取学生知识点掌握度详情(测试版)
+// Route::get('/students/{studentId}/knowledge-points', function (string $studentId) {
+//     // 实现代码...
+// })
+// ->withoutMiddleware([
+//     Authenticate::class,
+//     'auth',
+//     'auth:sanctum',
+//     'auth:api',
+// ])
+// ->name('api.students.knowledge-points.details');