| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- <?php
- namespace App\Models;
- use Illuminate\Database\Eloquent\Factories\HasFactory;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Database\Eloquent\Relations\BelongsTo;
- use Illuminate\Database\Eloquent\Relations\HasMany;
- use Illuminate\Support\Carbon;
- class MistakeRecord extends Model
- {
- use HasFactory;
- protected $table = 'mistake_records';
- protected $fillable = [
- 'student_id',
- 'question_id',
- 'paper_id',
- 'source',
- 'question_text',
- 'student_answer',
- 'correct_answer',
- 'knowledge_point',
- 'explanation',
- 'is_corrected',
- 'review_status',
- 'review_count',
- 'force_review',
- 'is_favorite',
- 'in_retry_list',
- 'reviewed_at',
- 'next_review_at',
- 'error_type',
- 'kp_ids',
- 'skill_ids',
- 'difficulty',
- 'importance',
- 'mastery_level',
- 'remark',
- ];
- protected $casts = [
- 'is_corrected' => '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)),
- ];
- }
- }
|