api.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. <?php
  2. use App\Http\Controllers\Api\IntelligentExamController;
  3. use App\Http\Controllers\Api\PreQuestionApiController;
  4. use App\Http\Controllers\Api\TextbookApiController;
  5. use App\Http\Controllers\Api\MistakeBookController;
  6. use App\Http\Controllers\Api\QuestionSearchController;
  7. use App\Http\Controllers\Api\QuestionRandomController;
  8. use App\Http\Controllers\Api\PaperAssembleController;
  9. use App\Http\Controllers\Api\PaperJsonController;
  10. use App\Http\Controllers\Api\QuestionSolutionController;
  11. use App\Http\Controllers\Api\KnowledgeRecommendController;
  12. use App\Http\Controllers\Api\AbilityEvaluateController;
  13. use App\Services\QuestionServiceApi;
  14. use Illuminate\Support\Facades\Log;
  15. use Illuminate\Support\Facades\Route;
  16. use App\Events\QuestionGenerationCompleted;
  17. use App\Events\QuestionGenerationFailed;
  18. use Illuminate\Auth\Middleware\Authenticate;
  19. use App\Http\Controllers\Api\ExamAnalysisApiController;
  20. /*
  21. |--------------------------------------------------------------------------
  22. | 题库管理 API 路由
  23. |--------------------------------------------------------------------------
  24. */
  25. // 给 Python/内部服务消费的“筛选题库”API(需要 X-Internal-Token)
  26. Route::middleware('internal.token')->get('/pre-questions', [PreQuestionApiController::class, 'index'])
  27. ->name('api.pre-questions.index');
  28. Route::get('/questions/search', QuestionSearchController::class)->name('api.questions.search');
  29. Route::get('/questions/random', QuestionRandomController::class)->name('api.questions.random');
  30. Route::post('/papers/assemble', PaperAssembleController::class)->name('api.papers.assemble');
  31. Route::get('/papers/{paperId}/json', [PaperJsonController::class, 'show'])->name('api.papers.json');
  32. Route::get('/questions/{id}/solution', QuestionSolutionController::class)->name('api.questions.solution');
  33. Route::get('/knowledge/recommend', KnowledgeRecommendController::class)->name('api.knowledge.recommend');
  34. Route::post('/abilities/evaluate', AbilityEvaluateController::class)->name('api.abilities.evaluate');
  35. // 接收题目生成回调
  36. Route::post('/questions/callback', function () {
  37. try {
  38. $data = request()->all();
  39. Log::info('Received question generation callback', $data);
  40. // 验证回调数据
  41. if (!isset($data['task_id']) || !isset($data['status'])) {
  42. return response()->json(['error' => 'Invalid callback data'], 400);
  43. }
  44. // 处理回调数据并存储通知到session
  45. if ($data['status'] === 'completed') {
  46. $result = $data['result'] ?? [];
  47. $total = $result['total'] ?? $data['total'] ?? ($result['saved'] ?? 0);
  48. $kpCode = $result['kp_code'] ?? $data['kp_code'] ?? '';
  49. // 将成功通知存储到session,供下次页面刷新时显示
  50. session()->flash('notification', [
  51. 'type' => 'success',
  52. 'title' => '✅ 题目生成完成',
  53. 'body' => "任务 ID: {$data['task_id']}\n生成题目: {$total} 道" . ($kpCode ? "\n知识点: {$kpCode}" : ''),
  54. 'color' => 'success'
  55. ]);
  56. Log::info("题目生成成功通知已存储", [
  57. 'task_id' => $data['task_id'],
  58. 'total' => $total,
  59. 'kp_code' => $kpCode
  60. ]);
  61. } elseif ($data['status'] === 'failed') {
  62. $error = $data['error'] ?? '未知错误';
  63. // 将失败通知存储到session
  64. session()->flash('notification', [
  65. 'type' => 'error',
  66. 'title' => '❌ 题目生成失败',
  67. 'body' => "任务 ID: {$data['task_id']}\n错误: {$error}",
  68. 'color' => 'danger'
  69. ]);
  70. Log::error("题目生成失败通知已存储", [
  71. 'task_id' => $data['task_id'],
  72. 'error' => $error
  73. ]);
  74. }
  75. return response()->json([
  76. 'success' => true,
  77. 'message' => 'Callback received and notification stored',
  78. 'status' => $data['status']
  79. ]);
  80. } catch (\Exception $e) {
  81. Log::error('Callback processing failed: ' . $e->getMessage());
  82. return response()->json(['error' => $e->getMessage()], 500);
  83. }
  84. })->name('api.questions.callback');
  85. // 接收OCR题目生成回调
  86. Route::post('/ocr-question-callback', function () {
  87. try {
  88. $data = request()->all();
  89. Log::info('Received OCR question generation callback', $data);
  90. // 验证必要的回调数据
  91. if (!isset($data['task_id']) || !isset($data['status']) || !isset($data['ocr_record_id'])) {
  92. Log::error('OCR callback missing required fields', $data);
  93. return response()->json([
  94. 'success' => false,
  95. 'error' => 'Missing required fields: task_id, status, ocr_record_id'
  96. ], 400);
  97. }
  98. $taskId = $data['task_id'];
  99. $ocrRecordId = $data['ocr_record_id'];
  100. $status = $data['status'];
  101. // 将回调结果存储到缓存中,供前端查询(保留30秒)
  102. $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
  103. cache([$cacheKey => $data], now()->addSeconds(30));
  104. Log::info("OCR callback cached with key: {$cacheKey}", [
  105. 'ocr_record_id' => $ocrRecordId,
  106. 'task_id' => $taskId,
  107. 'status' => $status,
  108. 'total_generated' => $data['result']['total_generated'] ?? 0,
  109. 'total_saved' => $data['result']['total_saved'] ?? 0
  110. ]);
  111. // 处理题目关联逻辑
  112. if ($status === 'completed') {
  113. $updatedCount = 0;
  114. // 从result中提取question_mappings(QuestionBank API将它放在result字段中)
  115. $mappings = $data['result']['question_mappings'] ?? $data['question_mappings'] ?? [];
  116. Log::info("Processing OCR question associations", [
  117. 'ocr_record_id' => $ocrRecordId,
  118. 'task_id' => $taskId,
  119. 'mappings_count' => count($mappings)
  120. ]);
  121. // 更新ocr_question_results表中的关联关系
  122. foreach ($mappings as $mapping) {
  123. try {
  124. $ocrQuestionNumber = $mapping['ocr_question_number'] ?? null;
  125. $questionBankId = $mapping['question_bank_id'] ?? null;
  126. $questionCode = $mapping['question_code'] ?? null;
  127. if ($ocrQuestionNumber && $questionBankId) {
  128. // 查找对应的OCR题目结果并更新
  129. $updated = DB::table('ocr_question_results')
  130. ->where('ocr_record_id', $ocrRecordId)
  131. ->where('question_number', $ocrQuestionNumber)
  132. ->update([
  133. 'question_bank_id' => $questionBankId,
  134. 'generation_status' => 'completed',
  135. 'generation_task_id' => $taskId,
  136. 'generation_error' => null,
  137. ]);
  138. if ($updated) {
  139. $updatedCount++;
  140. Log::info("Updated OCR question association", [
  141. 'ocr_record_id' => $ocrRecordId,
  142. 'question_number' => $ocrQuestionNumber,
  143. 'question_bank_id' => $questionBankId,
  144. 'question_code' => $questionCode
  145. ]);
  146. } else {
  147. Log::warning("No OCR question result found for association", [
  148. 'ocr_record_id' => $ocrRecordId,
  149. 'question_number' => $ocrQuestionNumber
  150. ]);
  151. }
  152. }
  153. } catch (\Exception $e) {
  154. Log::error("Failed to update OCR question association", [
  155. 'mapping' => $mapping,
  156. 'error' => $e->getMessage()
  157. ]);
  158. }
  159. }
  160. Log::info("OCR question association completed", [
  161. 'ocr_record_id' => $ocrRecordId,
  162. 'task_id' => $taskId,
  163. 'total_mappings' => count($mappings),
  164. 'updated_count' => $updatedCount
  165. ]);
  166. // 更新OCR记录的整体状态为已完成
  167. try {
  168. DB::table('ocr_records')
  169. ->where('id', $ocrRecordId)
  170. ->update([
  171. 'status' => 'completed',
  172. 'processed_at' => now(),
  173. 'updated_at' => now()
  174. ]);
  175. Log::info("Updated OCR record status to completed", [
  176. 'ocr_record_id' => $ocrRecordId,
  177. 'task_id' => $taskId
  178. ]);
  179. } catch (\Exception $e) {
  180. Log::error("Failed to update OCR record status", [
  181. 'ocr_record_id' => $ocrRecordId,
  182. 'error' => $e->getMessage()
  183. ]);
  184. }
  185. } elseif ($status === 'failed') {
  186. // 更新所有相关的OCR题目结果为失败状态
  187. try {
  188. $updated = DB::table('ocr_question_results')
  189. ->where('ocr_record_id', $ocrRecordId)
  190. ->where('generation_status', 'pending') // 只更新待处理的
  191. ->update([
  192. 'generation_status' => 'failed',
  193. 'generation_task_id' => $taskId,
  194. 'generation_error' => $data['error'] ?? 'Unknown error',
  195. ]);
  196. Log::info("Updated OCR questions to failed status", [
  197. 'ocr_record_id' => $ocrRecordId,
  198. 'task_id' => $taskId,
  199. 'updated_count' => $updated,
  200. 'error' => $data['error'] ?? 'Unknown error'
  201. ]);
  202. // 更新OCR记录的状态为失败
  203. DB::table('ocr_records')
  204. ->where('id', $ocrRecordId)
  205. ->update([
  206. 'status' => 'failed',
  207. 'error_message' => $data['error'] ?? 'Question generation failed',
  208. 'updated_at' => now()
  209. ]);
  210. Log::info("Updated OCR record status to failed", [
  211. 'ocr_record_id' => $ocrRecordId,
  212. 'task_id' => $taskId,
  213. 'error' => $data['error'] ?? 'Unknown error'
  214. ]);
  215. } catch (\Exception $e) {
  216. Log::error("Failed to update OCR questions to failed status", [
  217. 'ocr_record_id' => $ocrRecordId,
  218. 'error' => $e->getMessage()
  219. ]);
  220. }
  221. }
  222. return response()->json([
  223. 'success' => true,
  224. 'message' => 'OCR callback received and processed',
  225. 'data' => [
  226. 'task_id' => $taskId,
  227. 'ocr_record_id' => $ocrRecordId,
  228. 'status' => $status,
  229. 'cache_key' => $cacheKey,
  230. 'associations_processed' => $status === 'completed' ? count($data['question_mappings'] ?? []) : 0
  231. ]
  232. ]);
  233. } catch (\Exception $e) {
  234. Log::error('OCR callback processing failed: ' . $e->getMessage());
  235. Log::error('Exception details: ' . $e->getTraceAsString());
  236. return response()->json([
  237. 'success' => false,
  238. 'error' => 'Callback processing failed: ' . $e->getMessage()
  239. ], 500);
  240. }
  241. })->name('api.ocr.callback');
  242. // 获取题目生成回调结果
  243. Route::get('/questions/callback/{taskId}', function (string $taskId) {
  244. // ✅ 优先从缓存读取(跨域友好)
  245. $callbackData = cache($taskId);
  246. if ($callbackData) {
  247. // 清除已读取的回调数据
  248. cache()->forget($taskId);
  249. session()->forget('question_gen_callback_' . $taskId);
  250. return response()->json($callbackData);
  251. }
  252. // 备选:从session读取
  253. $sessionData = session('question_gen_callback_' . $taskId);
  254. if ($sessionData) {
  255. // 清除已读取的回调数据
  256. session()->forget('question_gen_callback_' . $taskId);
  257. return response()->json($sessionData);
  258. }
  259. // 未收到回调
  260. return response()->json(['status' => 'pending'], 202);
  261. })->name('api.questions.callback.get');
  262. // 获取OCR题目生成回调结果
  263. Route::get('/ocr-question-callback/{ocrRecordId}/{taskId}', function (int $ocrRecordId, string $taskId) {
  264. $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
  265. $callbackData = cache($cacheKey);
  266. if ($callbackData) {
  267. // 清除已读取的回调数据
  268. cache()->forget($cacheKey);
  269. return response()->json([
  270. 'success' => true,
  271. 'data' => $callbackData
  272. ]);
  273. }
  274. return response()->json([
  275. 'success' => false,
  276. 'status' => 'pending',
  277. 'message' => 'OCR callback not received yet'
  278. ], 202);
  279. })->name('api.ocr.callback.get');
  280. // 题目相关 API
  281. Route::get('/questions', function (QuestionServiceApi $service) {
  282. try {
  283. $page = (int) request()->get('page', 1);
  284. $perPage = (int) request()->get('per_page', 25);
  285. $filters = [
  286. 'kp_code' => request()->get('kp_code'),
  287. 'difficulty' => request()->get('difficulty'),
  288. 'search' => request()->get('search'),
  289. ];
  290. $response = $service->listQuestions($page, $perPage, $filters);
  291. return response()->json($response);
  292. } catch (\Exception $e) {
  293. \Log::error('Failed to fetch questions: ' . $e->getMessage());
  294. return response()->json([
  295. 'data' => [],
  296. 'meta' => [
  297. 'page' => 1,
  298. 'per_page' => 25,
  299. 'total' => 0,
  300. 'total_pages' => 0,
  301. ],
  302. 'error' => $e->getMessage(),
  303. ], 500);
  304. }
  305. });
  306. // 获取题目统计信息
  307. Route::get('/questions/statistics', function (QuestionServiceApi $service) {
  308. try {
  309. $stats = $service->getStatistics();
  310. return response()->json($stats);
  311. } catch (\Exception $e) {
  312. \Log::error('Failed to get question statistics: ' . $e->getMessage());
  313. return response()->json(['error' => $e->getMessage()], 500);
  314. }
  315. });
  316. // 语义搜索题目
  317. Route::post('/questions/search', function (QuestionServiceApi $service) {
  318. try {
  319. $data = request()->only(['query', 'limit']);
  320. $results = $service->searchQuestions($data['query'], $data['limit'] ?? 20);
  321. return response()->json($results);
  322. } catch (\Exception $e) {
  323. \Log::error('Question search failed: ' . $e->getMessage());
  324. return response()->json(['error' => $e->getMessage()], 500);
  325. }
  326. });
  327. // 获取单个题目详情
  328. Route::get('/questions/{id}', function (int $id, QuestionServiceApi $service) {
  329. try {
  330. $question = $service->getQuestionById($id);
  331. if (!$question) {
  332. return response()->json(['error' => 'Question not found'], 404);
  333. }
  334. return response()->json($question);
  335. } catch (\Exception $e) {
  336. \Log::error("Failed to get question {$id}: " . $e->getMessage());
  337. return response()->json(['error' => $e->getMessage()], 500);
  338. }
  339. });
  340. // AI 生成题目
  341. Route::post('/questions/generate', function (QuestionServiceApi $service) {
  342. try {
  343. $data = request()->only(['kp_code', 'keyword', 'count', 'strategy']);
  344. $result = $service->generateQuestions($data);
  345. return response()->json($result);
  346. } catch (\Exception $e) {
  347. \Log::error('Question generation failed: ' . $e->getMessage());
  348. return response()->json([
  349. 'success' => false,
  350. 'message' => $e->getMessage(),
  351. ], 500);
  352. }
  353. });
  354. // 删除题目
  355. Route::delete('/questions/{id}', function (int $id, QuestionServiceApi $service) {
  356. try {
  357. $deleted = $service->deleteQuestion($id);
  358. return response()->json([
  359. 'success' => $deleted,
  360. 'message' => $deleted ? 'Question deleted' : 'Failed to delete',
  361. ]);
  362. } catch (\Exception $e) {
  363. \Log::error("Failed to delete question {$id}: " . $e->getMessage());
  364. return response()->json([
  365. 'success' => false,
  366. 'message' => $e->getMessage(),
  367. ], 500);
  368. }
  369. });
  370. // 获取知识点选项
  371. Route::get('/knowledge-points', function (QuestionServiceApi $service) {
  372. try {
  373. $points = $service->getKnowledgePointOptions();
  374. return response()->json($points);
  375. } catch (\Exception $e) {
  376. \Log::error('Failed to get knowledge points: ' . $e->getMessage());
  377. return response()->json([], 500);
  378. }
  379. });
  380. // 智能出卷对外接口:生成试卷并返回PDF/判卷地址
  381. Route::post('/intelligent-exams', [IntelligentExamController::class, 'store'])
  382. ->withoutMiddleware([
  383. Authenticate::class,
  384. 'auth',
  385. 'auth:sanctum',
  386. 'auth:api',
  387. ])
  388. ->name('api.intelligent-exams.store');
  389. // 智能出卷任务状态查询
  390. Route::get('/intelligent-exams/status/{taskId}', [IntelligentExamController::class, 'status'])
  391. ->withoutMiddleware([
  392. Authenticate::class,
  393. 'auth',
  394. 'auth:sanctum',
  395. 'auth:api',
  396. ])
  397. ->name('api.intelligent-exams.status');
  398. // 学情报告对外接口:生成并返回学情报告 PDF
  399. Route::post('/exam-analysis/report', [ExamAnalysisApiController::class, 'store'])
  400. ->withoutMiddleware([
  401. Authenticate::class,
  402. 'auth',
  403. 'auth:sanctum',
  404. 'auth:api',
  405. ])
  406. ->name('api.exam-analysis.report');
  407. // 学情报告任务状态查询
  408. Route::get('/exam-analysis/status/{taskId}', [ExamAnalysisApiController::class, 'status'])
  409. ->withoutMiddleware([
  410. Authenticate::class,
  411. 'auth',
  412. 'auth:sanctum',
  413. 'auth:api',
  414. ])
  415. ->name('api.exam-analysis.status');
  416. /*
  417. |--------------------------------------------------------------------------
  418. | 错题本 API 路由
  419. |--------------------------------------------------------------------------
  420. */
  421. // 获取错题列表
  422. Route::get('/mistake-book', [MistakeBookController::class, 'listMistakes'])
  423. ->withoutMiddleware([
  424. Authenticate::class,
  425. 'auth',
  426. 'auth:sanctum',
  427. 'auth:api',
  428. ])
  429. ->name('api.mistake-book.list');
  430. // 新增错题
  431. Route::post('/mistake-book', [MistakeBookController::class, 'addMistake'])
  432. ->withoutMiddleware([
  433. Authenticate::class,
  434. 'auth',
  435. 'auth:sanctum',
  436. 'auth:api',
  437. ])
  438. ->name('api.mistake-book.create');
  439. // 获取单条错题详情
  440. Route::get('/mistake-book/{mistakeId}', [MistakeBookController::class, 'getMistakeDetail'])
  441. ->withoutMiddleware([
  442. Authenticate::class,
  443. 'auth',
  444. 'auth:sanctum',
  445. 'auth:api',
  446. ])
  447. ->name('api.mistake-book.detail');
  448. // 获取错题统计概要
  449. Route::get('/mistake-book/summary', [MistakeBookController::class, 'getSummary'])
  450. ->withoutMiddleware([
  451. Authenticate::class,
  452. 'auth',
  453. 'auth:sanctum',
  454. 'auth:api',
  455. ])
  456. ->name('api.mistake-book.summary');
  457. // 获取错误模式分析
  458. Route::get('/mistake-book/analytics/mistake-pattern', [MistakeBookController::class, 'getMistakePatterns'])
  459. ->withoutMiddleware([
  460. Authenticate::class,
  461. 'auth',
  462. 'auth:sanctum',
  463. 'auth:api',
  464. ])
  465. ->name('api.mistake-book.patterns');
  466. // 收藏/取消收藏错题
  467. Route::post('/mistake-book/{mistakeId}/favorite', [MistakeBookController::class, 'toggleFavorite'])
  468. ->withoutMiddleware([
  469. Authenticate::class,
  470. 'auth',
  471. 'auth:sanctum',
  472. 'auth:api',
  473. ])
  474. ->name('api.mistake-book.favorite');
  475. // 标记错题已复习
  476. Route::post('/mistake-book/{mistakeId}/review', [MistakeBookController::class, 'markReviewed'])
  477. ->withoutMiddleware([
  478. Authenticate::class,
  479. 'auth',
  480. 'auth:sanctum',
  481. 'auth:api',
  482. ])
  483. ->name('api.mistake-book.review');
  484. // 加入重练清单
  485. Route::post('/mistake-book/{mistakeId}/retry-list', [MistakeBookController::class, 'addToRetryList'])
  486. ->withoutMiddleware([
  487. Authenticate::class,
  488. 'auth',
  489. 'auth:sanctum',
  490. 'auth:api',
  491. ])
  492. ->name('api.mistake-book.retry-list');
  493. // 推荐练习题
  494. Route::post('/mistake-book/recommend-practice', [MistakeBookController::class, 'recommendPractice'])
  495. ->withoutMiddleware([
  496. Authenticate::class,
  497. 'auth',
  498. 'auth:sanctum',
  499. 'auth:api',
  500. ])
  501. ->name('api.mistake-book.recommend-practice');
  502. // 获取错题本快照数据(仪表板用)
  503. Route::get('/mistake-book/snapshot', [MistakeBookController::class, 'getSnapshot'])
  504. ->withoutMiddleware([
  505. Authenticate::class,
  506. 'auth',
  507. 'auth:sanctum',
  508. 'auth:api',
  509. ])
  510. ->name('api.mistake-book.snapshot');
  511. /*
  512. |--------------------------------------------------------------------------
  513. | 错题复习状态管理 API 路由
  514. |--------------------------------------------------------------------------
  515. */
  516. Route::post('/mistake-book/{mistakeId}/review-status', [MistakeBookController::class, 'updateReviewStatus'])
  517. ->withoutMiddleware([
  518. Authenticate::class,
  519. 'auth',
  520. 'auth:sanctum',
  521. 'auth:api',
  522. ])
  523. ->name('api.mistake-book.review-status.update');
  524. Route::get('/mistake-book/{mistakeId}/review-status', [MistakeBookController::class, 'getReviewStatus'])
  525. ->withoutMiddleware([
  526. Authenticate::class,
  527. 'auth',
  528. 'auth:sanctum',
  529. 'auth:api',
  530. ])
  531. ->name('api.mistake-book.review-status.get');
  532. Route::post('/mistake-book/{mistakeId}/increment-review', [MistakeBookController::class, 'incrementReview'])
  533. ->withoutMiddleware([
  534. Authenticate::class,
  535. 'auth',
  536. 'auth:sanctum',
  537. 'auth:api',
  538. ])
  539. ->name('api.mistake-book.increment-review');
  540. Route::post('/mistake-book/{mistakeId}/reset-review', [MistakeBookController::class, 'resetReview'])
  541. ->withoutMiddleware([
  542. Authenticate::class,
  543. 'auth',
  544. 'auth:sanctum',
  545. 'auth:api',
  546. ])
  547. ->name('api.mistake-book.reset-review');
  548. /*
  549. |--------------------------------------------------------------------------
  550. | 错题批量操作 API 路由
  551. |--------------------------------------------------------------------------
  552. */
  553. Route::post('/mistake-book/batch-operation', [MistakeBookController::class, 'batchOperation'])
  554. ->withoutMiddleware([
  555. Authenticate::class,
  556. 'auth',
  557. 'auth:sanctum',
  558. 'auth:api',
  559. ])
  560. ->name('api.mistake-book.batch-operation');
  561. Route::post('/mistake-book/batch/mark-reviewed', [MistakeBookController::class, 'batchMarkReviewed'])
  562. ->withoutMiddleware([
  563. Authenticate::class,
  564. 'auth',
  565. 'auth:sanctum',
  566. 'auth:api',
  567. ])
  568. ->name('api.mistake-book.batch.mark-reviewed');
  569. Route::post('/mistake-book/batch/mark-mastered', [MistakeBookController::class, 'batchMarkMastered'])
  570. ->withoutMiddleware([
  571. Authenticate::class,
  572. 'auth',
  573. 'auth:sanctum',
  574. 'auth:api',
  575. ])
  576. ->name('api.mistake-book.batch.mark-mastered');
  577. Route::post('/mistake-book/batch/add-to-retry-list', [MistakeBookController::class, 'batchAddToRetryList'])
  578. ->withoutMiddleware([
  579. Authenticate::class,
  580. 'auth',
  581. 'auth:sanctum',
  582. 'auth:api',
  583. ])
  584. ->name('api.mistake-book.batch.add-to-retry-list');
  585. Route::post('/mistake-book/batch/remove-from-retry-list', [MistakeBookController::class, 'batchRemoveFromRetryList'])
  586. ->withoutMiddleware([
  587. Authenticate::class,
  588. 'auth',
  589. 'auth:sanctum',
  590. 'auth:api',
  591. ])
  592. ->name('api.mistake-book.batch.remove-from-retry-list');
  593. Route::post('/mistake-book/batch/set-error-type', [MistakeBookController::class, 'batchSetErrorType'])
  594. ->withoutMiddleware([
  595. Authenticate::class,
  596. 'auth',
  597. 'auth:sanctum',
  598. 'auth:api',
  599. ])
  600. ->name('api.mistake-book.batch.set-error-type');
  601. Route::post('/mistake-book/batch/set-importance', [MistakeBookController::class, 'batchSetImportance'])
  602. ->withoutMiddleware([
  603. Authenticate::class,
  604. 'auth',
  605. 'auth:sanctum',
  606. 'auth:api',
  607. ])
  608. ->name('api.mistake-book.batch.set-importance');
  609. Route::post('/mistake-book/batch/toggle-favorite', [MistakeBookController::class, 'batchToggleFavorite'])
  610. ->withoutMiddleware([
  611. Authenticate::class,
  612. 'auth',
  613. 'auth:sanctum',
  614. 'auth:api',
  615. ])
  616. ->name('api.mistake-book.batch.toggle-favorite');
  617. /*
  618. |--------------------------------------------------------------------------
  619. | 知识点掌握情况 API 路由
  620. |--------------------------------------------------------------------------
  621. */
  622. use App\Http\Controllers\Api\KnowledgeMasteryController;
  623. // 获取学生知识点掌握情况统计
  624. Route::get('/knowledge-mastery/stats/{studentId}', [KnowledgeMasteryController::class, 'stats'])
  625. ->withoutMiddleware([
  626. Authenticate::class,
  627. 'auth',
  628. 'auth:sanctum',
  629. 'auth:api',
  630. ])
  631. ->name('api.knowledge-mastery.stats');
  632. // 获取学生知识点掌握摘要
  633. Route::get('/knowledge-mastery/summary/{studentId}', [KnowledgeMasteryController::class, 'summary'])
  634. ->withoutMiddleware([
  635. Authenticate::class,
  636. 'auth',
  637. 'auth:sanctum',
  638. 'auth:api',
  639. ])
  640. ->name('api.knowledge-mastery.summary');
  641. // 获取学生知识点图谱数据
  642. Route::get('/knowledge-mastery/graph/{studentId}', [KnowledgeMasteryController::class, 'graph'])
  643. ->withoutMiddleware([
  644. Authenticate::class,
  645. 'auth',
  646. 'auth:sanctum',
  647. 'auth:api',
  648. ])
  649. ->name('api.knowledge-mastery.graph');
  650. // 获取学生知识点图谱快照列表
  651. Route::get('/knowledge-mastery/graph/snapshots/{studentId}', [KnowledgeMasteryController::class, 'graphSnapshots'])
  652. ->withoutMiddleware([
  653. Authenticate::class,
  654. 'auth',
  655. 'auth:sanctum',
  656. 'auth:api',
  657. ])
  658. ->name('api.knowledge-mastery.graph.snapshots');
  659. // 创建知识点掌握度快照
  660. Route::post('/knowledge-mastery/snapshot/{studentId}', [KnowledgeMasteryController::class, 'createSnapshot'])
  661. ->withoutMiddleware([
  662. Authenticate::class,
  663. 'auth',
  664. 'auth:sanctum',
  665. 'auth:api',
  666. ])
  667. ->name('api.knowledge-mastery.snapshot.create');
  668. /*
  669. |--------------------------------------------------------------------------
  670. | 教材管理 API 路由
  671. |--------------------------------------------------------------------------
  672. */
  673. // 获取教材列表(按年级排序)
  674. Route::get('/textbooks', [TextbookApiController::class, 'index'])
  675. ->name('api.textbooks.index');
  676. // 根据年级获取教材
  677. Route::get('/textbooks/grade/{grade}', [TextbookApiController::class, 'getByGrade'])
  678. ->name('api.textbooks.by-grade');
  679. // 获取教材系列列表(必须在 {id} 路由之前定义)
  680. Route::get('/textbooks/series', [TextbookApiController::class, 'getSeries'])
  681. ->name('api.textbooks.series');
  682. // 获取年级枚举
  683. Route::get('/textbooks/grades', [TextbookApiController::class, 'getGradeEnums'])
  684. ->name('api.textbooks.grades');
  685. // 获取单个教材详情
  686. Route::get('/textbooks/{id}', [TextbookApiController::class, 'show'])
  687. ->name('api.textbooks.show');
  688. // 获取教材目录
  689. Route::get('/textbooks/{id}/catalog', [TextbookApiController::class, 'getCatalog'])
  690. ->name('api.textbooks.catalog');
  691. /*
  692. |--------------------------------------------------------------------------
  693. | MathRecSys 集成 API 路由
  694. |--------------------------------------------------------------------------
  695. */
  696. use App\Http\Controllers\Api\StudentController;
  697. // 健康检查
  698. Route::get('/mathrecsys/health', [StudentController::class, 'checkServiceHealth'])->name('api.mathrecsys.health');
  699. // 学生相关 API
  700. Route::prefix('mathrecsys/students')->name('api.mathrecsys.students.')->group(function () {
  701. // 获取学生完整信息
  702. Route::get('{studentId}', [StudentController::class, 'show'])->name('show');
  703. // 获取个性化推荐
  704. Route::get('{studentId}/recommendations', [StudentController::class, 'getRecommendations'])->name('recommendations');
  705. // 获取学习轨迹
  706. Route::get('{studentId}/trajectory', [StudentController::class, 'getTrajectory'])->name('trajectory');
  707. // 获取学习建议
  708. Route::get('{studentId}/suggestions', [StudentController::class, 'getSuggestions'])->name('suggestions');
  709. // 智能分析题目
  710. Route::post('{studentId}/analyze', [StudentController::class, 'analyzeQuestion'])->name('analyze');
  711. // 更新掌握度
  712. Route::put('{studentId}/mastery', [StudentController::class, 'updateMastery'])->name('update-mastery');
  713. });
  714. // 班级分析 API
  715. Route::prefix('mathrecsys/classes')->name('api.mathrecsys.classes.')->group(function () {
  716. Route::get('{classId}/analysis', [StudentController::class, 'classAnalysis'])->name('analysis');
  717. });
  718. // 测试 API
  719. Route::get('/mathrecsys/test', function () {
  720. return response()->json([
  721. 'success' => true,
  722. 'message' => 'MathRecSys API integration is working',
  723. 'timestamp' => now()->toISOString()
  724. ]);
  725. })->name('api.mathrecsys.test');
  726. // 测试OCR题目生成API调用
  727. Route::post('/test-ocr-generation', function () {
  728. try {
  729. $service = new \App\Services\QuestionBankService();
  730. // 模拟前端传递的OCR题目数据
  731. $questions = [
  732. [
  733. 'id' => 1,
  734. 'content' => '计算:2+3-4'
  735. ],
  736. [
  737. 'id' => 2,
  738. 'content' => '解方程:x+5=10'
  739. ]
  740. ];
  741. Log::info('开始测试OCR题目生成', [
  742. 'questions_count' => count($questions),
  743. 'ocr_record_id' => 12
  744. ]);
  745. // 使用异步API,系统自动生成回调URL
  746. $response = $service->generateQuestionsFromOcrAsync(
  747. $questions,
  748. '高一',
  749. '数学',
  750. 12, // OCR记录ID
  751. null, // 让系统自动生成回调URL
  752. 'api.ocr.callback' // 回调路由名称
  753. );
  754. Log::info('OCR题目生成响应', [
  755. 'response' => $response,
  756. 'status' => $response['status'] ?? 'unknown',
  757. 'task_id' => $response['task_id'] ?? 'N/A'
  758. ]);
  759. return response()->json([
  760. 'success' => true,
  761. 'message' => 'OCR题目生成测试完成',
  762. 'data' => $response
  763. ]);
  764. } catch (\Exception $e) {
  765. Log::error('测试OCR题目生成失败', [
  766. 'error' => $e->getMessage(),
  767. 'trace' => $e->getTraceAsString()
  768. ]);
  769. return response()->json([
  770. 'success' => false,
  771. 'error' => $e->getMessage()
  772. ], 500);
  773. }
  774. })->name('api.test.ocr.generation');