api.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <?php
  2. use App\Http\Controllers\Api\IntelligentExamController;
  3. use App\Http\Controllers\Api\TextbookApiController;
  4. use App\Services\QuestionServiceApi;
  5. use Illuminate\Support\Facades\Log;
  6. use Illuminate\Support\Facades\Route;
  7. use App\Events\QuestionGenerationCompleted;
  8. use App\Events\QuestionGenerationFailed;
  9. use Illuminate\Auth\Middleware\Authenticate;
  10. use App\Http\Controllers\Api\ExamAnalysisApiController;
  11. /*
  12. |--------------------------------------------------------------------------
  13. | 题库管理 API 路由
  14. |--------------------------------------------------------------------------
  15. */
  16. // 接收题目生成回调
  17. Route::post('/questions/callback', function () {
  18. try {
  19. $data = request()->all();
  20. Log::info('Received question generation callback', $data);
  21. // 验证回调数据
  22. if (!isset($data['task_id']) || !isset($data['status'])) {
  23. return response()->json(['error' => 'Invalid callback data'], 400);
  24. }
  25. // 处理回调数据并存储通知到session
  26. if ($data['status'] === 'completed') {
  27. $result = $data['result'] ?? [];
  28. $total = $result['total'] ?? $data['total'] ?? ($result['saved'] ?? 0);
  29. $kpCode = $result['kp_code'] ?? $data['kp_code'] ?? '';
  30. // 将成功通知存储到session,供下次页面刷新时显示
  31. session()->flash('notification', [
  32. 'type' => 'success',
  33. 'title' => '✅ 题目生成完成',
  34. 'body' => "任务 ID: {$data['task_id']}\n生成题目: {$total} 道" . ($kpCode ? "\n知识点: {$kpCode}" : ''),
  35. 'color' => 'success'
  36. ]);
  37. Log::info("题目生成成功通知已存储", [
  38. 'task_id' => $data['task_id'],
  39. 'total' => $total,
  40. 'kp_code' => $kpCode
  41. ]);
  42. } elseif ($data['status'] === 'failed') {
  43. $error = $data['error'] ?? '未知错误';
  44. // 将失败通知存储到session
  45. session()->flash('notification', [
  46. 'type' => 'error',
  47. 'title' => '❌ 题目生成失败',
  48. 'body' => "任务 ID: {$data['task_id']}\n错误: {$error}",
  49. 'color' => 'danger'
  50. ]);
  51. Log::error("题目生成失败通知已存储", [
  52. 'task_id' => $data['task_id'],
  53. 'error' => $error
  54. ]);
  55. }
  56. return response()->json([
  57. 'success' => true,
  58. 'message' => 'Callback received and notification stored',
  59. 'status' => $data['status']
  60. ]);
  61. } catch (\Exception $e) {
  62. Log::error('Callback processing failed: ' . $e->getMessage());
  63. return response()->json(['error' => $e->getMessage()], 500);
  64. }
  65. })->name('api.questions.callback');
  66. // 接收OCR题目生成回调
  67. Route::post('/ocr-question-callback', function () {
  68. try {
  69. $data = request()->all();
  70. Log::info('Received OCR question generation callback', $data);
  71. // 验证必要的回调数据
  72. if (!isset($data['task_id']) || !isset($data['status']) || !isset($data['ocr_record_id'])) {
  73. Log::error('OCR callback missing required fields', $data);
  74. return response()->json([
  75. 'success' => false,
  76. 'error' => 'Missing required fields: task_id, status, ocr_record_id'
  77. ], 400);
  78. }
  79. $taskId = $data['task_id'];
  80. $ocrRecordId = $data['ocr_record_id'];
  81. $status = $data['status'];
  82. // 将回调结果存储到缓存中,供前端查询(保留30秒)
  83. $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
  84. cache([$cacheKey => $data], now()->addSeconds(30));
  85. Log::info("OCR callback cached with key: {$cacheKey}", [
  86. 'ocr_record_id' => $ocrRecordId,
  87. 'task_id' => $taskId,
  88. 'status' => $status,
  89. 'total_generated' => $data['result']['total_generated'] ?? 0,
  90. 'total_saved' => $data['result']['total_saved'] ?? 0
  91. ]);
  92. // 处理题目关联逻辑
  93. if ($status === 'completed') {
  94. $updatedCount = 0;
  95. // 从result中提取question_mappings(QuestionBank API将它放在result字段中)
  96. $mappings = $data['result']['question_mappings'] ?? $data['question_mappings'] ?? [];
  97. Log::info("Processing OCR question associations", [
  98. 'ocr_record_id' => $ocrRecordId,
  99. 'task_id' => $taskId,
  100. 'mappings_count' => count($mappings)
  101. ]);
  102. // 更新ocr_question_results表中的关联关系
  103. foreach ($mappings as $mapping) {
  104. try {
  105. $ocrQuestionNumber = $mapping['ocr_question_number'] ?? null;
  106. $questionBankId = $mapping['question_bank_id'] ?? null;
  107. $questionCode = $mapping['question_code'] ?? null;
  108. if ($ocrQuestionNumber && $questionBankId) {
  109. // 查找对应的OCR题目结果并更新
  110. $updated = DB::table('ocr_question_results')
  111. ->where('ocr_record_id', $ocrRecordId)
  112. ->where('question_number', $ocrQuestionNumber)
  113. ->update([
  114. 'question_bank_id' => $questionBankId,
  115. 'generation_status' => 'completed',
  116. 'generation_task_id' => $taskId,
  117. 'generation_error' => null,
  118. ]);
  119. if ($updated) {
  120. $updatedCount++;
  121. Log::info("Updated OCR question association", [
  122. 'ocr_record_id' => $ocrRecordId,
  123. 'question_number' => $ocrQuestionNumber,
  124. 'question_bank_id' => $questionBankId,
  125. 'question_code' => $questionCode
  126. ]);
  127. } else {
  128. Log::warning("No OCR question result found for association", [
  129. 'ocr_record_id' => $ocrRecordId,
  130. 'question_number' => $ocrQuestionNumber
  131. ]);
  132. }
  133. }
  134. } catch (\Exception $e) {
  135. Log::error("Failed to update OCR question association", [
  136. 'mapping' => $mapping,
  137. 'error' => $e->getMessage()
  138. ]);
  139. }
  140. }
  141. Log::info("OCR question association completed", [
  142. 'ocr_record_id' => $ocrRecordId,
  143. 'task_id' => $taskId,
  144. 'total_mappings' => count($mappings),
  145. 'updated_count' => $updatedCount
  146. ]);
  147. // 更新OCR记录的整体状态为已完成
  148. try {
  149. DB::table('ocr_records')
  150. ->where('id', $ocrRecordId)
  151. ->update([
  152. 'status' => 'completed',
  153. 'processed_at' => now(),
  154. 'updated_at' => now()
  155. ]);
  156. Log::info("Updated OCR record status to completed", [
  157. 'ocr_record_id' => $ocrRecordId,
  158. 'task_id' => $taskId
  159. ]);
  160. } catch (\Exception $e) {
  161. Log::error("Failed to update OCR record status", [
  162. 'ocr_record_id' => $ocrRecordId,
  163. 'error' => $e->getMessage()
  164. ]);
  165. }
  166. } elseif ($status === 'failed') {
  167. // 更新所有相关的OCR题目结果为失败状态
  168. try {
  169. $updated = DB::table('ocr_question_results')
  170. ->where('ocr_record_id', $ocrRecordId)
  171. ->where('generation_status', 'pending') // 只更新待处理的
  172. ->update([
  173. 'generation_status' => 'failed',
  174. 'generation_task_id' => $taskId,
  175. 'generation_error' => $data['error'] ?? 'Unknown error',
  176. ]);
  177. Log::info("Updated OCR questions to failed status", [
  178. 'ocr_record_id' => $ocrRecordId,
  179. 'task_id' => $taskId,
  180. 'updated_count' => $updated,
  181. 'error' => $data['error'] ?? 'Unknown error'
  182. ]);
  183. // 更新OCR记录的状态为失败
  184. DB::table('ocr_records')
  185. ->where('id', $ocrRecordId)
  186. ->update([
  187. 'status' => 'failed',
  188. 'error_message' => $data['error'] ?? 'Question generation failed',
  189. 'updated_at' => now()
  190. ]);
  191. Log::info("Updated OCR record status to failed", [
  192. 'ocr_record_id' => $ocrRecordId,
  193. 'task_id' => $taskId,
  194. 'error' => $data['error'] ?? 'Unknown error'
  195. ]);
  196. } catch (\Exception $e) {
  197. Log::error("Failed to update OCR questions to failed status", [
  198. 'ocr_record_id' => $ocrRecordId,
  199. 'error' => $e->getMessage()
  200. ]);
  201. }
  202. }
  203. return response()->json([
  204. 'success' => true,
  205. 'message' => 'OCR callback received and processed',
  206. 'data' => [
  207. 'task_id' => $taskId,
  208. 'ocr_record_id' => $ocrRecordId,
  209. 'status' => $status,
  210. 'cache_key' => $cacheKey,
  211. 'associations_processed' => $status === 'completed' ? count($data['question_mappings'] ?? []) : 0
  212. ]
  213. ]);
  214. } catch (\Exception $e) {
  215. Log::error('OCR callback processing failed: ' . $e->getMessage());
  216. Log::error('Exception details: ' . $e->getTraceAsString());
  217. return response()->json([
  218. 'success' => false,
  219. 'error' => 'Callback processing failed: ' . $e->getMessage()
  220. ], 500);
  221. }
  222. })->name('api.ocr.callback');
  223. // 获取题目生成回调结果
  224. Route::get('/questions/callback/{taskId}', function (string $taskId) {
  225. // ✅ 优先从缓存读取(跨域友好)
  226. $callbackData = cache($taskId);
  227. if ($callbackData) {
  228. // 清除已读取的回调数据
  229. cache()->forget($taskId);
  230. session()->forget('question_gen_callback_' . $taskId);
  231. return response()->json($callbackData);
  232. }
  233. // 备选:从session读取
  234. $sessionData = session('question_gen_callback_' . $taskId);
  235. if ($sessionData) {
  236. // 清除已读取的回调数据
  237. session()->forget('question_gen_callback_' . $taskId);
  238. return response()->json($sessionData);
  239. }
  240. // 未收到回调
  241. return response()->json(['status' => 'pending'], 202);
  242. })->name('api.questions.callback.get');
  243. // 获取OCR题目生成回调结果
  244. Route::get('/ocr-question-callback/{ocrRecordId}/{taskId}', function (int $ocrRecordId, string $taskId) {
  245. $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
  246. $callbackData = cache($cacheKey);
  247. if ($callbackData) {
  248. // 清除已读取的回调数据
  249. cache()->forget($cacheKey);
  250. return response()->json([
  251. 'success' => true,
  252. 'data' => $callbackData
  253. ]);
  254. }
  255. return response()->json([
  256. 'success' => false,
  257. 'status' => 'pending',
  258. 'message' => 'OCR callback not received yet'
  259. ], 202);
  260. })->name('api.ocr.callback.get');
  261. // 题目相关 API
  262. Route::get('/questions', function (QuestionServiceApi $service) {
  263. try {
  264. $page = (int) request()->get('page', 1);
  265. $perPage = (int) request()->get('per_page', 25);
  266. $filters = [
  267. 'kp_code' => request()->get('kp_code'),
  268. 'difficulty' => request()->get('difficulty'),
  269. 'search' => request()->get('search'),
  270. ];
  271. $response = $service->listQuestions($page, $perPage, $filters);
  272. return response()->json($response);
  273. } catch (\Exception $e) {
  274. \Log::error('Failed to fetch questions: ' . $e->getMessage());
  275. return response()->json([
  276. 'data' => [],
  277. 'meta' => [
  278. 'page' => 1,
  279. 'per_page' => 25,
  280. 'total' => 0,
  281. 'total_pages' => 0,
  282. ],
  283. 'error' => $e->getMessage(),
  284. ], 500);
  285. }
  286. });
  287. // 获取题目统计信息
  288. Route::get('/questions/statistics', function (QuestionServiceApi $service) {
  289. try {
  290. $stats = $service->getStatistics();
  291. return response()->json($stats);
  292. } catch (\Exception $e) {
  293. \Log::error('Failed to get question statistics: ' . $e->getMessage());
  294. return response()->json(['error' => $e->getMessage()], 500);
  295. }
  296. });
  297. // 语义搜索题目
  298. Route::post('/questions/search', function (QuestionServiceApi $service) {
  299. try {
  300. $data = request()->only(['query', 'limit']);
  301. $results = $service->searchQuestions($data['query'], $data['limit'] ?? 20);
  302. return response()->json($results);
  303. } catch (\Exception $e) {
  304. \Log::error('Question search failed: ' . $e->getMessage());
  305. return response()->json(['error' => $e->getMessage()], 500);
  306. }
  307. });
  308. // 获取单个题目详情
  309. Route::get('/questions/{id}', function (int $id, QuestionServiceApi $service) {
  310. try {
  311. $question = $service->getQuestionById($id);
  312. if (!$question) {
  313. return response()->json(['error' => 'Question not found'], 404);
  314. }
  315. return response()->json($question);
  316. } catch (\Exception $e) {
  317. \Log::error("Failed to get question {$id}: " . $e->getMessage());
  318. return response()->json(['error' => $e->getMessage()], 500);
  319. }
  320. });
  321. // AI 生成题目
  322. Route::post('/questions/generate', function (QuestionServiceApi $service) {
  323. try {
  324. $data = request()->only(['kp_code', 'keyword', 'count', 'strategy']);
  325. $result = $service->generateQuestions($data);
  326. return response()->json($result);
  327. } catch (\Exception $e) {
  328. \Log::error('Question generation failed: ' . $e->getMessage());
  329. return response()->json([
  330. 'success' => false,
  331. 'message' => $e->getMessage(),
  332. ], 500);
  333. }
  334. });
  335. // 删除题目
  336. Route::delete('/questions/{id}', function (int $id, QuestionServiceApi $service) {
  337. try {
  338. $deleted = $service->deleteQuestion($id);
  339. return response()->json([
  340. 'success' => $deleted,
  341. 'message' => $deleted ? 'Question deleted' : 'Failed to delete',
  342. ]);
  343. } catch (\Exception $e) {
  344. \Log::error("Failed to delete question {$id}: " . $e->getMessage());
  345. return response()->json([
  346. 'success' => false,
  347. 'message' => $e->getMessage(),
  348. ], 500);
  349. }
  350. });
  351. // 获取知识点选项
  352. Route::get('/knowledge-points', function (QuestionServiceApi $service) {
  353. try {
  354. $points = $service->getKnowledgePointOptions();
  355. return response()->json($points);
  356. } catch (\Exception $e) {
  357. \Log::error('Failed to get knowledge points: ' . $e->getMessage());
  358. return response()->json([], 500);
  359. }
  360. });
  361. // 智能出卷对外接口:生成试卷并返回PDF/判卷地址
  362. Route::post('/intelligent-exams', [IntelligentExamController::class, 'store'])
  363. ->withoutMiddleware([
  364. Authenticate::class,
  365. 'auth',
  366. 'auth:sanctum',
  367. 'auth:api',
  368. ])
  369. ->name('api.intelligent-exams.store');
  370. // 学情报告对外接口:生成并返回学情报告 PDF
  371. Route::post('/exam-analysis/report', [ExamAnalysisApiController::class, 'store'])
  372. ->withoutMiddleware([
  373. Authenticate::class,
  374. 'auth',
  375. 'auth:sanctum',
  376. 'auth:api',
  377. ])
  378. ->name('api.exam-analysis.report');
  379. /*
  380. |--------------------------------------------------------------------------
  381. | 教材管理 API 路由
  382. |--------------------------------------------------------------------------
  383. */
  384. // 获取教材列表(按年级排序)
  385. Route::get('/textbooks', [TextbookApiController::class, 'index'])
  386. ->name('api.textbooks.index');
  387. // 根据年级获取教材
  388. Route::get('/textbooks/grade/{grade}', [TextbookApiController::class, 'getByGrade'])
  389. ->name('api.textbooks.by-grade');
  390. // 获取教材系列列表(必须在 {id} 路由之前定义)
  391. Route::get('/textbooks/series', [TextbookApiController::class, 'getSeries'])
  392. ->name('api.textbooks.series');
  393. // 获取单个教材详情
  394. Route::get('/textbooks/{id}', [TextbookApiController::class, 'show'])
  395. ->name('api.textbooks.show');
  396. // 获取教材目录
  397. Route::get('/textbooks/{id}/catalog', [TextbookApiController::class, 'getCatalog'])
  398. ->name('api.textbooks.catalog');
  399. /*
  400. |--------------------------------------------------------------------------
  401. | MathRecSys 集成 API 路由
  402. |--------------------------------------------------------------------------
  403. */
  404. use App\Http\Controllers\Api\StudentController;
  405. // 健康检查
  406. Route::get('/mathrecsys/health', [StudentController::class, 'checkServiceHealth'])->name('api.mathrecsys.health');
  407. // 学生相关 API
  408. Route::prefix('mathrecsys/students')->name('api.mathrecsys.students.')->group(function () {
  409. // 获取学生完整信息
  410. Route::get('{studentId}', [StudentController::class, 'show'])->name('show');
  411. // 获取个性化推荐
  412. Route::get('{studentId}/recommendations', [StudentController::class, 'getRecommendations'])->name('recommendations');
  413. // 获取学习轨迹
  414. Route::get('{studentId}/trajectory', [StudentController::class, 'getTrajectory'])->name('trajectory');
  415. // 获取学习建议
  416. Route::get('{studentId}/suggestions', [StudentController::class, 'getSuggestions'])->name('suggestions');
  417. // 智能分析题目
  418. Route::post('{studentId}/analyze', [StudentController::class, 'analyzeQuestion'])->name('analyze');
  419. // 更新掌握度
  420. Route::put('{studentId}/mastery', [StudentController::class, 'updateMastery'])->name('update-mastery');
  421. });
  422. // 班级分析 API
  423. Route::prefix('mathrecsys/classes')->name('api.mathrecsys.classes.')->group(function () {
  424. Route::get('{classId}/analysis', [StudentController::class, 'classAnalysis'])->name('analysis');
  425. });
  426. // 测试 API
  427. Route::get('/mathrecsys/test', function () {
  428. return response()->json([
  429. 'success' => true,
  430. 'message' => 'MathRecSys API integration is working',
  431. 'timestamp' => now()->toISOString()
  432. ]);
  433. })->name('api.mathrecsys.test');
  434. // 测试OCR题目生成API调用
  435. Route::post('/test-ocr-generation', function () {
  436. try {
  437. $service = new \App\Services\QuestionBankService();
  438. // 模拟前端传递的OCR题目数据
  439. $questions = [
  440. [
  441. 'id' => 1,
  442. 'content' => '计算:2+3-4'
  443. ],
  444. [
  445. 'id' => 2,
  446. 'content' => '解方程:x+5=10'
  447. ]
  448. ];
  449. Log::info('开始测试OCR题目生成', [
  450. 'questions_count' => count($questions),
  451. 'ocr_record_id' => 12
  452. ]);
  453. // 使用异步API,系统自动生成回调URL
  454. $response = $service->generateQuestionsFromOcrAsync(
  455. $questions,
  456. '高一',
  457. '数学',
  458. 12, // OCR记录ID
  459. null, // 让系统自动生成回调URL
  460. 'api.ocr.callback' // 回调路由名称
  461. );
  462. Log::info('OCR题目生成响应', [
  463. 'response' => $response,
  464. 'status' => $response['status'] ?? 'unknown',
  465. 'task_id' => $response['task_id'] ?? 'N/A'
  466. ]);
  467. return response()->json([
  468. 'success' => true,
  469. 'message' => 'OCR题目生成测试完成',
  470. 'data' => $response
  471. ]);
  472. } catch (\Exception $e) {
  473. Log::error('测试OCR题目生成失败', [
  474. 'error' => $e->getMessage(),
  475. 'trace' => $e->getTraceAsString()
  476. ]);
  477. return response()->json([
  478. 'success' => false,
  479. 'error' => $e->getMessage()
  480. ], 500);
  481. }
  482. })->name('api.test.ocr.generation');