'decimal:4', 'direct_mastery_level' => 'decimal:4', 'confidence_level' => 'decimal:4', 'mastery_change' => 'decimal:4', 'avg_time_seconds' => 'decimal:2', 'first_attempt_at' => 'datetime', 'last_attempt_at' => 'datetime', 'last_mastery_update' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /** * 关联学生 */ public function student(): BelongsTo { return $this->belongsTo(Student::class, 'student_id', 'student_id'); } /** * 关联知识点 */ public function knowledgePoint(): BelongsTo { return $this->belongsTo(KnowledgePoint::class, 'kp_code', 'kp_code'); } /** * 作用域:按学生筛选 */ public function scopeForStudent($query, string $studentId) { return $query->where('student_id', $studentId); } /** * 作用域:按知识点筛选 */ public function scopeForKnowledgePoint($query, string $kpCode) { return $query->where('kp_code', $kpCode); } /** * 作用域:薄弱点(掌握度低于阈值) */ public function scopeWeaknesses($query, float $threshold = 0.7) { return $query->where('mastery_level', '<', $threshold); } /** * 作用域:按掌握度排序 */ public function scopeOrderByMastery($query, string $direction = 'asc') { return $query->orderBy('mastery_level', $direction); } /** * 获取薄弱点列表 */ public static function getWeaknesses(string $studentId, float $threshold = 0.7, int $limit = 20): array { return self::forStudent($studentId) ->weaknesses($threshold) ->orderByMastery('asc') ->limit($limit) ->get() ->toArray(); } public static function allAtLeast(int $studentId, array $kpCodes, float $threshold): bool { if (empty($kpCodes)) { return false; } // 使用 DB::table 避免 Eloquent accessor 把数字转成文字标签 // 【新增】同时获取 direct_mastery_level,判断时优先使用 $records = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->whereIn('kp_code', $kpCodes) ->get(['kp_code', 'mastery_level', 'direct_mastery_level']) ->keyBy('kp_code'); foreach ($kpCodes as $kpCode) { $record = $records->get($kpCode); // 取 direct_mastery_level 和 mastery_level 的最大值 // 避免"学了之后反而从达标变成未达标"的问题 if ($record) { $direct = $record->direct_mastery_level; $mastery = (float) $record->mastery_level; $level = $direct !== null ? max((float) $direct, $mastery) : $mastery; } else { $level = 0.0; } if ($level < $threshold) { return false; } } return true; } /** * 判断所有知识点是否达标(跳过没有题目的知识点) * 用于章节摸底后的知识点学习流程 * * @param int $studentId 学生ID * @param array $kpCodes 知识点编码列表 * @param float $threshold 达标阈值(默认0.9) * @return bool 是否全部达标 */ public static function allAtLeastSkipNoQuestions(int $studentId, array $kpCodes, float $threshold = 0.9): bool { if (empty($kpCodes)) { return true; // 没有知识点,视为达标 } // 获取掌握度(使用 DB::table 避免 Eloquent accessor 把数字转成文字标签) // 【新增】同时获取 direct_mastery_level,判断时优先使用 $records = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->whereIn('kp_code', $kpCodes) ->get(['kp_code', 'mastery_level', 'direct_mastery_level']) ->keyBy('kp_code'); // 获取有题目的知识点 $kpCodesWithQuestions = \App\Models\Question::query() ->whereIn('kp_code', $kpCodes) ->distinct() ->pluck('kp_code') ->toArray(); $hasAnyKpWithQuestions = false; foreach ($kpCodes as $kpCode) { // 跳过没有题目的知识点 if (!in_array($kpCode, $kpCodesWithQuestions)) { continue; } $hasAnyKpWithQuestions = true; // 取 direct_mastery_level 和 mastery_level 的最大值 // 避免"学了之后反而从达标变成未达标"的问题 $record = $records->get($kpCode); if ($record) { $direct = $record->direct_mastery_level; $mastery = (float) $record->mastery_level; $level = $direct !== null ? max((float) $direct, $mastery) : $mastery; } else { $level = 0.0; } if ($level < $threshold) { return false; } } // 如果没有任何有题的知识点,视为达标 return $hasAnyKpWithQuestions ? true : true; } /** * 获取第一个未达标的知识点(跳过没有题目的知识点) * * @param int $studentId 学生ID * @param array $kpCodes 知识点编码列表(按顺序) * @param float $threshold 达标阈值(默认0.9) * @return string|null 第一个未达标的知识点编码,如果全部达标返回null */ public static function getFirstUnmasteredKpCode(int $studentId, array $kpCodes, float $threshold = 0.9): ?string { if (empty($kpCodes)) { return null; } // 获取掌握度(使用 DB::table 避免 Eloquent accessor 把数字转成文字标签) // 【新增】同时获取 direct_mastery_level,判断时优先使用 $records = \Illuminate\Support\Facades\DB::table('student_knowledge_mastery') ->where('student_id', $studentId) ->whereIn('kp_code', $kpCodes) ->get(['kp_code', 'mastery_level', 'direct_mastery_level']) ->keyBy('kp_code'); // 获取有题目的知识点 $kpCodesWithQuestions = \App\Models\Question::query() ->whereIn('kp_code', $kpCodes) ->distinct() ->pluck('kp_code') ->toArray(); foreach ($kpCodes as $kpCode) { // 跳过没有题目的知识点 if (!in_array($kpCode, $kpCodesWithQuestions)) { continue; } // 取 direct_mastery_level 和 mastery_level 的最大值 // 避免"学了之后反而从达标变成未达标"的问题 $record = $records->get($kpCode); if ($record) { $direct = $record->direct_mastery_level; $mastery = (float) $record->mastery_level; $level = $direct !== null ? max((float) $direct, $mastery) : $mastery; } else { $level = 0.0; } if ($level < $threshold) { return $kpCode; } } return null; // 全部达标 } /** * 计算掌握度等级 */ public function getMasteryLevelAttribute($value): string { if ($value >= 0.85) { return '优秀'; } elseif ($value >= 0.70) { return '良好'; } elseif ($value >= 0.50) { return '及格'; } else { return '薄弱'; } } /** * 获取趋势标签 */ public function getTrendLabelAttribute(): string { return match ($this->mastery_trend) { 'improving' => '上升', 'declining' => '下降', 'stable' => '稳定', 'insufficient' => '数据不足', default => '未知', }; } /** * 计算成功率 */ public function getSuccessRateAttribute(): float { if ($this->total_attempts <= 0) { return 0.0; } return round(($this->correct_attempts / $this->total_attempts) * 100, 2); } }