MistakeBookController.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Services\MistakeBookService;
  5. use Illuminate\Http\Request;
  6. use Illuminate\Http\JsonResponse;
  7. use Illuminate\Support\Facades\Log;
  8. class MistakeBookController extends Controller
  9. {
  10. protected MistakeBookService $mistakeBookService;
  11. public function __construct(MistakeBookService $mistakeBookService)
  12. {
  13. $this->mistakeBookService = $mistakeBookService;
  14. }
  15. /**
  16. * 获取错题列表
  17. *
  18. * @param Request $request
  19. * @return JsonResponse
  20. */
  21. public function listMistakes(Request $request): JsonResponse
  22. {
  23. try {
  24. $params = $request->only([
  25. 'student_id',
  26. 'start_time',
  27. 'end_time',
  28. 'kp_ids',
  29. 'skill_ids',
  30. 'error_types',
  31. 'time_range',
  32. 'page',
  33. 'per_page',
  34. 'sort_by'
  35. ]);
  36. // 设置默认值
  37. $params['page'] = (int) ($params['page'] ?? 1);
  38. $params['per_page'] = (int) ($params['per_page'] ?? 20);
  39. $params['sort_by'] = $params['sort_by'] ?? 'created_at_desc';
  40. // 调用服务层获取错题列表
  41. $result = $this->mistakeBookService->listMistakes($params);
  42. Log::info('获取错题列表成功', [
  43. 'student_id' => $params['student_id'] ?? 'N/A',
  44. 'page' => $params['page'],
  45. 'per_page' => $params['per_page'],
  46. 'total' => $result['meta']['total'] ?? 0
  47. ]);
  48. return response()->json([
  49. 'success' => true,
  50. 'data' => $result['data'] ?? [],
  51. 'meta' => $result['meta'] ?? [],
  52. 'statistics' => $result['statistics'] ?? null
  53. ]);
  54. } catch (\Exception $e) {
  55. Log::error('获取错题列表失败', [
  56. 'error' => $e->getMessage(),
  57. 'trace' => $e->getTraceAsString(),
  58. 'params' => $request->all()
  59. ]);
  60. return response()->json([
  61. 'success' => false,
  62. 'message' => '获取错题列表失败: ' . $e->getMessage(),
  63. 'data' => [],
  64. 'meta' => [
  65. 'total' => 0,
  66. 'page' => (int) ($request->get('page', 1)),
  67. 'per_page' => (int) ($request->get('per_page', 20))
  68. ]
  69. ], 500);
  70. }
  71. }
  72. /**
  73. * 获取单条错题详情
  74. *
  75. * @param string $mistakeId
  76. * @param Request $request
  77. * @return JsonResponse
  78. */
  79. public function getMistakeDetail(string $mistakeId, Request $request): JsonResponse
  80. {
  81. try {
  82. $studentId = $request->get('student_id');
  83. $mistake = $this->mistakeBookService->getMistakeDetail($mistakeId, $studentId);
  84. if (empty($mistake)) {
  85. return response()->json([
  86. 'success' => false,
  87. 'message' => '错题记录不存在'
  88. ], 404);
  89. }
  90. Log::info('获取错题详情成功', [
  91. 'mistake_id' => $mistakeId,
  92. 'student_id' => $studentId ?? 'N/A'
  93. ]);
  94. return response()->json([
  95. 'success' => true,
  96. 'data' => $mistake
  97. ]);
  98. } catch (\Exception $e) {
  99. Log::error('获取错题详情失败', [
  100. 'mistake_id' => $mistakeId,
  101. 'error' => $e->getMessage(),
  102. 'trace' => $e->getTraceAsString()
  103. ]);
  104. return response()->json([
  105. 'success' => false,
  106. 'message' => '获取错题详情失败: ' . $e->getMessage()
  107. ], 500);
  108. }
  109. }
  110. /**
  111. * 获取错题统计概要
  112. *
  113. * @param Request $request
  114. * @return JsonResponse
  115. */
  116. public function getSummary(Request $request): JsonResponse
  117. {
  118. try {
  119. $studentId = $request->get('student_id');
  120. if (!$studentId) {
  121. return response()->json([
  122. 'success' => false,
  123. 'message' => '缺少必要参数:student_id'
  124. ], 400);
  125. }
  126. $startTime = $request->get('start_time');
  127. $endTime = $request->get('end_time');
  128. // 构建查询参数
  129. $params = ['student_id' => $studentId];
  130. if ($startTime) {
  131. $params['start_time'] = $startTime;
  132. }
  133. if ($endTime) {
  134. $params['end_time'] = $endTime;
  135. }
  136. // 获取错题列表(用于计算统计信息)
  137. $mistakesResult = $this->mistakeBookService->listMistakes($params);
  138. // 获取概要统计
  139. $summary = $this->mistakeBookService->summarize($studentId);
  140. Log::info('获取错题概要统计成功', [
  141. 'student_id' => $studentId,
  142. 'total_mistakes' => $summary['total'] ?? 0
  143. ]);
  144. return response()->json([
  145. 'success' => true,
  146. 'data' => [
  147. 'total' => $summary['total'] ?? 0,
  148. 'this_week' => $summary['this_week'] ?? 0,
  149. 'pending_review' => $summary['pending_review'] ?? 0,
  150. 'mastery_rate' => $summary['mastery_rate'] ?? 0.0,
  151. 'statistics' => $mistakesResult['statistics'] ?? null
  152. ]
  153. ]);
  154. } catch (\Exception $e) {
  155. Log::error('获取错题概要统计失败', [
  156. 'student_id' => $request->get('student_id'),
  157. 'error' => $e->getMessage(),
  158. 'trace' => $e->getTraceAsString()
  159. ]);
  160. return response()->json([
  161. 'success' => false,
  162. 'message' => '获取错题概要统计失败: ' . $e->getMessage()
  163. ], 500);
  164. }
  165. }
  166. /**
  167. * 获取错误模式分析
  168. *
  169. * @param Request $request
  170. * @return JsonResponse
  171. */
  172. public function getMistakePatterns(Request $request): JsonResponse
  173. {
  174. try {
  175. $studentId = $request->get('student_id');
  176. if (!$studentId) {
  177. return response()->json([
  178. 'success' => false,
  179. 'message' => '缺少必要参数:student_id'
  180. ], 400);
  181. }
  182. $patterns = $this->mistakeBookService->getMistakePatterns($studentId);
  183. Log::info('获取错误模式分析成功', [
  184. 'student_id' => $studentId,
  185. 'top_kps_count' => count($patterns['top_kps'] ?? [])
  186. ]);
  187. return response()->json([
  188. 'success' => true,
  189. 'data' => $patterns
  190. ]);
  191. } catch (\Exception $e) {
  192. Log::error('获取错误模式分析失败', [
  193. 'student_id' => $request->get('student_id'),
  194. 'error' => $e->getMessage(),
  195. 'trace' => $e->getTraceAsString()
  196. ]);
  197. return response()->json([
  198. 'success' => false,
  199. 'message' => '获取错误模式分析失败: ' . $e->getMessage()
  200. ], 500);
  201. }
  202. }
  203. /**
  204. * 收藏/取消收藏错题
  205. *
  206. * @param string $mistakeId
  207. * @param Request $request
  208. * @return JsonResponse
  209. */
  210. public function toggleFavorite(string $mistakeId, Request $request): JsonResponse
  211. {
  212. try {
  213. $data = $request->only(['favorite']);
  214. $favorite = $data['favorite'] ?? true;
  215. $result = $this->mistakeBookService->toggleFavorite($mistakeId, $favorite);
  216. Log::info(($favorite ? '收藏' : '取消收藏') . '错题', [
  217. 'mistake_id' => $mistakeId,
  218. 'favorite' => $favorite,
  219. 'result' => $result
  220. ]);
  221. return response()->json([
  222. 'success' => $result,
  223. 'message' => $result ? '操作成功' : '操作失败',
  224. 'data' => [
  225. 'mistake_id' => $mistakeId,
  226. 'favorite' => $favorite
  227. ]
  228. ]);
  229. } catch (\Exception $e) {
  230. Log::error('收藏错题失败', [
  231. 'mistake_id' => $mistakeId,
  232. 'error' => $e->getMessage(),
  233. 'trace' => $e->getTraceAsString()
  234. ]);
  235. return response()->json([
  236. 'success' => false,
  237. 'message' => '收藏错题失败: ' . $e->getMessage()
  238. ], 500);
  239. }
  240. }
  241. /**
  242. * 标记错题已复习
  243. *
  244. * @param string $mistakeId
  245. * @return JsonResponse
  246. */
  247. public function markReviewed(string $mistakeId): JsonResponse
  248. {
  249. try {
  250. $result = $this->mistakeBookService->markReviewed($mistakeId);
  251. Log::info('标记错题已复习', [
  252. 'mistake_id' => $mistakeId,
  253. 'result' => $result
  254. ]);
  255. return response()->json([
  256. 'success' => $result,
  257. 'message' => $result ? '操作成功' : '操作失败',
  258. 'data' => [
  259. 'mistake_id' => $mistakeId,
  260. 'reviewed' => $result,
  261. 'reviewed_at' => $result ? now()->toISOString() : null
  262. ]
  263. ]);
  264. } catch (\Exception $e) {
  265. Log::error('标记错题已复习失败', [
  266. 'mistake_id' => $mistakeId,
  267. 'error' => $e->getMessage(),
  268. 'trace' => $e->getTraceAsString()
  269. ]);
  270. return response()->json([
  271. 'success' => false,
  272. 'message' => '标记错题已复习失败: ' . $e->getMessage()
  273. ], 500);
  274. }
  275. }
  276. /**
  277. * 加入重练清单
  278. *
  279. * @param string $mistakeId
  280. * @return JsonResponse
  281. */
  282. public function addToRetryList(string $mistakeId): JsonResponse
  283. {
  284. try {
  285. $result = $this->mistakeBookService->addToRetryList($mistakeId);
  286. Log::info('加入重练清单', [
  287. 'mistake_id' => $mistakeId,
  288. 'result' => $result
  289. ]);
  290. return response()->json([
  291. 'success' => $result,
  292. 'message' => $result ? '操作成功' : '操作失败',
  293. 'data' => [
  294. 'mistake_id' => $mistakeId,
  295. 'added' => $result
  296. ]
  297. ]);
  298. } catch (\Exception $e) {
  299. Log::error('加入重练清单失败', [
  300. 'mistake_id' => $mistakeId,
  301. 'error' => $e->getMessage(),
  302. 'trace' => $e->getTraceAsString()
  303. ]);
  304. return response()->json([
  305. 'success' => false,
  306. 'message' => '加入重练清单失败: ' . $e->getMessage()
  307. ], 500);
  308. }
  309. }
  310. /**
  311. * 获取题目正确率
  312. *
  313. * @param string $questionId
  314. * @return JsonResponse
  315. */
  316. public function getQuestionAccuracy(string $questionId): JsonResponse
  317. {
  318. try {
  319. $accuracy = $this->mistakeBookService->getQuestionAccuracy($questionId);
  320. Log::info('获取题目正确率成功', [
  321. 'question_id' => $questionId,
  322. 'accuracy' => $accuracy
  323. ]);
  324. return response()->json([
  325. 'success' => true,
  326. 'data' => [
  327. 'question_id' => $questionId,
  328. 'accuracy' => $accuracy,
  329. 'correct_attempts' => null // LearningAnalytics不返回具体次数
  330. ]
  331. ]);
  332. } catch (\Exception $e) {
  333. Log::error('获取题目正确率失败', [
  334. 'question_id' => $questionId,
  335. 'error' => $e->getMessage(),
  336. 'trace' => $e->getTraceAsString()
  337. ]);
  338. return response()->json([
  339. 'success' => false,
  340. 'message' => '获取题目正确率失败: ' . $e->getMessage()
  341. ], 500);
  342. }
  343. }
  344. /**
  345. * 推荐练习题
  346. *
  347. * @param Request $request
  348. * @return JsonResponse
  349. */
  350. public function recommendPractice(Request $request): JsonResponse
  351. {
  352. try {
  353. $studentId = $request->get('student_id');
  354. $kpIds = $request->get('kp_ids', []);
  355. $skillIds = $request->get('skill_ids', []);
  356. if (!$studentId) {
  357. return response()->json([
  358. 'success' => false,
  359. 'message' => '缺少必要参数:student_id'
  360. ], 400);
  361. }
  362. $recommendations = $this->mistakeBookService->recommendPractice(
  363. $studentId,
  364. $kpIds,
  365. $skillIds
  366. );
  367. Log::info('获取推荐练习题成功', [
  368. 'student_id' => $studentId,
  369. 'kp_ids' => $kpIds,
  370. 'recommendations_count' => count($recommendations['data'] ?? [])
  371. ]);
  372. return response()->json([
  373. 'success' => true,
  374. 'data' => $recommendations['data'] ?? []
  375. ]);
  376. } catch (\Exception $e) {
  377. Log::error('获取推荐练习题失败', [
  378. 'student_id' => $request->get('student_id'),
  379. 'error' => $e->getMessage(),
  380. 'trace' => $e->getTraceAsString()
  381. ]);
  382. return response()->json([
  383. 'success' => false,
  384. 'message' => '获取推荐练习题失败: ' . $e->getMessage()
  385. ], 500);
  386. }
  387. }
  388. /**
  389. * 获取错题本快照数据(用于仪表板)
  390. *
  391. * @param Request $request
  392. * @return JsonResponse
  393. */
  394. public function getSnapshot(Request $request): JsonResponse
  395. {
  396. try {
  397. $studentId = $request->get('student_id');
  398. $limit = (int) ($request->get('limit', 5));
  399. if (!$studentId) {
  400. return response()->json([
  401. 'success' => false,
  402. 'message' => '缺少必要参数:student_id'
  403. ], 400);
  404. }
  405. $snapshot = $this->mistakeBookService->getPanelSnapshot($studentId, $limit);
  406. Log::info('获取错题本快照数据成功', [
  407. 'student_id' => $studentId,
  408. 'limit' => $limit,
  409. 'recent_count' => count($snapshot['recent'] ?? [])
  410. ]);
  411. return response()->json([
  412. 'success' => true,
  413. 'data' => $snapshot
  414. ]);
  415. } catch (\Exception $e) {
  416. Log::error('获取错题本快照数据失败', [
  417. 'student_id' => $request->get('student_id'),
  418. 'error' => $e->getMessage(),
  419. 'trace' => $e->getTraceAsString()
  420. ]);
  421. return response()->json([
  422. 'success' => false,
  423. 'message' => '获取错题本快照数据失败: ' . $e->getMessage()
  424. ], 500);
  425. }
  426. }
  427. /**
  428. * 修改错题复习状态
  429. */
  430. public function updateReviewStatus(Request $request, string $mistakeId): JsonResponse
  431. {
  432. try {
  433. $validated = $request->validate([
  434. 'action' => 'required|in:increment,reset',
  435. 'force_review' => 'boolean',
  436. ]);
  437. $action = $validated['action'];
  438. $forceReview = $validated['force_review'] ?? false;
  439. $result = $this->mistakeBookService->updateReviewStatus($mistakeId, $action, $forceReview);
  440. if ($result['success'] ?? false) {
  441. return response()->json([
  442. 'success' => true,
  443. 'data' => $result,
  444. 'message' => $result['message'] ?? '复习状态更新成功',
  445. ]);
  446. }
  447. return response()->json([
  448. 'success' => false,
  449. 'message' => $result['error'] ?? '更新复习状态失败',
  450. ], 400);
  451. } catch (\Throwable $e) {
  452. Log::error('Update review status error', [
  453. 'mistake_id' => $mistakeId,
  454. 'error' => $e->getMessage(),
  455. ]);
  456. return response()->json([
  457. 'success' => false,
  458. 'message' => '更新复习状态失败',
  459. 'error' => $e->getMessage(),
  460. ], 500);
  461. }
  462. }
  463. /**
  464. * 获取错题复习状态
  465. */
  466. public function getReviewStatus(string $mistakeId): JsonResponse
  467. {
  468. try {
  469. $result = $this->mistakeBookService->getReviewStatus($mistakeId);
  470. if ($result['success'] ?? false) {
  471. return response()->json([
  472. 'success' => true,
  473. 'data' => $result,
  474. ]);
  475. }
  476. return response()->json([
  477. 'success' => false,
  478. 'message' => $result['error'] ?? '获取复习状态失败',
  479. ], 400);
  480. } catch (\Throwable $e) {
  481. Log::error('Get review status error', [
  482. 'mistake_id' => $mistakeId,
  483. 'error' => $e->getMessage(),
  484. ]);
  485. return response()->json([
  486. 'success' => false,
  487. 'message' => '获取复习状态失败',
  488. 'error' => $e->getMessage(),
  489. ], 500);
  490. }
  491. }
  492. /**
  493. * 增加复习次数
  494. */
  495. public function incrementReview(Request $request, string $mistakeId): JsonResponse
  496. {
  497. try {
  498. $validated = $request->validate([
  499. 'force_review' => 'boolean',
  500. ]);
  501. $forceReview = $validated['force_review'] ?? false;
  502. $result = $this->mistakeBookService->incrementReviewCount($mistakeId, $forceReview);
  503. if ($result['success'] ?? false) {
  504. return response()->json([
  505. 'success' => true,
  506. 'data' => $result,
  507. 'message' => $result['message'] ?? '复习次数增加成功',
  508. ]);
  509. }
  510. return response()->json([
  511. 'success' => false,
  512. 'message' => $result['error'] ?? '增加复习次数失败',
  513. ], 400);
  514. } catch (\Throwable $e) {
  515. Log::error('Increment review count error', [
  516. 'mistake_id' => $mistakeId,
  517. 'error' => $e->getMessage(),
  518. ]);
  519. return response()->json([
  520. 'success' => false,
  521. 'message' => '增加复习次数失败',
  522. 'error' => $e->getMessage(),
  523. ], 500);
  524. }
  525. }
  526. /**
  527. * 重置为强制复习状态
  528. */
  529. public function resetReview(string $mistakeId): JsonResponse
  530. {
  531. try {
  532. $result = $this->mistakeBookService->resetReviewStatus($mistakeId);
  533. if ($result['success'] ?? false) {
  534. return response()->json([
  535. 'success' => true,
  536. 'data' => $result,
  537. 'message' => $result['message'] ?? '复习状态重置成功',
  538. ]);
  539. }
  540. return response()->json([
  541. 'success' => false,
  542. 'message' => $result['error'] ?? '重置复习状态失败',
  543. ], 400);
  544. } catch (\Throwable $e) {
  545. Log::error('Reset review status error', [
  546. 'mistake_id' => $mistakeId,
  547. 'error' => $e->getMessage(),
  548. ]);
  549. return response()->json([
  550. 'success' => false,
  551. 'message' => '重置复习状态失败',
  552. 'error' => $e->getMessage(),
  553. ], 500);
  554. }
  555. }
  556. }