api.php 27 KB

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