掌握度, '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, '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'], ]); 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.0; // 默认 0.0 // 统计正确和错误次数 $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 ]); } // 【公司要求】数值更新:newMastery = oldMastery + change $newMastery = $oldMastery + $totalChange; // 【公司要求】边界限制:0.0 ~ 1.0 $newMastery = max(0.0, min(1.0, $newMastery)); // 【公司要求】保存到数据库(只保存核心掌握度数据) DB::table('student_knowledge_mastery') ->updateOrInsert( ['student_id' => $studentId, 'kp_code' => $kpCode], [ 'mastery_level' => $newMastery, 'confidence_level' => 0.0, // 不再计算置信度 'total_attempts' => ($historyMastery->total_attempts ?? 0) + $totalAttempts, 'correct_attempts' => ($historyMastery->correct_attempts ?? 0) + $correctAttempts, 'mastery_trend' => 'stable', // 不再判断趋势,统一设为stable 'last_mastery_update' => now(), 'updated_at' => now(), ] ); // 【公司要求】返回值:只返回核心掌握度数据 return [ 'mastery' => round($newMastery, 4), 'total_attempts' => $totalAttempts, 'correct_attempts' => $correctAttempts, 'accuracy_rate' => round(($correctAttempts / $totalAttempts) * 100, 2), 'old_mastery' => $oldMastery, 'change' => round($totalChange, 4), 'details' => [ 'exam_base_difficulty' => $examBaseDifficulty, 'total_change' => round($totalChange, 4) ], ]; } /** * 【公司要求】难度映射:将题目中0.0-1.0的浮点数难度映射为1-4等级 * * 公司要求: * 0.0 ~ 0.25 -> 1级(筑基) * 0.25 ~ 0.5 -> 2级(提分) * 0.5 ~ 0.75 -> 3级(培优) * 0.75 ~ 1.0 -> 4级(竞赛) * * @param float $difficulty 题目难度(0.0-1.0浮点数) * @return int 难度等级(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级(竞赛) } } /** * 【公司要求】根据难度关系计算权重变化 * * 公司要求的三种难度关系及权重: * 1. 越级(Question > {学案基准难度}):对 +0.15 / 错 -0.05 * 2. 适应(Question = {学案基准难度}):对 +0.10 / 错 -0.10 * 3. 降级(Question < {学案基准难度}):对 +0.05 / 错 -0.15 * * 学案基准难度获取: * - 来源:试卷表(papers.difficulty_category) * - 映射:筑基→1级,提分→2级,培优→3级,竞赛→4级 * * @param int $questionLevel 题目难度等级(1-4) * @param int $examBaseDifficulty 学案基准难度(1-4) * @param bool $isCorrect 答题是否正确 * @return float 权重变化值 */ 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 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) { // 【关键修复】从题目表查询题目难度 $questionDifficulty = 0.6; // 默认难度 if ($step->question_id) { $question = DB::table('questions') ->where('id', $step->question_id) ->orWhere('question_code', $step->question_id) ->first(); if ($question && $question->difficulty !== null) { $questionDifficulty = floatval($question->difficulty); } } $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, // 步骤分数本身就是满分 'question_difficulty' => $questionDifficulty, // 【新增】题目难度 '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) { // 【关键修复】从题目表查询题目难度 $questionDifficulty = 0.6; // 默认难度 if ($question->question_id) { $questionInfo = DB::table('questions') ->where('id', $question->question_id) ->orWhere('question_code', $question->question_id) ->first(); if ($questionInfo && $questionInfo->difficulty !== null) { $questionDifficulty = floatval($questionInfo->difficulty); } } $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, 'question_difficulty' => $questionDifficulty, // 【新增】题目难度 '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; } } /** * 【公司要求】计算父节点掌握度(所有子节点算术平均值) * * 公司要求: * 1. 查询结构:通过 knowledge_points 表获取知识点的层级关系(parent_kp_code) * 2. 平均值计算:如果一个知识点是父节点,它的掌握度不再从数据库直接读, * 而是实时计算其下所有子节点掌握度的算术平均数 * 3. 多级递归:支持多级结构,能够从最底层逐级向上求平均 * * 递归计算流程: * - 从最底层叶子节点开始,逐级向上计算每个父节点的掌握度 * - 父节点掌握度 = 所有直接子节点掌握度的算术平均数 * * @param string $studentId 学生ID * @param string $parentKpCode 父节点编码 * @param int $maxDepth 最大递归深度(默认10级) * @return float 父节点掌握度(0.0-1.0) */ public function calculateParentMastery(string $studentId, string $parentKpCode, int $maxDepth = 3): float { return $this->calculateParentMasteryRecursive($studentId, $parentKpCode, 1, $maxDepth); } /** * 【公司要求】递归计算父节点掌握度(多级结构支持) * * 多级递归计算流程: * 1. 获取父节点的所有直接子节点 * 2. 对每个子节点: * - 如果子节点也是父节点,递归计算其掌握度 * - 如果子节点是叶子节点,从 student_knowledge_mastery 表读取掌握度 * 3. 计算所有子节点掌握度的算术平均数 * 4. 返回父节点掌握度 * * @param string $studentId 学生ID * @param string $parentKpCode 父节点编码 * @param int $currentDepth 当前递归深度 * @param int $maxDepth 最大递归深度(防止无限递归) * @return float 父节点掌握度 */ private function calculateParentMasteryRecursive(string $studentId, string $parentKpCode, int $currentDepth, int $maxDepth): float { try { // 【公司要求】防止无限递归 if ($currentDepth > $maxDepth) { Log::warning('父节点掌握度计算达到最大递归深度', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode, 'current_depth' => $currentDepth, 'max_depth' => $maxDepth ]); return 0.0; } // 【公司要求】查询结构:通过 knowledge_points 表获取知识点的层级关系(parent_kp_code) // 获取所有直接子节点 $childKps = DB::table('knowledge_points') ->where('parent_kp_code', $parentKpCode) ->pluck('kp_code') ->toArray(); if (empty($childKps)) { // 如果没有子节点,返回0 Log::debug('父节点没有子节点,返回0', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode ]); return 0.0; } // 【公司要求】多级递归:支持多级结构,能够从最底层逐级向上求平均 // 获取所有子节点的掌握度(递归计算) $masteryLevels = []; foreach ($childKps as $childKpCode) { // 【公司要求】多级递归:递归计算子节点掌握度 $childMastery = $this->calculateParentMasteryRecursive($studentId, $childKpCode, $currentDepth + 1, $maxDepth); // 如果子节点没有子节点,则从student_knowledge_mastery表读取 if ($childMastery == 0.0) { $mastery = DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->where('kp_code', $childKpCode) ->value('mastery_level'); if ($mastery !== null) { $masteryLevels[] = floatval($mastery); } } else { // 子节点有子节点,使用递归计算的结果 $masteryLevels[] = $childMastery; } } if (empty($masteryLevels)) { Log::warning('父节点所有子节点都没有掌握度数据', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode, 'child_count' => count($childKps) ]); return 0.0; } // 【公司要求】平均值计算:父节点掌握度 = 所有子节点掌握度的算术平均数 $averageMastery = array_sum($masteryLevels) / count($masteryLevels); $result = round($averageMastery, 4); Log::info('父节点掌握度计算完成', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode, 'child_count' => count($childKps), 'mastery_count' => count($masteryLevels), 'average_mastery' => $result, 'child_masteries' => $masteryLevels, 'current_depth' => $currentDepth ]); return $result; } catch (\Exception $e) { Log::error('计算父节点掌握度失败', [ 'student_id' => $studentId, 'parent_kp_code' => $parentKpCode, 'current_depth' => $currentDepth, '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, // 新增:父节点掌握度 ]; } }