learningAnalyticsBase = ''; $this->timeout = 20; } /** * 新增错题 */ public function createMistake(array $payload): array { $studentId = $payload['student_id'] ?? null; $questionId = $payload['question_id'] ?? null; $paperId = $payload['paper_id'] ?? null; $myAnswer = $payload['my_answer'] ?? null; $correctAnswer = $payload['correct_answer'] ?? null; $questionText = $payload['question_text'] ?? null; $knowledgePoint = $payload['knowledge_point'] ?? null; $explanation = $payload['explanation'] ?? null; $kpIds = $payload['kp_ids'] ?? null; $source = $payload['source'] ?? MistakeRecord::SOURCE_PRACTICE; $happenedAt = $payload['happened_at'] ?? now(); if (!$studentId) { throw new \InvalidArgumentException('学生ID不能为空'); } // 使用事务确保数据一致性 return \DB::transaction(function () use ( $studentId, $questionId, $paperId, $myAnswer, $correctAnswer, $questionText, $knowledgePoint, $explanation, $kpIds, $source, $happenedAt ) { // 检查是否已存在相同错题(避免重复) $query = MistakeRecord::where('student_id', $studentId) ->where('source', $source) ->where('created_at', '>=', now()->subDay()); // 如果有 question_id,用它去重;否则用 question_text if ($questionId) { $query->where('question_id', $questionId); } elseif ($questionText) { $query->where('question_text', $questionText); } $existingMistake = $query->first(); if ($existingMistake) { return [ 'duplicate' => true, 'mistake_id' => $existingMistake->id, 'message' => '错题已存在', ]; } // 创建错题记录 $mistake = MistakeRecord::create([ 'student_id' => $studentId, 'question_id' => $questionId, 'paper_id' => $paperId, 'student_answer' => $myAnswer, 'correct_answer' => $correctAnswer, 'question_text' => $questionText, 'knowledge_point' => $knowledgePoint, 'explanation' => $explanation, 'kp_ids' => $kpIds, 'source' => $source, 'created_at' => $happenedAt, 'review_status' => MistakeRecord::REVIEW_STATUS_PENDING, 'review_count' => 0, ]); // 清除相关缓存 $this->clearCache($studentId); return [ 'duplicate' => false, 'mistake_id' => $mistake->id, 'created_at' => $mistake->created_at, ]; }); } /** * 获取错题列表 */ public function listMistakes(array $params = []): array { $studentId = $params['student_id'] ?? null; $page = (int) ($params['page'] ?? 1); $perPage = (int) ($params['per_page'] ?? 20); if (!$studentId) { return [ 'data' => [], 'meta' => ['total' => 0, 'page' => $page, 'per_page' => $perPage], ]; } // 构建缓存键 $cacheKey = $this->buildCacheKey('list', $params); // 尝试从缓存获取 if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } try { $query = MistakeRecord::forStudent($studentId) ->with(['student']) // 预加载学生信息 ->orderByDesc('created_at'); // 应用筛选条件 $this->applyFilters($query, $params); // 获取总数 $total = $query->count(); // 分页获取数据 $mistakes = $query->skip(($page - 1) * $perPage) ->take($perPage) ->get(); // 转换数据格式 $data = $mistakes->map(function ($mistake) { return $this->transformMistakeRecord($mistake); })->toArray(); $result = [ 'data' => $data, 'meta' => [ 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'last_page' => (int) ceil($total / $perPage), ], ]; // 缓存结果 Cache::put($cacheKey, $result, self::CACHE_TTL_LIST); return $result; } catch (\Throwable $e) { Log::error('获取错题列表失败', [ 'student_id' => $studentId, 'error' => $e->getMessage(), 'params' => $params, ]); return [ 'data' => [], 'meta' => ['total' => 0, 'page' => $page, 'per_page' => $perPage], ]; } } /** * 获取错题详情 */ public function getMistakeDetail(string $mistakeId, ?string $studentId = null): array { try { $query = MistakeRecord::with(['student']); if ($studentId) { $query->forStudent($studentId); } $mistake = $query->find($mistakeId); if (!$mistake) { return []; } return $this->transformMistakeRecord($mistake, true); } catch (\Throwable $e) { Log::error('获取错题详情失败', [ 'mistake_id' => $mistakeId, 'student_id' => $studentId, 'error' => $e->getMessage(), ]); return []; } } /** * 获取错题统计概要 */ public function summarize(string $studentId): array { $cacheKey = "mistake_book:summary:{$studentId}"; return Cache::remember($cacheKey, self::CACHE_TTL_SUMMARY, function () use ($studentId) { return MistakeRecord::getSummary($studentId); }); } /** * 获取错误模式分析 */ public function getMistakePatterns(string $studentId): array { $cacheKey = "mistake_book:patterns:{$studentId}"; return Cache::remember($cacheKey, self::CACHE_TTL_PATTERNS, function () use ($studentId) { return MistakeRecord::getMistakePatterns($studentId); }); } /** * 收藏/取消收藏错题 */ public function toggleFavorite(string $mistakeId, bool $favorite = true): bool { try { $mistake = MistakeRecord::find($mistakeId); if (!$mistake) { return false; } $mistake->update(['is_favorite' => $favorite]); // 清除缓存 $this->clearCache($mistake->student_id); return true; } catch (\Throwable $e) { Log::error('收藏错题失败', [ 'mistake_id' => $mistakeId, 'favorite' => $favorite, 'error' => $e->getMessage(), ]); return false; } } /** * 标记已复习 */ public function markReviewed(string $mistakeId): bool { try { $mistake = MistakeRecord::find($mistakeId); if (!$mistake) { return false; } $mistake->markAsReviewed(); // 清除缓存 $this->clearCache($mistake->student_id); return true; } catch (\Throwable $e) { Log::error('标记已复习失败', [ 'mistake_id' => $mistakeId, 'error' => $e->getMessage(), ]); return false; } } /** * 修改复习状态 */ public function updateReviewStatus(string $mistakeId, string $action = 'increment', bool $forceReview = false): array { try { $mistake = MistakeRecord::find($mistakeId); if (!$mistake) { return [ 'success' => false, 'error' => '错题记录不存在', ]; } match ($action) { 'increment' => $mistake->markAsReviewed(), 'mastered' => $mistake->markAsMastered(), 'reset' => $mistake->update([ 'review_status' => MistakeRecord::REVIEW_STATUS_PENDING, 'review_count' => 0, 'mastery_level' => null, 'reviewed_at' => null, 'next_review_at' => null, ]), default => throw new \InvalidArgumentException('无效的操作类型'), }; // ⚠️ 重要:重新加载模型数据以获取最新状态 $mistake->refresh(); return [ 'success' => true, 'mistake_id' => $mistakeId, 'review_status' => $mistake->review_status, 'review_count' => $mistake->review_count, 'reviewed_at' => $mistake->reviewed_at?->toISOString(), 'next_review_at' => $mistake->next_review_at?->toISOString(), 'mastery_level' => $mistake->mastery_level, 'message' => '复习状态更新成功', ]; } catch (\Throwable $e) { Log::error('更新复习状态失败', [ 'mistake_id' => $mistakeId, 'action' => $action, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } /** * 获取复习状态 */ public function getReviewStatus(string $mistakeId): array { try { $mistake = MistakeRecord::find($mistakeId); if (!$mistake) { return [ 'success' => false, 'error' => '错题记录不存在', ]; } return [ 'success' => true, 'mistake_id' => $mistakeId, 'review_status' => $mistake->review_status, 'review_count' => $mistake->review_count, 'reviewed_at' => $mistake->reviewed_at?->toISOString(), 'next_review_at' => $mistake->next_review_at?->toISOString(), 'mastery_level' => $mistake->mastery_level, 'force_review' => $mistake->force_review, ]; } catch (\Throwable $e) { Log::error('获取复习状态失败', [ 'mistake_id' => $mistakeId, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } /** * 增加复习次数 */ public function incrementReviewCount(string $mistakeId, bool $forceReview = false): array { return $this->updateReviewStatus($mistakeId, 'increment', $forceReview); } /** * 重置为强制复习状态 */ public function resetReviewStatus(string $mistakeId): array { return $this->updateReviewStatus($mistakeId, 'reset'); } /** * 添加到重练清单 */ public function addToRetryList(string $mistakeId): bool { try { $mistake = MistakeRecord::find($mistakeId); if (!$mistake) { return false; } $mistake->addToRetryList(); // 清除缓存 $this->clearCache($mistake->student_id); return true; } catch (\Throwable $e) { Log::error('加入重练清单失败', [ 'mistake_id' => $mistakeId, 'error' => $e->getMessage(), ]); return false; } } /** * 批量操作错题 */ public function batchOperation(array $mistakeIds, string $operation, array $params = []): array { if (empty($mistakeIds)) { return [ 'success' => false, 'error' => '请选择要操作的错题', ]; } try { $mistakes = MistakeRecord::whereIn('id', $mistakeIds)->get(); $successCount = 0; $errors = []; foreach ($mistakes as $mistake) { try { match ($operation) { 'favorite' => $mistake->toggleFavorite(), 'reviewed' => $mistake->markAsReviewed(), 'mastered' => $mistake->markAsMastered(), 'retry_list' => $mistake->addToRetryList(), 'remove_retry_list' => $mistake->removeFromRetryList(), 'set_error_type' => $mistake->update(['error_type' => $params['error_type'] ?? null]), 'set_importance' => $mistake->update(['importance' => $params['importance'] ?? 5]), default => throw new \InvalidArgumentException('不支持的操作'), }; $successCount++; } catch (\Throwable $e) { $errors[] = [ 'mistake_id' => $mistake->id, 'error' => $e->getMessage(), ]; } } // 清除缓存 if ($successCount > 0) { $studentIds = $mistakes->pluck('student_id')->unique(); foreach ($studentIds as $studentId) { $this->clearCache($studentId); } } return [ 'success' => true, 'total' => count($mistakeIds), 'success_count' => $successCount, 'error_count' => count($errors), 'errors' => $errors, ]; } catch (\Throwable $e) { Log::error('批量操作失败', [ 'operation' => $operation, 'mistake_ids' => $mistakeIds, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } /** * 基于错题推荐练习题(本地实现) */ public function recommendPractice(string $studentId, array $kpIds = [], array $skillIds = []): array { try { // 获取学生未掌握的知识点 $weakKnowledgePoints = MistakeRecord::forStudent($studentId) ->where('review_status', '!=', MistakeRecord::REVIEW_STATUS_MASTERED) ->pluck('knowledge_point') ->filter() ->unique() ->values() ->toArray(); // 合并传入的知识点 $allKpIds = array_unique(array_merge($kpIds, $weakKnowledgePoints)); // TODO: 这里应该调用本地题库服务 // 目前返回模拟数据 $recommendations = []; foreach (array_slice($allKpIds, 0, 5) as $kpId) { $recommendations[] = [ 'id' => "rec_{$kpId}_{$studentId}", 'kp_code' => $kpId, 'question_text' => "针对知识点 {$kpId} 的练习题", 'difficulty' => 0.5, 'source' => 'recommendation', ]; } return ['data' => $recommendations]; } catch (\Throwable $e) { Log::error('推荐练习题失败', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); return ['data' => []]; } } /** * 获取仪表板快照数据 */ public function getPanelSnapshot(string $studentId, int $limit = 5): array { try { $recentMistakes = MistakeRecord::forStudent($studentId) ->orderByDesc('created_at') ->limit($limit) ->get() ->map(fn($m) => $this->transformMistakeRecord($m)) ->toArray(); $patterns = $this->getMistakePatterns($studentId); $summary = $this->summarize($studentId); return [ 'recent' => $recentMistakes, 'weak_skills' => array_slice($patterns['error_types'] ?? [], 0, 5, true), 'weak_kps' => array_slice($patterns['knowledge_points'] ?? [], 0, 5, true), 'error_types' => $patterns['error_types'] ?? [], 'stats' => $summary, ]; } catch (\Throwable $e) { Log::error('获取快照数据失败', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); return [ 'recent' => [], 'weak_skills' => [], 'weak_kps' => [], 'error_types' => [], 'stats' => [ 'total' => 0, 'pending' => 0, 'this_week' => 0, ], ]; } } /** * 应用筛选条件 */ private function applyFilters($query, array $params): void { // 知识点筛选 if (!empty($params['kp_ids'])) { $kpIds = is_array($params['kp_ids']) ? $params['kp_ids'] : explode(',', $params['kp_ids']); $query->byKnowledgePoint($kpIds); } // 技能筛选 if (!empty($params['skill_ids'])) { $skillIds = is_array($params['skill_ids']) ? $params['skill_ids'] : explode(',', $params['skill_ids']); $query->where(function ($q) use ($skillIds) { foreach ($skillIds as $skillId) { $q->orWhereJsonContains('skill_ids', $skillId); } }); } // 错误类型筛选 if (!empty($params['error_types'])) { $errorTypes = is_array($params['error_types']) ? $params['error_types'] : explode(',', $params['error_types']); $query->whereIn('error_type', $errorTypes); } // 时间范围筛选 if (!empty($params['time_range'])) { match ($params['time_range']) { 'last_7' => $query->where('created_at', '>=', now()->subDays(7)), 'last_30' => $query->where('created_at', '>=', now()->subDays(30)), 'last_90' => $query->where('created_at', '>=', now()->subDays(90)), default => null, }; } // 自定义时间范围 if (!empty($params['start_date'])) { $query->whereDate('created_at', '>=', $params['start_date']); } if (!empty($params['end_date'])) { $query->whereDate('created_at', '<=', $params['end_date']); } // 复习状态筛选 if (!empty($params['unreviewed_only'])) { $query->pending(); } if (!empty($params['favorite_only'])) { $query->favorites(); } if (!empty($params['in_retry_list_only'])) { $query->inRetryList(); } // 排序 $sortBy = $params['sort_by'] ?? 'created_at_desc'; match ($sortBy) { 'created_at_asc' => $query->orderBy('created_at'), 'created_at_desc' => $query->orderByDesc('created_at'), 'review_status_asc' => $query->orderBy('review_status'), 'difficulty_desc' => $query->orderByDesc('difficulty'), default => null, }; } /** * 转换错题记录格式 */ private function transformMistakeRecord(MistakeRecord $mistake, bool $detailed = false): array { $data = [ 'id' => $mistake->id, 'student_id' => $mistake->student_id, 'question_id' => $mistake->question_id, 'paper_id' => $mistake->paper_id, 'question_text' => $mistake->question_text, 'student_answer' => $mistake->student_answer, 'correct_answer' => $mistake->correct_answer, 'knowledge_point' => $mistake->knowledge_point, 'explanation' => $mistake->explanation, 'source' => $mistake->source, 'source_label' => $mistake->source_label, 'created_at' => $mistake->created_at?->toISOString(), 'review_status' => $mistake->review_status, 'review_status_label' => $mistake->review_status_label, 'review_count' => $mistake->review_count, 'is_favorite' => $mistake->is_favorite, 'in_retry_list' => $mistake->in_retry_list, 'reviewed_at' => $mistake->reviewed_at?->toISOString(), 'next_review_at' => $mistake->next_review_at?->toISOString(), 'error_type' => $mistake->error_type, 'error_type_label' => $mistake->error_type_label, 'kp_ids' => $mistake->kp_ids, 'skill_ids' => $mistake->skill_ids, 'difficulty' => $mistake->difficulty, 'difficulty_level' => $mistake->difficulty_level, 'importance' => $mistake->importance, 'mastery_level' => $mistake->mastery_level, ]; if ($detailed) { $data['student'] = [ 'id' => $mistake->student?->student_id, 'name' => $mistake->student?->name, 'grade' => $mistake->student?->grade, 'class_name' => $mistake->student?->class_name, ]; } return $data; } /** * 构建缓存键 */ private function buildCacheKey(string $type, array $params): string { $key = "mistake_book:{$type}:" . md5(serialize($params)); return $key; } /** * 清除缓存 */ private function clearCache(string|int $studentId): void { try { $patterns = [ "mistake_book:list:{$studentId}", "mistake_book:summary:{$studentId}", "mistake_book:patterns:{$studentId}", ]; foreach ($patterns as $key) { Cache::forget($key); } } catch (\Exception $e) { // 缓存清除失败不影响主流程 Log::debug('缓存清除失败(可忽略)', ['error' => $e->getMessage()]); } } }