|
|
@@ -5,6 +5,7 @@ namespace App\Services;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
+use App\Models\Student;
|
|
|
|
|
|
class StudentProgressService
|
|
|
{
|
|
|
@@ -15,8 +16,12 @@ class StudentProgressService
|
|
|
{
|
|
|
Log::info('获取学生学习进度', ['student_id' => $studentId]);
|
|
|
|
|
|
+ $studentGrade = Student::query()
|
|
|
+ ->where('student_id', $studentId)
|
|
|
+ ->value('grade');
|
|
|
+
|
|
|
// 获取知识图谱结构
|
|
|
- $graphData = $this->getKnowledgeGraphStructure();
|
|
|
+ $graphData = $this->getKnowledgeGraphStructure($studentGrade ? (int) $studentGrade : null);
|
|
|
$leafKpCodes = $graphData['leafKpCodes'];
|
|
|
$leafKpCodesSet = $graphData['leafKpCodesSet'];
|
|
|
$kpCodes = $graphData['kpCodes'];
|
|
|
@@ -24,6 +29,18 @@ class StudentProgressService
|
|
|
|
|
|
// 获取学生掌握度数据
|
|
|
$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)) {
|
|
|
@@ -44,9 +61,7 @@ class StudentProgressService
|
|
|
}
|
|
|
|
|
|
// 筛选叶子节点
|
|
|
- $childMasteryData = collect($mergedData)->filter(function ($item) use ($leafKpCodesSet) {
|
|
|
- return isset($leafKpCodesSet[$item['kp_code']]);
|
|
|
- });
|
|
|
+ $childMasteryData = collect($mergedData);
|
|
|
|
|
|
// 如果没有子知识点数据,也返回空结构
|
|
|
if ($childMasteryData->isEmpty()) {
|
|
|
@@ -79,7 +94,7 @@ class StudentProgressService
|
|
|
'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()),
|
|
|
+ '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),
|
|
|
@@ -129,11 +144,13 @@ class StudentProgressService
|
|
|
|
|
|
Log::info('批量获取学生学习进度', ['count' => count($studentIds)]);
|
|
|
|
|
|
- // 获取知识图谱结构(所有学生共用)
|
|
|
- $graphData = $this->getKnowledgeGraphStructure();
|
|
|
- $leafKpCodes = $graphData['leafKpCodes'];
|
|
|
- $leafKpCodesSet = $graphData['leafKpCodesSet'];
|
|
|
- $maxChildScore = count($leafKpCodes) * 1.0;
|
|
|
+ $gradeMap = Student::query()
|
|
|
+ ->whereIn('student_id', $studentIds)
|
|
|
+ ->pluck('grade', 'student_id')
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ $stageGraph = [];
|
|
|
+ $stageStats = [];
|
|
|
|
|
|
// 批量获取掌握度数据
|
|
|
$batchData = $this->getBatchStudentMasteryData($studentIds);
|
|
|
@@ -145,6 +162,19 @@ class StudentProgressService
|
|
|
|
|
|
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 获取
|
|
|
@@ -183,12 +213,25 @@ class StudentProgressService
|
|
|
'learning_progress' => 0,
|
|
|
'learning_progress_percentage' => 0,
|
|
|
'mastered_child_count' => 0,
|
|
|
- 'total_child_count' => count($leafKpCodes),
|
|
|
+ '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;
|
|
|
@@ -207,7 +250,7 @@ class StudentProgressService
|
|
|
'learning_progress' => round($learningProgress, 6),
|
|
|
'learning_progress_percentage' => round($learningProgress * 100, 2),
|
|
|
'mastered_child_count' => $masteredChildCount,
|
|
|
- 'total_child_count' => count($leafKpCodes),
|
|
|
+ 'total_child_count' => count($stageGraph[$stageKey]['leafKpCodes']),
|
|
|
'child_mastery_sum' => round($childMasterySum, 4),
|
|
|
'has_data' => true,
|
|
|
];
|
|
|
@@ -220,7 +263,7 @@ class StudentProgressService
|
|
|
'data' => $results,
|
|
|
'meta' => [
|
|
|
'total_students' => count($studentIds),
|
|
|
- 'total_child_knowledge_points' => count($leafKpCodes),
|
|
|
+ 'stage_summary' => $stageStats,
|
|
|
'calculated_at' => now()->toISOString(),
|
|
|
]
|
|
|
];
|
|
|
@@ -229,26 +272,92 @@ class StudentProgressService
|
|
|
/**
|
|
|
* 获取知识图谱结构(缓存 5 分钟)
|
|
|
*/
|
|
|
- private function getKnowledgeGraphStructure(): array
|
|
|
+ private function getKnowledgeGraphStructure(?int $grade = null): array
|
|
|
{
|
|
|
- return Cache::remember('knowledge_graph_structure', 300, function () {
|
|
|
- $allKps = DB::connection('remote_mysql')
|
|
|
+ $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'])
|
|
|
- ->get();
|
|
|
+ ->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;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取单个学生的掌握度数据
|
|
|
*/
|
|
|
@@ -277,37 +386,6 @@ class StudentProgressService
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
@@ -323,16 +401,9 @@ class StudentProgressService
|
|
|
->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(),
|
|
|
+ 'simple' => [],
|
|
|
];
|
|
|
}
|
|
|
}
|