MistakeBookService.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. <?php
  2. namespace App\Services;
  3. use App\Models\MistakeRecord;
  4. use App\Models\Student;
  5. use App\Models\Teacher;
  6. use Illuminate\Support\Arr;
  7. use Illuminate\Support\Facades\Cache;
  8. use Illuminate\Support\Facades\Log;
  9. use Illuminate\Support\Facades\Http;
  10. class MistakeBookService
  11. {
  12. protected string $learningAnalyticsBase;
  13. protected int $timeout;
  14. // 缓存时间(秒)
  15. const CACHE_TTL_LIST = 60; // 5分钟
  16. const CACHE_TTL_SUMMARY = 600; // 10分钟
  17. const CACHE_TTL_PATTERNS = 1800; // 30分钟
  18. public function __construct(
  19. ?string $learningAnalyticsBase = null,
  20. ?int $timeout = null
  21. ) {
  22. // 已迁移到本地,不再使用LearningAnalytics
  23. $this->learningAnalyticsBase = '';
  24. $this->timeout = 20;
  25. }
  26. /**
  27. * 新增错题
  28. */
  29. public function createMistake(array $payload): array
  30. {
  31. $studentId = $payload['student_id'] ?? null;
  32. $questionId = $payload['question_id'] ?? null;
  33. $paperId = $payload['paper_id'] ?? null;
  34. $myAnswer = $payload['my_answer'] ?? null;
  35. $correctAnswer = $payload['correct_answer'] ?? null;
  36. $questionText = $payload['question_text'] ?? null;
  37. $knowledgePoint = $payload['knowledge_point'] ?? null;
  38. $explanation = $payload['explanation'] ?? null;
  39. $kpIds = $payload['kp_ids'] ?? null;
  40. $source = $payload['source'] ?? MistakeRecord::SOURCE_PRACTICE;
  41. $happenedAt = $payload['happened_at'] ?? now();
  42. if (!$studentId) {
  43. throw new \InvalidArgumentException('学生ID不能为空');
  44. }
  45. // 使用事务确保数据一致性
  46. return \DB::transaction(function () use (
  47. $studentId, $questionId, $paperId, $myAnswer, $correctAnswer,
  48. $questionText, $knowledgePoint, $explanation, $kpIds, $source, $happenedAt
  49. ) {
  50. // 检查是否已存在相同错题(避免重复)
  51. $query = MistakeRecord::where('student_id', $studentId)
  52. ->where('source', $source)
  53. ->where('created_at', '>=', now()->subDay());
  54. // 如果有 question_id,用它去重;否则用 question_text
  55. if ($questionId) {
  56. $query->where('question_id', $questionId);
  57. } elseif ($questionText) {
  58. $query->where('question_text', $questionText);
  59. }
  60. $existingMistake = $query->first();
  61. if ($existingMistake) {
  62. return [
  63. 'duplicate' => true,
  64. 'mistake_id' => $existingMistake->id,
  65. 'message' => '错题已存在',
  66. ];
  67. }
  68. // 创建错题记录
  69. $mistake = MistakeRecord::create([
  70. 'student_id' => $studentId,
  71. 'question_id' => $questionId,
  72. 'paper_id' => $paperId,
  73. 'student_answer' => $myAnswer,
  74. 'correct_answer' => $correctAnswer,
  75. 'question_text' => $questionText,
  76. 'knowledge_point' => $knowledgePoint,
  77. 'explanation' => $explanation,
  78. 'kp_ids' => $kpIds,
  79. 'source' => $source,
  80. 'created_at' => $happenedAt,
  81. 'review_status' => MistakeRecord::REVIEW_STATUS_PENDING,
  82. 'review_count' => 0,
  83. ]);
  84. // 清除相关缓存
  85. $this->clearCache($studentId);
  86. return [
  87. 'duplicate' => false,
  88. 'mistake_id' => $mistake->id,
  89. 'created_at' => $mistake->created_at,
  90. ];
  91. });
  92. }
  93. /**
  94. * 获取错题列表
  95. */
  96. public function listMistakes(array $params = []): array
  97. {
  98. $studentId = $params['student_id'] ?? null;
  99. $page = (int) ($params['page'] ?? 1);
  100. $perPage = (int) ($params['per_page'] ?? 20);
  101. if (!$studentId) {
  102. return [
  103. 'data' => [],
  104. 'meta' => ['total' => 0, 'page' => $page, 'per_page' => $perPage],
  105. 'statistics' => [ // ✅ 无 student_id 时也返回空统计
  106. 'total' => 0,
  107. 'pending' => 0,
  108. 'reviewed' => 0,
  109. 'mastered' => 0,
  110. 'favorites' => 0,
  111. 'in_retry_list' => 0,
  112. 'this_week' => 0,
  113. 'mastery_rate' => 0.0,
  114. ],
  115. ];
  116. }
  117. // 构建缓存键
  118. $cacheKey = $this->buildCacheKey('list', $params);
  119. // 尝试从缓存获取
  120. if (Cache::has($cacheKey)) {
  121. return Cache::get($cacheKey);
  122. }
  123. try {
  124. $query = MistakeRecord::forStudent($studentId)
  125. ->with(['student']) // 预加载学生信息
  126. ->orderByDesc('created_at');
  127. // 应用筛选条件
  128. $this->applyFilters($query, $params);
  129. // 获取总数
  130. $total = $query->count();
  131. // 分页获取数据
  132. $mistakes = $query->skip(($page - 1) * $perPage)
  133. ->take($perPage)
  134. ->get();
  135. // 转换数据格式
  136. $data = $mistakes->map(function ($mistake) {
  137. return $this->transformMistakeRecord($mistake);
  138. })->toArray();
  139. // 获取统计信息
  140. $summary = $this->summarize($studentId);
  141. $result = [
  142. 'data' => $data,
  143. 'meta' => [
  144. 'total' => $total,
  145. 'page' => $page,
  146. 'per_page' => $perPage,
  147. 'last_page' => (int) ceil($total / $perPage),
  148. ],
  149. 'statistics' => $summary, // ✅ 添加统计信息
  150. ];
  151. // 缓存结果
  152. Cache::put($cacheKey, $result, self::CACHE_TTL_LIST);
  153. return $result;
  154. } catch (\Throwable $e) {
  155. Log::error('获取错题列表失败', [
  156. 'student_id' => $studentId,
  157. 'error' => $e->getMessage(),
  158. 'params' => $params,
  159. ]);
  160. return [
  161. 'data' => [],
  162. 'meta' => ['total' => 0, 'page' => $page, 'per_page' => $perPage],
  163. 'statistics' => [ // ✅ 错误时也返回空统计
  164. 'total' => 0,
  165. 'pending' => 0,
  166. 'reviewed' => 0,
  167. 'mastered' => 0,
  168. 'favorites' => 0,
  169. 'in_retry_list' => 0,
  170. 'this_week' => 0,
  171. 'mastery_rate' => 0.0,
  172. ],
  173. ];
  174. }
  175. }
  176. /**
  177. * 获取错题详情
  178. */
  179. public function getMistakeDetail(string $mistakeId, ?string $studentId = null): array
  180. {
  181. try {
  182. $query = MistakeRecord::with(['student']);
  183. if ($studentId) {
  184. $query->forStudent($studentId);
  185. }
  186. $mistake = $query->find($mistakeId);
  187. if (!$mistake) {
  188. return [];
  189. }
  190. return $this->transformMistakeRecord($mistake, true);
  191. } catch (\Throwable $e) {
  192. Log::error('获取错题详情失败', [
  193. 'mistake_id' => $mistakeId,
  194. 'student_id' => $studentId,
  195. 'error' => $e->getMessage(),
  196. ]);
  197. return [];
  198. }
  199. }
  200. /**
  201. * 获取错题统计概要
  202. */
  203. public function summarize(string $studentId): array
  204. {
  205. $cacheKey = "mistake_book:summary:{$studentId}";
  206. return Cache::remember($cacheKey, self::CACHE_TTL_SUMMARY, function () use ($studentId) {
  207. return MistakeRecord::getSummary($studentId);
  208. });
  209. }
  210. /**
  211. * 获取错误模式分析
  212. */
  213. public function getMistakePatterns(string $studentId): array
  214. {
  215. $cacheKey = "mistake_book:patterns:{$studentId}";
  216. return Cache::remember($cacheKey, self::CACHE_TTL_PATTERNS, function () use ($studentId) {
  217. return MistakeRecord::getMistakePatterns($studentId);
  218. });
  219. }
  220. /**
  221. * 收藏/取消收藏错题
  222. */
  223. public function toggleFavorite(string $mistakeId, bool $favorite = true): bool
  224. {
  225. try {
  226. $mistake = MistakeRecord::find($mistakeId);
  227. if (!$mistake) {
  228. return false;
  229. }
  230. $mistake->update(['is_favorite' => $favorite]);
  231. // 清除缓存
  232. $this->clearCache($mistake->student_id);
  233. return true;
  234. } catch (\Throwable $e) {
  235. Log::error('收藏错题失败', [
  236. 'mistake_id' => $mistakeId,
  237. 'favorite' => $favorite,
  238. 'error' => $e->getMessage(),
  239. ]);
  240. return false;
  241. }
  242. }
  243. /**
  244. * 标记已复习
  245. */
  246. public function markReviewed(string $mistakeId): bool
  247. {
  248. try {
  249. $mistake = MistakeRecord::find($mistakeId);
  250. if (!$mistake) {
  251. return false;
  252. }
  253. $mistake->markAsReviewed();
  254. // 清除缓存
  255. $this->clearCache($mistake->student_id);
  256. return true;
  257. } catch (\Throwable $e) {
  258. Log::error('标记已复习失败', [
  259. 'mistake_id' => $mistakeId,
  260. 'error' => $e->getMessage(),
  261. ]);
  262. return false;
  263. }
  264. }
  265. /**
  266. * 修改复习状态
  267. */
  268. public function updateReviewStatus(string $mistakeId, string $action = 'increment', bool $forceReview = false): array
  269. {
  270. try {
  271. $mistake = MistakeRecord::find($mistakeId);
  272. if (!$mistake) {
  273. return [
  274. 'success' => false,
  275. 'error' => '错题记录不存在',
  276. ];
  277. }
  278. match ($action) {
  279. 'increment' => $mistake->markAsReviewed(),
  280. 'mastered' => $mistake->markAsMastered(),
  281. 'reset' => $mistake->update([
  282. 'review_status' => MistakeRecord::REVIEW_STATUS_PENDING,
  283. 'review_count' => 0,
  284. 'mastery_level' => null,
  285. 'reviewed_at' => null,
  286. 'next_review_at' => null,
  287. ]),
  288. default => throw new \InvalidArgumentException('无效的操作类型'),
  289. };
  290. // ⚠️ 重要:重新加载模型数据以获取最新状态
  291. $mistake->refresh();
  292. return [
  293. 'success' => true,
  294. 'mistake_id' => $mistakeId,
  295. 'review_status' => $mistake->review_status,
  296. 'review_count' => $mistake->review_count,
  297. 'reviewed_at' => $mistake->reviewed_at?->toISOString(),
  298. 'next_review_at' => $mistake->next_review_at?->toISOString(),
  299. 'mastery_level' => $mistake->mastery_level,
  300. 'message' => '复习状态更新成功',
  301. ];
  302. } catch (\Throwable $e) {
  303. Log::error('更新复习状态失败', [
  304. 'mistake_id' => $mistakeId,
  305. 'action' => $action,
  306. 'error' => $e->getMessage(),
  307. ]);
  308. return [
  309. 'success' => false,
  310. 'error' => $e->getMessage(),
  311. ];
  312. }
  313. }
  314. /**
  315. * 获取复习状态
  316. */
  317. public function getReviewStatus(string $mistakeId): array
  318. {
  319. try {
  320. $mistake = MistakeRecord::find($mistakeId);
  321. if (!$mistake) {
  322. return [
  323. 'success' => false,
  324. 'error' => '错题记录不存在',
  325. ];
  326. }
  327. return [
  328. 'success' => true,
  329. 'mistake_id' => $mistakeId,
  330. 'review_status' => $mistake->review_status,
  331. 'review_count' => $mistake->review_count,
  332. 'reviewed_at' => $mistake->reviewed_at?->toISOString(),
  333. 'next_review_at' => $mistake->next_review_at?->toISOString(),
  334. 'mastery_level' => $mistake->mastery_level,
  335. 'force_review' => $mistake->force_review,
  336. ];
  337. } catch (\Throwable $e) {
  338. Log::error('获取复习状态失败', [
  339. 'mistake_id' => $mistakeId,
  340. 'error' => $e->getMessage(),
  341. ]);
  342. return [
  343. 'success' => false,
  344. 'error' => $e->getMessage(),
  345. ];
  346. }
  347. }
  348. /**
  349. * 增加复习次数
  350. */
  351. public function incrementReviewCount(string $mistakeId, bool $forceReview = false): array
  352. {
  353. return $this->updateReviewStatus($mistakeId, 'increment', $forceReview);
  354. }
  355. /**
  356. * 重置为强制复习状态
  357. */
  358. public function resetReviewStatus(string $mistakeId): array
  359. {
  360. return $this->updateReviewStatus($mistakeId, 'reset');
  361. }
  362. /**
  363. * 添加到重练清单
  364. */
  365. public function addToRetryList(string $mistakeId): bool
  366. {
  367. try {
  368. $mistake = MistakeRecord::find($mistakeId);
  369. if (!$mistake) {
  370. return false;
  371. }
  372. $mistake->addToRetryList();
  373. // 清除缓存
  374. $this->clearCache($mistake->student_id);
  375. return true;
  376. } catch (\Throwable $e) {
  377. Log::error('加入重练清单失败', [
  378. 'mistake_id' => $mistakeId,
  379. 'error' => $e->getMessage(),
  380. ]);
  381. return false;
  382. }
  383. }
  384. /**
  385. * 批量操作错题
  386. */
  387. public function batchOperation(array $mistakeIds, string $operation, array $params = []): array
  388. {
  389. if (empty($mistakeIds)) {
  390. return [
  391. 'success' => false,
  392. 'error' => '请选择要操作的错题',
  393. ];
  394. }
  395. try {
  396. $mistakes = MistakeRecord::whereIn('id', $mistakeIds)->get();
  397. $successCount = 0;
  398. $errors = [];
  399. foreach ($mistakes as $mistake) {
  400. try {
  401. match ($operation) {
  402. 'favorite' => $mistake->toggleFavorite(),
  403. 'reviewed' => $mistake->markAsReviewed(),
  404. 'mastered' => $mistake->markAsMastered(),
  405. 'retry_list' => $mistake->addToRetryList(),
  406. 'remove_retry_list' => $mistake->removeFromRetryList(),
  407. 'set_error_type' => $mistake->update(['error_type' => $params['error_type'] ?? null]),
  408. 'set_importance' => $mistake->update(['importance' => $params['importance'] ?? 5]),
  409. default => throw new \InvalidArgumentException('不支持的操作'),
  410. };
  411. $successCount++;
  412. } catch (\Throwable $e) {
  413. $errors[] = [
  414. 'mistake_id' => $mistake->id,
  415. 'error' => $e->getMessage(),
  416. ];
  417. }
  418. }
  419. // 清除缓存
  420. if ($successCount > 0) {
  421. $studentIds = $mistakes->pluck('student_id')->unique();
  422. foreach ($studentIds as $studentId) {
  423. $this->clearCache($studentId);
  424. }
  425. }
  426. return [
  427. 'success' => true,
  428. 'total' => count($mistakeIds),
  429. 'success_count' => $successCount,
  430. 'error_count' => count($errors),
  431. 'errors' => $errors,
  432. ];
  433. } catch (\Throwable $e) {
  434. Log::error('批量操作失败', [
  435. 'operation' => $operation,
  436. 'mistake_ids' => $mistakeIds,
  437. 'error' => $e->getMessage(),
  438. ]);
  439. return [
  440. 'success' => false,
  441. 'error' => $e->getMessage(),
  442. ];
  443. }
  444. }
  445. /**
  446. * 基于错题推荐练习题(本地实现)
  447. */
  448. public function recommendPractice(string $studentId, array $kpIds = [], array $skillIds = []): array
  449. {
  450. try {
  451. // 获取学生未掌握的知识点
  452. $weakKnowledgePoints = MistakeRecord::forStudent($studentId)
  453. ->where('review_status', '!=', MistakeRecord::REVIEW_STATUS_MASTERED)
  454. ->pluck('knowledge_point')
  455. ->filter()
  456. ->unique()
  457. ->values()
  458. ->toArray();
  459. // 合并传入的知识点
  460. $allKpIds = array_unique(array_merge($kpIds, $weakKnowledgePoints));
  461. // TODO: 这里应该调用本地题库服务
  462. // 目前返回模拟数据
  463. $recommendations = [];
  464. foreach (array_slice($allKpIds, 0, 5) as $kpId) {
  465. $recommendations[] = [
  466. 'id' => "rec_{$kpId}_{$studentId}",
  467. 'kp_code' => $kpId,
  468. 'question_text' => "针对知识点 {$kpId} 的练习题",
  469. 'difficulty' => 0.5,
  470. 'source' => 'recommendation',
  471. ];
  472. }
  473. return ['data' => $recommendations];
  474. } catch (\Throwable $e) {
  475. Log::error('推荐练习题失败', [
  476. 'student_id' => $studentId,
  477. 'error' => $e->getMessage(),
  478. ]);
  479. return ['data' => []];
  480. }
  481. }
  482. /**
  483. * 获取仪表板快照数据
  484. */
  485. public function getPanelSnapshot(string $studentId, int $limit = 5): array
  486. {
  487. try {
  488. $recentMistakes = MistakeRecord::forStudent($studentId)
  489. ->orderByDesc('created_at')
  490. ->limit($limit)
  491. ->get()
  492. ->map(fn($m) => $this->transformMistakeRecord($m))
  493. ->toArray();
  494. $patterns = $this->getMistakePatterns($studentId);
  495. $summary = $this->summarize($studentId);
  496. return [
  497. 'recent' => $recentMistakes,
  498. 'weak_skills' => array_slice($patterns['error_types'] ?? [], 0, 5, true),
  499. 'weak_kps' => array_slice($patterns['knowledge_points'] ?? [], 0, 5, true),
  500. 'error_types' => $patterns['error_types'] ?? [],
  501. 'stats' => $summary,
  502. ];
  503. } catch (\Throwable $e) {
  504. Log::error('获取快照数据失败', [
  505. 'student_id' => $studentId,
  506. 'error' => $e->getMessage(),
  507. ]);
  508. return [
  509. 'recent' => [],
  510. 'weak_skills' => [],
  511. 'weak_kps' => [],
  512. 'error_types' => [],
  513. 'stats' => [
  514. 'total' => 0,
  515. 'pending' => 0,
  516. 'this_week' => 0,
  517. ],
  518. ];
  519. }
  520. }
  521. /**
  522. * 应用筛选条件
  523. */
  524. private function applyFilters($query, array $params): void
  525. {
  526. // 知识点筛选
  527. if (!empty($params['kp_ids'])) {
  528. $kpIds = is_array($params['kp_ids']) ? $params['kp_ids'] : explode(',', $params['kp_ids']);
  529. $query->byKnowledgePoint($kpIds);
  530. }
  531. // 技能筛选
  532. if (!empty($params['skill_ids'])) {
  533. $skillIds = is_array($params['skill_ids']) ? $params['skill_ids'] : explode(',', $params['skill_ids']);
  534. $query->where(function ($q) use ($skillIds) {
  535. foreach ($skillIds as $skillId) {
  536. $q->orWhereJsonContains('skill_ids', $skillId);
  537. }
  538. });
  539. }
  540. // 错误类型筛选
  541. if (!empty($params['error_types'])) {
  542. $errorTypes = is_array($params['error_types']) ? $params['error_types'] : explode(',', $params['error_types']);
  543. $query->whereIn('error_type', $errorTypes);
  544. }
  545. // 时间范围筛选
  546. if (!empty($params['time_range'])) {
  547. match ($params['time_range']) {
  548. 'last_7' => $query->where('created_at', '>=', now()->subDays(7)),
  549. 'last_30' => $query->where('created_at', '>=', now()->subDays(30)),
  550. 'last_90' => $query->where('created_at', '>=', now()->subDays(90)),
  551. default => null,
  552. };
  553. }
  554. // 自定义时间范围
  555. if (!empty($params['start_date'])) {
  556. $query->whereDate('created_at', '>=', $params['start_date']);
  557. }
  558. if (!empty($params['end_date'])) {
  559. $query->whereDate('created_at', '<=', $params['end_date']);
  560. }
  561. // 复习状态筛选
  562. if (!empty($params['unreviewed_only'])) {
  563. $query->pending();
  564. }
  565. if (!empty($params['favorite_only'])) {
  566. $query->favorites();
  567. }
  568. if (!empty($params['in_retry_list_only'])) {
  569. $query->inRetryList();
  570. }
  571. // 排序
  572. $sortBy = $params['sort_by'] ?? 'created_at_desc';
  573. match ($sortBy) {
  574. 'created_at_asc' => $query->orderBy('created_at'),
  575. 'created_at_desc' => $query->orderByDesc('created_at'),
  576. 'review_status_asc' => $query->orderBy('review_status'),
  577. 'difficulty_desc' => $query->orderByDesc('difficulty'),
  578. default => null,
  579. };
  580. }
  581. /**
  582. * 转换错题记录格式
  583. */
  584. private function transformMistakeRecord(MistakeRecord $mistake, bool $detailed = false): array
  585. {
  586. $data = [
  587. 'id' => $mistake->id,
  588. 'student_id' => $mistake->student_id,
  589. 'question_id' => $mistake->question_id,
  590. 'paper_id' => $mistake->paper_id,
  591. 'question_text' => $mistake->question_text,
  592. 'student_answer' => $mistake->student_answer,
  593. 'correct_answer' => $mistake->correct_answer,
  594. 'knowledge_point' => $mistake->knowledge_point,
  595. 'explanation' => $mistake->explanation,
  596. 'source' => $mistake->source,
  597. 'source_label' => $mistake->source_label,
  598. 'created_at' => $mistake->created_at?->toISOString(),
  599. 'review_status' => $mistake->review_status,
  600. 'review_status_label' => $mistake->review_status_label,
  601. 'review_count' => $mistake->review_count,
  602. 'is_favorite' => $mistake->is_favorite,
  603. 'in_retry_list' => $mistake->in_retry_list,
  604. 'reviewed_at' => $mistake->reviewed_at?->toISOString(),
  605. 'next_review_at' => $mistake->next_review_at?->toISOString(),
  606. 'error_type' => $mistake->error_type,
  607. 'error_type_label' => $mistake->error_type_label,
  608. 'kp_ids' => $mistake->kp_ids,
  609. 'skill_ids' => $mistake->skill_ids,
  610. 'difficulty' => $mistake->difficulty,
  611. 'difficulty_level' => $mistake->difficulty_level,
  612. 'importance' => $mistake->importance,
  613. 'mastery_level' => $mistake->mastery_level,
  614. ];
  615. if ($detailed) {
  616. $data['student'] = [
  617. 'id' => $mistake->student?->student_id,
  618. 'name' => $mistake->student?->name,
  619. 'grade' => $mistake->student?->grade,
  620. 'class_name' => $mistake->student?->class_name,
  621. ];
  622. }
  623. return $data;
  624. }
  625. /**
  626. * 构建缓存键
  627. */
  628. private function buildCacheKey(string $type, array $params): string
  629. {
  630. $key = "mistake_book:{$type}:" . md5(serialize($params));
  631. return $key;
  632. }
  633. /**
  634. * 清除缓存
  635. */
  636. private function clearCache(string|int $studentId): void
  637. {
  638. try {
  639. $patterns = [
  640. "mistake_book:list:{$studentId}",
  641. "mistake_book:summary:{$studentId}",
  642. "mistake_book:patterns:{$studentId}",
  643. ];
  644. foreach ($patterns as $key) {
  645. Cache::forget($key);
  646. }
  647. } catch (\Exception $e) {
  648. // 缓存清除失败不影响主流程
  649. Log::debug('缓存清除失败(可忽略)', ['error' => $e->getMessage()]);
  650. }
  651. }
  652. }