|
|
@@ -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;
|
|
|
+ }
|
|
|
+}
|