api.php 18 KB

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