api.php 17 KB

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