api.php 37 KB

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