'boolean', 'force_review' => 'boolean', 'is_favorite' => 'boolean', 'in_retry_list' => 'boolean', 'reviewed_at' => 'datetime', 'next_review_at' => 'datetime', 'kp_ids' => 'array', 'skill_ids' => 'array', 'difficulty' => 'decimal:2', 'mastery_level' => 'decimal:2', ]; // 复习状态常量 const REVIEW_STATUS_PENDING = 'pending'; const REVIEW_STATUS_REVIEWED = 'reviewed'; const REVIEW_STATUS_MASTERED = 'mastered'; const REVIEW_STATUS_IGNORED = 'ignored'; // 错误类型常量 const ERROR_TYPE_CONCEPT = 'concept'; const ERROR_TYPE_CALCULATION = 'calculation'; const ERROR_TYPE_CARELESS = 'careless'; const ERROR_TYPE_LOGIC = 'logic'; const ERROR_TYPE_OTHER = 'other'; // 来源常量 const SOURCE_EXAM = 'exam'; const SOURCE_PRACTICE = 'practice'; const SOURCE_HOMEWORK = 'homework'; const SOURCE_TEST = 'test'; /** * 关联学生 */ public function student(): BelongsTo { return $this->belongsTo(Student::class, 'student_id', 'student_id'); } /** * 获取复习状态标签 */ public function getReviewStatusLabelAttribute(): string { return match ($this->review_status) { self::REVIEW_STATUS_PENDING => '待复习', self::REVIEW_STATUS_REVIEWED => '已复习', self::REVIEW_STATUS_MASTERED => '已掌握', self::REVIEW_STATUS_IGNORED => '已忽略', default => '未知', }; } /** * 获取错误类型标签 */ public function getErrorTypeLabelAttribute(): string { return match ($this->error_type) { self::ERROR_TYPE_CONCEPT => '概念错误', self::ERROR_TYPE_CALCULATION => '计算错误', self::ERROR_TYPE_CARELESS => '粗心错误', self::ERROR_TYPE_LOGIC => '逻辑错误', self::ERROR_TYPE_OTHER => '其他', default => '未知', }; } /** * 获取来源标签 */ public function getSourceLabelAttribute(): string { return match ($this->source) { self::SOURCE_EXAM => '考试', self::SOURCE_PRACTICE => '练习', self::SOURCE_HOMEWORK => '作业', self::SOURCE_TEST => '测试', default => '未知', }; } /** * 获取难度等级 */ public function getDifficultyLevelAttribute(): string { if (!$this->difficulty) { return '未知'; } return match (true) { $this->difficulty < 0.3 => '简单', $this->difficulty < 0.7 => '中等', default => '困难', }; } /** * 标记为已复习 */ public function markAsReviewed(): self { // 先递增 review_count $this->increment('review_count'); // 然后更新其他字段 $this->update([ 'review_status' => self::REVIEW_STATUS_REVIEWED, 'reviewed_at' => now(), ]); // ⚠️ 重要:重新加载模型数据 $this->refresh(); // 根据复习次数计算下次复习时间 $this->calculateNextReviewDate(); return $this; } /** * 标记为已掌握 */ public function markAsMastered(): self { $this->update([ 'review_status' => self::REVIEW_STATUS_MASTERED, 'mastery_level' => 1.0, ]); return $this; } /** * 切换收藏状态 */ public function toggleFavorite(): self { $this->update([ 'is_favorite' => !$this->is_favorite, ]); return $this; } /** * 加入重练清单 */ public function addToRetryList(): self { $this->update([ 'in_retry_list' => true, 'force_review' => true, ]); return $this; } /** * 从重练清单移除 */ public function removeFromRetryList(): self { $this->update([ 'in_retry_list' => false, ]); return $this; } /** * 计算下次复习时间(艾宾浩斯遗忘曲线) */ public function calculateNextReviewDate(): self { $intervals = [1, 2, 4, 7, 15, 30, 60]; // 复习间隔天数 $reviewCount = $this->review_count; // 如果复习次数超过间隔数组长度,使用最大间隔 $days = $intervals[min($reviewCount - 1, count($intervals) - 1)] ?? 60; $this->update([ 'next_review_at' => now()->addDays($days), ]); // ⚠️ 重要:重新加载模型数据 $this->refresh(); return $this; } /** * 作用域:按学生筛选 */ public function scopeForStudent($query, int|string $studentId) { return $query->where('student_id', $studentId); } /** * 作用域:按复习状态筛选 */ public function scopeByReviewStatus($query, string $status) { return $query->where('review_status', $status); } /** * 作用域:待复习 */ public function scopePending($query) { return $query->where('review_status', self::REVIEW_STATUS_PENDING); } /** * 作用域:已收藏 */ public function scopeFavorites($query) { return $query->where('is_favorite', true); } /** * 作用域:在重练清单 */ public function scopeInRetryList($query) { return $query->where('in_retry_list', true); } /** * 作用域:本周新增 */ public function scopeThisWeek($query) { return $query->whereBetween('created_at', [ now()->startOfWeek(), now()->endOfWeek(), ]); } /** * 作用域:按时间范围筛选 */ public function scopeInDateRange($query, Carbon $startDate, Carbon $endDate) { return $query->whereBetween('created_at', [$startDate, $endDate]); } /** * 作用域:按错误类型筛选 */ public function scopeByErrorType($query, string $errorType) { return $query->where('error_type', $errorType); } /** * 作用域:按知识点筛选 */ public function scopeByKnowledgePoint($query, string|array $kpIds) { if (is_array($kpIds)) { return $query->where(function ($q) use ($kpIds) { foreach ($kpIds as $kpId) { $q->orWhereJsonContains('kp_ids', $kpId); } }); } return $query->whereJsonContains('kp_ids', $kpIds); } /** * 获取统计摘要 */ public static function getSummary(int|string $studentId): array { $query = self::forStudent($studentId); return [ 'total' => (clone $query)->count(), 'pending' => (clone $query)->pending()->count(), 'reviewed' => (clone $query)->byReviewStatus(self::REVIEW_STATUS_REVIEWED)->count(), 'mastered' => (clone $query)->byReviewStatus(self::REVIEW_STATUS_MASTERED)->count(), 'favorites' => (clone $query)->favorites()->count(), 'in_retry_list' => (clone $query)->inRetryList()->count(), 'this_week' => (clone $query)->thisWeek()->count(), 'mastery_rate' => self::calculateMasteryRate($studentId), ]; } /** * 计算掌握率 */ public static function calculateMasteryRate(int|string $studentId): float { $total = self::forStudent($studentId)->count(); if ($total === 0) { return 0.0; } $mastered = self::forStudent($studentId) ->byReviewStatus(self::REVIEW_STATUS_MASTERED) ->count(); return round(($mastered / $total) * 100, 2); } /** * 获取错误模式分析 */ public static function getMistakePatterns(int|string $studentId): array { $query = self::forStudent($studentId); // 错误类型分布 $errorTypes = (clone $query) ->selectRaw('error_type, COUNT(*) as count') ->groupBy('error_type') ->pluck('count', 'error_type') ->toArray(); // 知识点分布 $knowledgePoints = self::forStudent($studentId) ->selectRaw('knowledge_point, COUNT(*) as count') ->groupBy('knowledge_point') ->orderByDesc('count') ->limit(10) ->pluck('count', 'knowledge_point') ->toArray(); // 来源分布 $sources = (clone $query) ->selectRaw('source, COUNT(*) as count') ->groupBy('source') ->pluck('count', 'source') ->toArray(); // 难度分布 $difficultyStats = (clone $query) ->selectRaw('AVG(difficulty) as avg_difficulty, COUNT(*) as total') ->first(); return [ 'error_types' => $errorTypes, 'knowledge_points' => $knowledgePoints, 'sources' => $sources, 'difficulty_stats' => [ 'average' => round($difficultyStats->avg_difficulty ?? 0, 2), 'total' => $difficultyStats->total ?? 0, ], 'weak_kps' => array_keys(array_slice($knowledgePoints, 0, 5, true)), 'top_error_types' => array_keys(array_slice($errorTypes, 0, 3, true)), ]; } }