| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072 |
- <?php
- namespace App\Services;
- use App\Models\MistakeRecord;
- use App\Models\Student;
- use App\Models\Teacher;
- use Illuminate\Support\Arr;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Http;
- class MistakeBookService
- {
- protected string $learningAnalyticsBase;
- protected int $timeout;
- // 缓存时间(秒)
- const CACHE_TTL_SUMMARY = 600; // 10分钟
- const CACHE_TTL_PATTERNS = 1800; // 30分钟
- public function __construct(
- ?string $learningAnalyticsBase = null,
- ?int $timeout = null
- ) {
- // 已迁移到本地,不再使用LearningAnalytics
- $this->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);
- // 如果有 question_id 和 paper_id,用它们去重
- // 否则用 question_text 去重
- if ($questionId && $paperId) {
- $query->where('question_id', $questionId)
- ->where('paper_id', $paperId);
- Log::debug('错题记录重复检查', [
- 'student_id' => $studentId,
- 'question_id' => $questionId,
- 'paper_id' => $paperId,
- 'check_type' => 'question_id + paper_id'
- ]);
- } elseif ($questionId) {
- $query->where('question_id', $questionId);
- Log::debug('错题记录重复检查', [
- 'student_id' => $studentId,
- 'question_id' => $questionId,
- 'check_type' => 'question_id only'
- ]);
- } elseif ($questionText) {
- $query->where('question_text', $questionText);
- Log::debug('错题记录重复检查', [
- 'student_id' => $studentId,
- 'question_text' => substr($questionText, 0, 50) . '...',
- 'check_type' => 'question_text'
- ]);
- } else {
- Log::warning('错题记录保存失败:缺少题目ID和题目文本', [
- 'student_id' => $studentId,
- 'paper_id' => $paperId
- ]);
- return [
- 'duplicate' => false,
- 'error' => '缺少题目ID和题目文本,无法创建错题记录'
- ];
- }
- $existingMistake = $query->first();
- if ($existingMistake) {
- Log::debug('发现重复错题记录,合并知识点', [
- 'student_id' => $studentId,
- 'question_id' => $questionId,
- 'paper_id' => $paperId,
- 'existing_mistake_id' => $existingMistake->id
- ]);
- // 合并知识点到已有记录中
- $updates = ['updated_at' => now()];
- if ($kpIds) {
- $existingKpIds = $existingMistake->kp_ids ?? [];
- $newKpIds = is_array($kpIds) ? $kpIds : [$kpIds];
- // 合并并去重
- $mergedKpIds = array_values(array_unique(array_merge($existingKpIds, $newKpIds)));
- // 更新记录(仅更新 kp_ids)
- $updates['kp_ids'] = $mergedKpIds;
- Log::info('错题记录知识点合并成功', [
- 'student_id' => $studentId,
- 'question_id' => $questionId,
- 'paper_id' => $paperId,
- 'existing_kp_ids' => $existingKpIds,
- 'new_kp_ids' => $newKpIds,
- 'merged_kp_ids' => $mergedKpIds
- ]);
- }
- $existingMistake->update($updates);
- 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' => is_array($kpIds) ? $kpIds : ($kpIds ? [$kpIds] : null), // 确保是数组
- '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,
- ];
- });
- }
- /**
- * 批量新增错题(同一学生/卷子)
- *
- * @param array<int, array> $payloads
- * @return array
- */
- public function createMistakesBatch(array $payloads): array
- {
- if (empty($payloads)) {
- return ['created' => 0, 'duplicates' => 0, 'updated' => 0];
- }
- $studentId = $payloads[0]['student_id'] ?? null;
- $paperId = $payloads[0]['paper_id'] ?? null;
- $source = $payloads[0]['source'] ?? MistakeRecord::SOURCE_PRACTICE;
- if (!$studentId) {
- throw new \InvalidArgumentException('学生ID不能为空');
- }
- $questionIds = [];
- foreach ($payloads as $payload) {
- if (!empty($payload['question_id'])) {
- $questionIds[] = $payload['question_id'];
- }
- }
- $questionIds = array_values(array_unique($questionIds));
- return \DB::transaction(function () use ($payloads, $studentId, $paperId, $source, $questionIds) {
- $existingMap = [];
- if (!empty($questionIds)) {
- $existing = MistakeRecord::where('student_id', $studentId)
- ->where('source', $source)
- ->when($paperId, fn ($q) => $q->where('paper_id', $paperId))
- ->whereIn('question_id', $questionIds)
- ->get();
- foreach ($existing as $item) {
- $existingMap[$item->question_id] = $item;
- }
- }
- $toInsert = [];
- $created = 0;
- $duplicates = 0;
- $updated = 0;
- foreach ($payloads as $payload) {
- $questionId = $payload['question_id'] ?? null;
- if (!$questionId) {
- continue;
- }
- $kpIds = $payload['kp_ids'] ?? null;
- $kpIds = is_array($kpIds) ? $kpIds : ($kpIds ? [$kpIds] : null);
- if (isset($existingMap[$questionId])) {
- $duplicates++;
- $updates = ['updated_at' => now()];
- if ($kpIds) {
- $existingKpIds = $existingMap[$questionId]->kp_ids ?? [];
- $merged = array_values(array_unique(array_merge($existingKpIds, $kpIds)));
- if ($merged !== $existingKpIds) {
- $updates['kp_ids'] = $merged;
- $updated++;
- }
- }
- $existingMap[$questionId]->update($updates);
- continue;
- }
- $toInsert[] = [
- 'student_id' => $studentId,
- 'question_id' => $questionId,
- 'paper_id' => $payload['paper_id'] ?? null,
- 'student_answer' => $payload['my_answer'] ?? null,
- 'correct_answer' => $payload['correct_answer'] ?? null,
- 'question_text' => $payload['question_text'] ?? null,
- 'knowledge_point' => $payload['knowledge_point'] ?? null,
- 'explanation' => $payload['explanation'] ?? null,
- 'kp_ids' => $kpIds ? json_encode($kpIds, JSON_UNESCAPED_UNICODE) : null,
- 'source' => $payload['source'] ?? MistakeRecord::SOURCE_PRACTICE,
- 'created_at' => $payload['happened_at'] ?? now(),
- 'review_status' => MistakeRecord::REVIEW_STATUS_PENDING,
- 'review_count' => 0,
- ];
- }
- if (!empty($toInsert)) {
- // MistakeRecord::insert 不会触发模型事件,但速度更快
- MistakeRecord::insert($toInsert);
- $created = count($toInsert);
- }
- // 统一刷新本次题目的 updated_at(无论是否合并)
- if (!empty($questionIds)) {
- \DB::table('mistake_records')
- ->where('student_id', $studentId)
- ->where('source', $source)
- ->when($paperId, fn ($q) => $q->where('paper_id', $paperId))
- ->whereIn('question_id', $questionIds)
- ->update(['updated_at' => now()]);
- }
- $this->clearCache($studentId);
- return [
- 'created' => $created,
- 'duplicates' => $duplicates,
- 'updated' => $updated,
- ];
- });
- }
- /**
- * 获取错题列表
- */
- 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],
- 'statistics' => [ // ✅ 无 student_id 时也返回空统计
- 'total' => 0,
- 'pending' => 0,
- 'reviewed' => 0,
- 'mastered' => 0,
- 'favorites' => 0,
- 'in_retry_list' => 0,
- 'this_week' => 0,
- 'mastery_rate' => 0.0,
- ],
- ];
- }
- 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, false);
- })->toArray();
- // 获取统计信息
- $summary = $this->summarize($studentId);
- return [
- 'data' => $data,
- 'meta' => [
- 'total' => $total,
- 'page' => $page,
- 'per_page' => $perPage,
- 'last_page' => (int) ceil($total / $perPage),
- ],
- 'statistics' => $summary,
- ];
- } catch (\Throwable $e) {
- Log::error('获取错题列表失败', [
- 'student_id' => $studentId,
- 'error' => $e->getMessage(),
- 'params' => $params,
- ]);
- return [
- 'data' => [],
- 'meta' => ['total' => 0, 'page' => $page, 'per_page' => $perPage],
- 'statistics' => [ // ✅ 错误时也返回空统计
- 'total' => 0,
- 'pending' => 0,
- 'reviewed' => 0,
- 'mastered' => 0,
- 'favorites' => 0,
- 'in_retry_list' => 0,
- 'this_week' => 0,
- 'mastery_rate' => 0.0,
- ],
- ];
- }
- }
- /**
- * 获取错题详情
- */
- 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
- {
- // 【新增】通过 question_id 关联获取题目详情
- $questionDetails = $this->getQuestionDetails($mistake->question_id);
- // 【新增】通过 kp_ids 或 textbook_catalog_nodes_id 获取知识点信息
- $knowledgePoints = $this->getKnowledgePoints(
- $mistake->kp_ids,
- $mistake->question_id,
- $questionDetails['textbook_catalog_nodes_id'] ?? null
- );
- $data = [
- // ========== 错题记录基本信息 ==========
- 'id' => $mistake->id,
- 'student_id' => $mistake->student_id,
- 'question_id' => $mistake->question_id,
- 'paper_id' => $mistake->paper_id,
- '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,
- 'explanation' => $mistake->explanation,
- 'skill_ids' => $mistake->skill_ids,
- 'difficulty' => $mistake->difficulty,
- 'difficulty_level' => $mistake->difficulty_level,
- 'importance' => $mistake->importance,
- 'mastery_level' => $mistake->mastery_level,
- // ========== 题目信息(优先呈现)==========
- // 题目内容:优先使用 questions.stem,其次使用错题本自带
- 'question_text' => $questionDetails['stem'] ?? $mistake->question_text,
- // 答案:优先使用 questions.answer,其次使用错题本自带
- 'correct_answer' => $questionDetails['answer'] ?? $mistake->correct_answer,
- // 解题过程/解析:优先使用 questions.solution
- 'solution' => $questionDetails['solution'] ?? $mistake->explanation,
- // 选项
- 'options' => $questionDetails['options'] ?? null,
- // 题目类型
- 'question_type' => $questionDetails['question_type'] ?? null,
- // 题目难度(从题目表获取)
- 'question_difficulty' => $questionDetails['difficulty'] ?? null,
- // 题目标签
- 'question_tags' => $questionDetails['tags'] ?? null,
- // 题目来源
- 'question_source' => $questionDetails['source'] ?? null,
- // 学生答案(始终使用错题本中的记录)
- 'student_answer' => $mistake->student_answer,
- // ========== 知识点信息 ==========
- 'knowledge_points' => $knowledgePoints,
- ];
- 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 clearCache(string|int $studentId): void
- {
- try {
- $patterns = [
- "mistake_book:summary:{$studentId}",
- "mistake_book:patterns:{$studentId}",
- ];
- foreach ($patterns as $key) {
- Cache::forget($key);
- }
- } catch (\Exception $e) {
- // 缓存清除失败不影响主流程
- Log::debug('缓存清除失败(可忽略)', ['error' => $e->getMessage()]);
- }
- }
- /**
- * 【新增】通过 question_id 获取题目详情
- */
- private function getQuestionDetails(?string $questionId): ?array
- {
- if (!$questionId) {
- return null;
- }
- try {
- $question = \DB::connection('mysql')
- ->table('questions')
- ->where('id', $questionId)
- ->first();
- if (!$question) {
- return null;
- }
- return [
- 'stem' => $question->stem ?? null, // 题目内容(题干)
- 'options' => $question->options ?? null, // 选项
- 'answer' => $question->answer ?? null, // 答案
- 'solution' => $question->solution ?? null, // 解题过程/解析
- 'difficulty' => $question->difficulty ?? null,
- 'question_type' => $question->question_type ?? null,
- 'textbook_catalog_nodes_id' => $question->textbook_catalog_nodes_id ?? null,
- 'tags' => $question->tags ?? null, // 标签
- 'source' => $question->source ?? null, // 来源
- ];
- } catch (\Exception $e) {
- Log::warning('获取题目详情失败', [
- 'question_id' => $questionId,
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
- /**
- * 【新增】通过 kp_ids 或 textbook_catalog_nodes_id 获取知识点信息
- *
- * 逻辑:
- * 1. 如果有 kp_ids,直接从 knowledge_points 表查询
- * 2. 如果没有 kp_ids,通过 textbook_catalog_nodes_id 关联 textbook_chapter_knowledge_relation 表获取 kp_id
- */
- private function getKnowledgePoints(?array $kpIds, ?string $questionId = null, ?int $catalogNodesId = null): array
- {
- // 如果有 kp_ids,直接查询
- if (!empty($kpIds)) {
- return $this->queryKnowledgePointsByCodes($kpIds);
- }
- // 如果没有 kp_ids,通过题目ID和目录节点ID关联查询
- if ($questionId || $catalogNodesId) {
- $kpCodesFromRelation = $this->getKpCodesFromRelation($questionId, $catalogNodesId);
- if (!empty($kpCodesFromRelation)) {
- return $this->queryKnowledgePointsByCodes($kpCodesFromRelation);
- }
- }
- return [];
- }
- /**
- * 通过 kp_codes 查询知识点信息
- */
- private function queryKnowledgePointsByCodes(array $kpCodes): array
- {
- try {
- $knowledgePoints = \DB::connection('mysql')
- ->table('knowledge_points')
- ->whereIn('kp_code', $kpCodes)
- ->select(['kp_code', 'name', 'parent_kp_code'])
- ->get()
- ->toArray();
- return array_map(function ($kp) {
- return [
- 'kp_code' => $kp->kp_code,
- 'name' => $kp->name ?? $kp->kp_code,
- 'parent_kp_code' => $kp->parent_kp_code,
- ];
- }, $knowledgePoints);
- } catch (\Exception $e) {
- Log::warning('通过kp_codes获取知识点信息失败', [
- 'kp_codes' => $kpCodes,
- 'error' => $e->getMessage()
- ]);
- return [];
- }
- }
- /**
- * 通过 textbook_catalog_nodes_id 关联 textbook_chapter_knowledge_relation 表获取 kp_codes
- */
- private function getKpCodesFromRelation(?string $questionId, ?int $catalogNodesId): array
- {
- try {
- $query = \DB::connection('mysql')
- ->table('textbook_chapter_knowledge_relation')
- ->where('is_deleted', 0);
- if ($catalogNodesId) {
- $query->where('catalog_chapter_id', $catalogNodesId);
- } elseif ($questionId) {
- // 如果没有catalog_nodes_id,尝试从questions表获取
- $question = \DB::connection('mysql')
- ->table('questions')
- ->where('id', $questionId)
- ->first();
- if ($question && $question->textbook_catalog_nodes_id) {
- $query->where('catalog_chapter_id', $question->textbook_catalog_nodes_id);
- }
- }
- $relations = $query->select('kp_code')->get();
- if ($relations->isNotEmpty()) {
- Log::debug('通过目录节点关联获取到知识点', [
- 'question_id' => $questionId,
- 'catalog_nodes_id' => $catalogNodesId,
- 'kp_codes' => $relations->pluck('kp_code')->toArray()
- ]);
- }
- return $relations->pluck('kp_code')->toArray();
- } catch (\Exception $e) {
- Log::warning('通过目录节点关联获取kp_codes失败', [
- 'question_id' => $questionId,
- 'catalog_nodes_id' => $catalogNodesId,
- 'error' => $e->getMessage()
- ]);
- return [];
- }
- }
- }
|