| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Collection;
- /**
- * 知识掌握度计算引擎(PHP版本)
- * 基于学案基准难度的动态加减逻辑计算掌握度
- *
- * 新算法特点:
- * 1. 根据学案基准难度(筑基、提分、培优、竞赛)动态调整权重
- * 2. 难度映射:0.0-1.0 → 1-4级
- * 3. 权重计算:越级、适应、降级三种情况
- * 4. 父节点掌握度:子节点平均值
- */
- class MasteryCalculator
- {
- /**
- * 掌握度阈值配置
- */
- private const MASTERY_THRESHOLD_WEAK = 0.50; // 薄弱点阈值
- private const MASTERY_THRESHOLD_GOOD = 0.70; // 良好阈值
- private const MASTERY_THRESHOLD_MASTER = 0.85; // 掌握阈值
- /**
- * 最小正确率要求
- */
- private const MIN_CORRECT_RATE = 0.60;
- /**
- * 计算学生对指定知识点的掌握度
- *
- * @param string $studentId 学生ID
- * @param string $kpCode 知识点编码
- * @param array|null $attempts 答题记录(可选,默认从数据库查询)
- * @param int|null $examBaseDifficulty 学案基准难度(1-4级,1=筑基, 2=提分, 3=培优, 4=竞赛)
- * @return array 返回['mastery' => 掌握度, 'confidence' => 置信度, 'trend' => 趋势]
- */
- public function calculateMasteryLevel(string $studentId, string $kpCode, ?array $attempts = null, ?int $examBaseDifficulty = 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,
- ];
- }
- // 【新算法】使用学案基准难度的动态加减逻辑
- if ($examBaseDifficulty === null) {
- Log::warning('缺少学案基准难度,无法计算掌握度', [
- 'student_id' => $studentId,
- 'kp_code' => $kpCode,
- 'exam_base_difficulty' => $examBaseDifficulty,
- ]);
- return [
- 'mastery' => 0.0,
- 'confidence' => 0.0,
- 'trend' => 'insufficient',
- 'total_attempts' => count($attempts),
- 'correct_attempts' => 0,
- 'accuracy_rate' => 0.0,
- 'error' => '缺少学案基准难度参数',
- ];
- }
- $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 = 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,
- ];
- }
- /**
- * 【新功能】获取知识点层级关系
- */
- 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, // 新增:父节点掌握度
- ];
- }
- }
|