0.8, // 简单题目权重 0.60 => 1.0, // 中等题目权重 0.85 => 1.3, // 困难题目权重 ]; /** * 时间效率基准值(秒) * 不同难度题目的平均完成时间 */ private const TIME_BASELINE = [ 0.30 => 60, // 简单题平均用时 0.60 => 120, // 中等题平均用时 0.85 => 180, // 困难题平均用时 ]; /** * 掌握度阈值配置 */ private const MASTERY_THRESHOLD_WEAK = 0.50; // 薄弱点阈值 private const MASTERY_THRESHOLD_GOOD = 0.70; // 良好阈值 private const MASTERY_THRESHOLD_MASTER = 0.85; // 掌握阈值 /** * 最小练习题目数量 */ private const MIN_PRACTICE_QUESTIONS = 5; /** * 最小正确率要求 */ private const MIN_CORRECT_RATE = 0.60; /** * 计算学生对指定知识点的掌握度 * * @param string $studentId 学生ID * @param string $kpCode 知识点编码 * @param array|null $attempts 答题记录(可选,默认从数据库查询) * @return array 返回['mastery' => 掌握度, 'confidence' => 置信度, 'trend' => 趋势] */ public function calculateMasteryLevel(string $studentId, string $kpCode, ?array $attempts = null): array { // 如果没有提供答题记录,从数据库查询 if ($attempts === null) { $attempts = $this->getStudentAttempts($studentId, $kpCode); } if (empty($attempts)) { return [ 'mastery' => 0.0, 'confidence' => 0.0, 'trend' => 'insufficient', 'total_attempts' => 0, 'correct_attempts' => 0, 'accuracy_rate' => 0.0, ]; } // 1. 计算基础正确率 $accuracyRate = $this->calculateAccuracyRate($attempts); // 2. 计算难度加权分数 $difficultyScore = $this->calculateDifficultyScore($attempts); // 3. 计算时间效率分数 $timeScore = $this->calculateTimeEfficiency($attempts); // 4. 计算技能熟练度影响 $skillFactor = $this->calculateSkillFactor($attempts); // 5. 应用遗忘曲线衰减 $decayFactor = $this->calculateDecayFactor($attempts); // 6. 综合计算掌握度 $baseMastery = $accuracyRate * $difficultyScore * $timeScore * $skillFactor * $decayFactor; // 7. 计算置信度 $confidence = $this->calculateConfidence($attempts); // 8. 判断趋势 $trend = $this->determineTrend($attempts); // 9. 计算统计信息 $totalAttempts = count($attempts); $correctAttempts = count(array_filter($attempts, fn($a) => $a['is_correct'])); Log::info('MasteryCalculator::calculateMasteryLevel', [ 'student_id' => $studentId, 'kp_code' => $kpCode, 'total_attempts' => $totalAttempts, 'correct_attempts' => $correctAttempts, 'accuracy_rate' => $accuracyRate, 'difficulty_score' => $difficultyScore, 'time_score' => $timeScore, 'skill_factor' => $skillFactor, 'decay_factor' => $decayFactor, 'final_mastery' => $baseMastery, 'confidence' => $confidence, 'trend' => $trend, ]); return [ 'mastery' => round($baseMastery, 4), 'confidence' => round($confidence, 4), 'trend' => $trend, 'total_attempts' => $totalAttempts, 'correct_attempts' => $correctAttempts, 'accuracy_rate' => round($accuracyRate * 100, 2), 'details' => [ 'accuracy_rate' => $accuracyRate, 'difficulty_score' => $difficultyScore, 'time_score' => $timeScore, 'skill_factor' => $skillFactor, 'decay_factor' => $decayFactor, ], ]; } /** * 计算正确率 */ private function calculateAccuracyRate(array $attempts): float { if (empty($attempts)) { return 0.0; } $totalAttempts = count($attempts); $correctAttempts = count(array_filter($attempts, fn($a) => $a['is_correct'])); $partialAttempts = count(array_filter($attempts, function($a) { return isset($a['partial_score']) && floatval($a['partial_score']) > 0; })); // 部分正确按50%计算 $correctScore = $correctAttempts + $partialAttempts * 0.5; $accuracy = $correctScore / $totalAttempts; return min($accuracy, 1.0); } /** * 计算难度加权分数 */ private function calculateDifficultyScore(array $attempts): float { if (empty($attempts)) { return 0.0; } $weightedSum = 0.0; $totalWeight = 0.0; foreach ($attempts as $attempt) { $difficulty = floatval($attempt['question_difficulty'] ?? 0.6); $weight = self::DIFFICULTY_WEIGHTS[$difficulty] ?? 1.0; $score = $attempt['is_correct'] ? 1.0 : 0.0; $weightedSum += $score * $weight; $totalWeight += $weight; } if ($totalWeight == 0) { return 0.0; } return $weightedSum / $totalWeight; } /** * 计算时间效率分数 */ private function calculateTimeEfficiency(array $attempts): float { if (empty($attempts)) { return 0.0; } $efficiencyScores = []; foreach ($attempts as $attempt) { $difficulty = floatval($attempt['question_difficulty'] ?? 0.6); $baseline = self::TIME_BASELINE[$difficulty] ?? 120; $actualTime = floatval($attempt['attempt_time_seconds'] ?? 120); // 时间效率:基准时间/实际用时,最大不超过1.5 $efficiency = min($baseline / max($actualTime, 1), 1.5); $efficiencyScores[] = $efficiency; } // 取最近5次答题的平均效率 $recentEfficiency = array_slice($efficiencyScores, -5); $avgEfficiency = array_sum($recentEfficiency) / count($recentEfficiency); return min($avgEfficiency, 1.0); } /** * 计算技能熟练度影响 */ private function calculateSkillFactor(array $attempts, ?array $skillProficiency = null): float { // 简化实现:如果有技能熟练度数据,使用它;否则返回1.0 if ($skillProficiency && !empty($skillProficiency)) { // 计算平均技能熟练度 $avgProficiency = array_sum($skillProficiency) / count($skillProficiency); // 技能熟练度加权:范围0.8-1.2 return 0.8 + ($avgProficiency * 0.4); } return 1.0; } /** * 计算遗忘曲线衰减因子 */ private function calculateDecayFactor(array $attempts): float { if (empty($attempts)) { return 0.0; } // 获取最近一次答题时间 $lastAttemptTime = null; foreach ($attempts as $attempt) { $attemptTime = strtotime($attempt['completed_at'] ?? $attempt['created_at'] ?? 'now'); if ($lastAttemptTime === null || $attemptTime > $lastAttemptTime) { $lastAttemptTime = $attemptTime; } } if ($lastAttemptTime === null) { return 1.0; } // 计算距离现在的天数 $daysSinceLastAttempt = (time() - $lastAttemptTime) / 86400; // 86400 = 24*60*60 // 遗忘曲线:每天衰减2%,最大衰减50% $decayRate = min($daysSinceLastAttempt * 0.02, 0.5); $decayFactor = 1.0 - $decayRate; return max($decayFactor, 0.5); // 最低保持50% } /** * 计算置信度 */ 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)); } // 正确率也影响置信度 $accuracyRate = $this->calculateAccuracyRate($attempts); $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); $firstHalfAccuracy = $this->calculateAccuracyRate($firstHalf); $secondHalfAccuracy = $this->calculateAccuracyRate($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 = DB::table('student_attempts') ->where('student_id', $studentId) ->where('kp_code', $kpCode) ->orderBy('created_at', 'asc') ->get(); return $attempts->map(function ($item) { return (array) $item; })->toArray(); } /** * 批量更新学生掌握度 */ 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, ]; } }