掌握度, 'confidence' => 置信度, 'trend' => 趋势] */ public function calculateMasteryLevel(string $studentId, string $kpCode, ?array $attempts = null, ?int $examBaseDifficulty = null): array { // 优先使用传递的答题记录,如果没有则从数据库查询 if ($attempts === null || empty($attempts)) { $attempts = $this->getStudentAttempts($studentId, $kpCode); } if (empty($attempts)) { Log::warning('没有答题记录,无法计算掌握度', [ 'student_id' => $studentId, 'kp_code' => $kpCode, 'attempts_provided' => $attempts !== null, ]); return [ 'mastery' => 0.0, 'confidence' => 0.0, 'trend' => 'insufficient', 'total_attempts' => 0, 'correct_attempts' => 0, 'accuracy_rate' => 0.0, ]; } // 如果没有学案基准难度,使用默认值2(提分) if ($examBaseDifficulty === null) { Log::warning('缺少学案基准难度,使用默认值2(提分)', [ 'student_id' => $studentId, 'kp_code' => $kpCode, 'attempts_count' => count($attempts), ]); $examBaseDifficulty = 2; // 默认提分难度 } $masteryData = $this->calculateMasteryWithExamDifficulty($studentId, $kpCode, $attempts, $examBaseDifficulty); Log::info('掌握度计算完成', [ 'student_id' => $studentId, 'kp_code' => $kpCode, 'exam_base_difficulty' => $examBaseDifficulty, 'difficulty_name' => $this->getDifficultyName($examBaseDifficulty), 'total_attempts' => count($attempts), 'correct_attempts' => $masteryData['correct_attempts'], 'final_mastery' => $masteryData['mastery'], 'confidence' => $masteryData['confidence'], 'trend' => $masteryData['trend'], ]); return $masteryData; } /** * 获取难度等级名称 */ private function getDifficultyName(int $difficultyLevel): string { return match ($difficultyLevel) { 1 => '筑基', 2 => '提分', 3 => '培优', 4 => '竞赛', default => '未知', }; } /** * 【新算法】使用学案基准难度的动态加减逻辑计算掌握度 */ private function calculateMasteryWithExamDifficulty(string $studentId, string $kpCode, array $attempts, int $examBaseDifficulty): array { // 获取历史掌握度 $historyMastery = DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->where('kp_code', $kpCode) ->first(); $oldMastery = $historyMastery->mastery_level ?? 0.5; // 默认0.5 // 统计正确和错误次数 $totalAttempts = count($attempts); $correctAttempts = 0; $incorrectAttempts = 0; // 计算每次答题的权重变化 $totalChange = 0.0; foreach ($attempts as $attempt) { $isCorrect = boolval($attempt['is_correct'] ?? false); $questionDifficulty = floatval($attempt['question_difficulty'] ?? 0.6); // 难度映射:将0.0-1.0的浮点数难度映射为1-4等级 $questionLevel = $this->mapDifficultyToLevel($questionDifficulty); // 根据难度关系计算权重变化 $change = $this->calculateWeightByDifficultyRelation($questionLevel, $examBaseDifficulty, $isCorrect); $totalChange += $change; if ($isCorrect) { $correctAttempts++; } else { $incorrectAttempts++; } Log::debug('掌握度变化计算', [ 'question_id' => $attempt['question_id'] ?? '', 'question_difficulty' => $questionDifficulty, 'question_level' => $questionLevel, 'exam_base_difficulty' => $examBaseDifficulty, 'is_correct' => $isCorrect, 'change' => $change, 'running_total' => $totalChange ]); } // 计算平均变化(避免单次考试影响过大) $averageChange = $totalAttempts > 0 ? $totalChange / $totalAttempts : 0.0; // 应用权重调整(避免单次考试变化过大) $weightedChange = $averageChange * min($totalAttempts / 10.0, 1.0); // 最多10次考试达到满权重 // 新掌握度 = 旧掌握度 + 加权变化 $newMastery = $oldMastery + $weightedChange; // 边界限制:0.0 ~ 1.0 $newMastery = max(0.0, min(1.0, $newMastery)); // 计算置信度(基于答题次数) $confidence = $this->calculateConfidence($attempts); // 判断趋势 $trend = $this->determineTrend($attempts); // 保存到数据库 DB::table('student_knowledge_mastery') ->updateOrInsert( ['student_id' => $studentId, 'kp_code' => $kpCode], [ 'mastery_level' => $newMastery, 'confidence_level' => $confidence, 'total_attempts' => ($historyMastery->total_attempts ?? 0) + $totalAttempts, 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + $correctAttempts, 'mastery_trend' => $trend, 'last_mastery_update' => now(), 'updated_at' => now(), ] ); return [ 'mastery' => round($newMastery, 4), 'confidence' => round($confidence, 4), 'trend' => $trend, 'total_attempts' => $totalAttempts, 'correct_attempts' => $correctAttempts, 'accuracy_rate' => round(($correctAttempts / $totalAttempts) * 100, 2), 'old_mastery' => $oldMastery, 'change' => round($weightedChange, 4), 'details' => [ 'exam_base_difficulty' => $examBaseDifficulty, 'total_change' => round($totalChange, 4), 'average_change' => round($averageChange, 4), 'weighted_change' => round($weightedChange, 4), ], ]; } /** * 难度映射:将0.0-1.0的浮点数难度映射为1-4等级 */ private function mapDifficultyToLevel(float $difficulty): int { if ($difficulty >= 0.0 && $difficulty < 0.25) { return 1; // 1级 } elseif ($difficulty >= 0.25 && $difficulty < 0.5) { return 2; // 2级 } elseif ($difficulty >= 0.5 && $difficulty < 0.75) { return 3; // 3级 } else { return 4; // 4级 } } /** * 根据难度关系计算权重变化 */ private function calculateWeightByDifficultyRelation(int $questionLevel, int $examBaseDifficulty, bool $isCorrect): float { if ($questionLevel > $examBaseDifficulty) { // 越级:题目难度 > 学案基准难度 return $isCorrect ? 0.15 : -0.05; } elseif ($questionLevel == $examBaseDifficulty) { // 适应:题目难度 = 学案基准难度 return $isCorrect ? 0.10 : -0.10; } else { // 降级:题目难度 < 学案基准难度 return $isCorrect ? 0.05 : -0.15; } } /** * 计算置信度 */ private function calculateConfidence(array $attempts): float { if (empty($attempts)) { return 0.0; } $totalAttempts = count($attempts); // 基于答题次数的置信度:答题越多,置信度越高 // 5次以下线性增长,5次以上增长放缓 if ($totalAttempts < 5) { $baseConfidence = $totalAttempts / 5.0; } else { $baseConfidence = 0.5 + (1.0 - 0.5) * (1 - exp(-($totalAttempts - 5) / 10)); } // 计算正确率 $correctAttempts = count(array_filter($attempts, fn($a) => $a['is_correct'])); $accuracyRate = $correctAttempts / $totalAttempts; // 正确率也影响置信度 $accuracyFactor = 0.5 + $accuracyRate * 0.5; // 综合置信度 $confidence = $baseConfidence * $accuracyFactor; return min($confidence, 1.0); } /** * 判断学习趋势 */ private function determineTrend(array $attempts): string { if (count($attempts) < 3) { return 'insufficient'; // 数据不足 } // 按时间排序 usort($attempts, function($a, $b) { $timeA = strtotime($a['completed_at'] ?? $a['created_at'] ?? 0); $timeB = strtotime($b['completed_at'] ?? $b['created_at'] ?? 0); return $timeA <=> $timeB; }); // 分为前后两部分 $midPoint = intdiv(count($attempts), 2); $firstHalf = array_slice($attempts, 0, $midPoint); $secondHalf = array_slice($attempts, $midPoint); // 计算前半部分和后半部分的正确率 $firstHalfCorrect = count(array_filter($firstHalf, fn($a) => $a['is_correct'])); $firstHalfAccuracy = $firstHalfCorrect / count($firstHalf); $secondHalfCorrect = count(array_filter($secondHalf, fn($a) => $a['is_correct'])); $secondHalfAccuracy = $secondHalfCorrect / count($secondHalf); $improvement = $secondHalfAccuracy - $firstHalfAccuracy; if ($improvement > 0.1) { return 'improving'; // 提升 } elseif ($improvement < -0.1) { return 'declining'; // 下降 } else { return 'stable'; // 稳定 } } /** * 获取学生的答题记录 */ private function getStudentAttempts(string $studentId, string $kpCode): array { $attempts = []; // 优先从 student_answer_steps 表获取(步骤级记录) $stepAttempts = DB::table('student_answer_steps') ->where('student_id', $studentId) ->where('kp_id', $kpCode) ->orderBy('created_at', 'asc') ->get(); foreach ($stepAttempts as $step) { $attempts[] = [ 'student_id' => $step->student_id, 'paper_id' => $step->exam_id, 'question_id' => $step->question_id, 'kp_code' => $step->kp_id, 'is_correct' => (bool) $step->is_correct, 'score_obtained' => $step->step_score, 'max_score' => $step->step_score, // 步骤分数本身就是满分 'created_at' => $step->created_at, ]; } // 如果没有步骤级记录,从 student_answer_questions 表获取(题目级记录) if (empty($attempts)) { $questionAttempts = DB::table('student_answer_questions') ->where('student_id', $studentId) ->orderBy('created_at', 'asc') ->get(); foreach ($questionAttempts as $question) { $attempts[] = [ 'student_id' => $question->student_id, 'paper_id' => $question->exam_id, 'question_id' => $question->question_id, 'kp_code' => $kpCode, // 使用传入的kpCode 'is_correct' => ($question->score_obtained ?? 0) > 0, 'score_obtained' => $question->score_obtained ?? 0, 'max_score' => $question->max_score ?? 0, 'created_at' => $question->created_at, ]; } } return $attempts; } /** * 批量更新学生掌握度 */ public function batchUpdateMastery(string $studentId, array $kpCodes): array { $results = []; foreach ($kpCodes as $kpCode) { $masteryData = $this->calculateMasteryLevel($studentId, $kpCode); // 保存到数据库 DB::table('student_knowledge_mastery') ->updateOrInsert( ['student_id' => $studentId, 'kp_code' => $kpCode], [ 'mastery_level' => $masteryData['mastery'], 'confidence_level' => $masteryData['confidence'], 'total_attempts' => $masteryData['total_attempts'], 'correct_attempts' => $masteryData['correct_attempts'], 'mastery_trend' => $masteryData['trend'], 'last_mastery_update' => now(), 'updated_at' => now(), ] ); $results[$kpCode] = $masteryData; } return $results; } /** * 获取学生所有知识点的掌握度概览 */ public function getStudentMasteryOverview(string $studentId): array { $masteryList = DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->get(); if ($masteryList->isEmpty()) { return [ 'total_knowledge_points' => 0, 'average_mastery_level' => 0.0, 'mastered_knowledge_points' => 0, 'good_knowledge_points' => 0, 'weak_knowledge_points' => 0, 'weak_knowledge_points_list' => [], 'details' => [], ]; } $masteryArray = $masteryList->toArray(); $total = count($masteryArray); $average = $masteryArray ? array_sum(array_column($masteryArray, 'mastery_level')) / $total : 0; $mastered = []; $good = []; $weak = []; foreach ($masteryArray as $item) { $level = floatval($item->mastery_level); if ($level >= self::MASTERY_THRESHOLD_MASTER) { $mastered[] = $item; } elseif ($level >= self::MASTERY_THRESHOLD_GOOD) { $good[] = $item; } else { $weak[] = $item; } } return [ 'total_knowledge_points' => $total, 'average_mastery_level' => round($average, 4), 'mastered_knowledge_points' => count($mastered), 'good_knowledge_points' => count($good), 'weak_knowledge_points' => count($weak), 'weak_knowledge_points_list' => $weak, 'details' => $masteryArray, ]; } /** * 【新功能】获取知识点层级关系 */ private function getKnowledgePointHierarchy(string $kpCode): ?array { try { $kp = DB::table('knowledge_points') ->where('kp_code', $kpCode) ->first(); if (!$kp) { return null; } return [ 'kp_code' => $kp->kp_code, 'parent_kp_code' => $kp->parent_kp_code, 'level' => $kp->level ?? 1, ]; } catch (\Exception $e) { Log::warning('获取知识点层级关系失败', [ 'kp_code' => $kpCode, 'error' => $e->getMessage(), ]); return null; } } /** * 【新功能】计算父节点掌握度(子节点平均值) */ public function calculateParentMastery(string $studentId, string $parentKpCode): float { try { // 获取所有子节点 $childKps = DB::table('knowledge_points') ->where('parent_kp_code', $parentKpCode) ->pluck('kp_code') ->toArray(); if (empty($childKps)) { // 如果没有子节点,返回0 return 0.0; } // 获取所有子节点的掌握度 $masteryLevels = []; foreach ($childKps as $childKpCode) { $mastery = DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->where('kp_code', $childKpCode) ->value('mastery_level'); if ($mastery !== null) { $masteryLevels[] = floatval($mastery); } } if (empty($masteryLevels)) { return 0.0; } // 计算算术平均数 $averageMastery = array_sum($masteryLevels) / count($masteryLevels); Log::info('父节点掌握度计算', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode, 'child_count' => count($childKps), 'mastery_count' => count($masteryLevels), 'average_mastery' => round($averageMastery, 4), 'child_masteries' => $masteryLevels, ]); return round($averageMastery, 4); } catch (\Exception $e) { Log::error('计算父节点掌握度失败', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode, 'error' => $e->getMessage(), ]); return 0.0; } } /** * 【增强】获取学生所有知识点的掌握度概览(支持父节点计算) */ public function getStudentMasteryOverviewWithHierarchy(string $studentId): array { $masteryList = DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->get(); if ($masteryList->isEmpty()) { return [ 'total_knowledge_points' => 0, 'average_mastery_level' => 0.0, 'mastered_knowledge_points' => 0, 'good_knowledge_points' => 0, 'weak_knowledge_points' => 0, 'weak_knowledge_points_list' => [], 'details' => [], 'parent_mastery_levels' => [], // 新增:父节点掌握度 ]; } $masteryArray = $masteryList->toArray(); $total = count($masteryArray); $average = $masteryArray ? array_sum(array_column($masteryArray, 'mastery_level')) / $total : 0; $mastered = []; $good = []; $weak = []; foreach ($masteryArray as $item) { $level = floatval($item->mastery_level); if ($level >= self::MASTERY_THRESHOLD_MASTER) { $mastered[] = $item; } elseif ($level >= self::MASTERY_THRESHOLD_GOOD) { $good[] = $item; } else { $weak[] = $item; } } // 【新功能】计算父节点掌握度 $parentMasteryLevels = []; $parentKpCodes = DB::table('knowledge_points') ->whereNotNull('parent_kp_code') ->distinct() ->pluck('parent_kp_code') ->toArray(); foreach ($parentKpCodes as $parentKpCode) { $parentMastery = $this->calculateParentMastery($studentId, $parentKpCode); $parentMasteryLevels[$parentKpCode] = $parentMastery; } return [ 'total_knowledge_points' => $total, 'average_mastery_level' => round($average, 4), 'mastered_knowledge_points' => count($mastered), 'good_knowledge_points' => count($good), 'weak_knowledge_points' => count($weak), 'weak_knowledge_points_list' => $weak, 'details' => $masteryArray, 'parent_mastery_levels' => $parentMasteryLevels, // 新增:父节点掌握度 ]; } }