api.php 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586
  1. <?php
  2. use App\Http\Controllers\Api\AbilityEvaluateController;
  3. use App\Http\Controllers\Api\ExamAnalysisApiController;
  4. use App\Http\Controllers\Api\IntelligentExamController;
  5. use App\Http\Controllers\Api\KnowledgeRecommendController;
  6. use App\Http\Controllers\Api\MistakeBookController;
  7. use App\Http\Controllers\Api\PaperAssembleController;
  8. use App\Http\Controllers\Api\PaperJsonController;
  9. use App\Http\Controllers\Api\PreQuestionApiController;
  10. use App\Http\Controllers\Api\QuestionRandomController;
  11. use App\Http\Controllers\Api\QuestionSearchController;
  12. use App\Http\Controllers\Api\QuestionSolutionController;
  13. use App\Http\Controllers\Api\StudentAnswerAnalysisController;
  14. use App\Http\Controllers\Api\StudentKnowledgeController;
  15. use App\Http\Controllers\Api\TextbookApiController;
  16. use App\Services\QuestionServiceApi;
  17. use Illuminate\Auth\Middleware\Authenticate;
  18. use Illuminate\Support\Facades\Log;
  19. use Illuminate\Support\Facades\Route;
  20. /*
  21. |--------------------------------------------------------------------------
  22. | 题库管理 API 路由
  23. |--------------------------------------------------------------------------
  24. */
  25. // 给 Python/内部服务消费的“筛选题库”API(需要 X-Internal-Token)
  26. Route::middleware('internal.token')->get('/pre-questions', [PreQuestionApiController::class, 'index'])
  27. ->name('api.pre-questions.index');
  28. Route::get('/questions/search', QuestionSearchController::class)->name('api.questions.search');
  29. Route::get('/questions/random', QuestionRandomController::class)->name('api.questions.random');
  30. Route::post('/papers/assemble', PaperAssembleController::class)->name('api.papers.assemble');
  31. Route::get('/papers/{paperId}/json', [PaperJsonController::class, 'show'])->name('api.papers.json');
  32. Route::get('/questions/{id}/solution', QuestionSolutionController::class)->name('api.questions.solution');
  33. Route::get('/knowledge/recommend', KnowledgeRecommendController::class)->name('api.knowledge.recommend');
  34. Route::post('/abilities/evaluate', AbilityEvaluateController::class)->name('api.abilities.evaluate');
  35. // 接收题目生成回调
  36. Route::post('/questions/callback', function () {
  37. try {
  38. $data = request()->all();
  39. Log::info('Received question generation callback', $data);
  40. // 验证回调数据
  41. if (! isset($data['task_id']) || ! isset($data['status'])) {
  42. return response()->json(['error' => 'Invalid callback data'], 400);
  43. }
  44. // 处理回调数据并存储通知到session
  45. if ($data['status'] === 'completed') {
  46. $result = $data['result'] ?? [];
  47. $total = $result['total'] ?? $data['total'] ?? ($result['saved'] ?? 0);
  48. $kpCode = $result['kp_code'] ?? $data['kp_code'] ?? '';
  49. // 将成功通知存储到session,供下次页面刷新时显示
  50. session()->flash('notification', [
  51. 'type' => 'success',
  52. 'title' => '✅ 题目生成完成',
  53. 'body' => "任务 ID: {$data['task_id']}\n生成题目: {$total} 道".($kpCode ? "\n知识点: {$kpCode}" : ''),
  54. 'color' => 'success',
  55. ]);
  56. Log::info('题目生成成功通知已存储', [
  57. 'task_id' => $data['task_id'],
  58. 'total' => $total,
  59. 'kp_code' => $kpCode,
  60. ]);
  61. } elseif ($data['status'] === 'failed') {
  62. $error = $data['error'] ?? '未知错误';
  63. // 将失败通知存储到session
  64. session()->flash('notification', [
  65. 'type' => 'error',
  66. 'title' => '❌ 题目生成失败',
  67. 'body' => "任务 ID: {$data['task_id']}\n错误: {$error}",
  68. 'color' => 'danger',
  69. ]);
  70. Log::error('题目生成失败通知已存储', [
  71. 'task_id' => $data['task_id'],
  72. 'error' => $error,
  73. ]);
  74. }
  75. return response()->json([
  76. 'success' => true,
  77. 'message' => 'Callback received and notification stored',
  78. 'status' => $data['status'],
  79. ]);
  80. } catch (\Exception $e) {
  81. Log::error('Callback processing failed: '.$e->getMessage());
  82. return response()->json(['error' => $e->getMessage()], 500);
  83. }
  84. })->name('api.questions.callback');
  85. // 接收OCR题目生成回调
  86. Route::post('/ocr-question-callback', function () {
  87. try {
  88. $data = request()->all();
  89. Log::info('Received OCR question generation callback', $data);
  90. // 验证必要的回调数据
  91. if (! isset($data['task_id']) || ! isset($data['status']) || ! isset($data['ocr_record_id'])) {
  92. Log::error('OCR callback missing required fields', $data);
  93. return response()->json([
  94. 'success' => false,
  95. 'error' => 'Missing required fields: task_id, status, ocr_record_id',
  96. ], 400);
  97. }
  98. $taskId = $data['task_id'];
  99. $ocrRecordId = $data['ocr_record_id'];
  100. $status = $data['status'];
  101. // 将回调结果存储到缓存中,供前端查询(保留30秒)
  102. $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
  103. cache([$cacheKey => $data], now()->addSeconds(30));
  104. Log::info("OCR callback cached with key: {$cacheKey}", [
  105. 'ocr_record_id' => $ocrRecordId,
  106. 'task_id' => $taskId,
  107. 'status' => $status,
  108. 'total_generated' => $data['result']['total_generated'] ?? 0,
  109. 'total_saved' => $data['result']['total_saved'] ?? 0,
  110. ]);
  111. // 处理题目关联逻辑
  112. if ($status === 'completed') {
  113. $updatedCount = 0;
  114. // 从result中提取question_mappings(QuestionBank API将它放在result字段中)
  115. $mappings = $data['result']['question_mappings'] ?? $data['question_mappings'] ?? [];
  116. Log::info('Processing OCR question associations', [
  117. 'ocr_record_id' => $ocrRecordId,
  118. 'task_id' => $taskId,
  119. 'mappings_count' => count($mappings),
  120. ]);
  121. // 更新ocr_question_results表中的关联关系
  122. foreach ($mappings as $mapping) {
  123. try {
  124. $ocrQuestionNumber = $mapping['ocr_question_number'] ?? null;
  125. $questionBankId = $mapping['question_bank_id'] ?? null;
  126. $questionCode = $mapping['question_code'] ?? null;
  127. if ($ocrQuestionNumber && $questionBankId) {
  128. // 查找对应的OCR题目结果并更新
  129. $updated = DB::table('ocr_question_results')
  130. ->where('ocr_record_id', $ocrRecordId)
  131. ->where('question_number', $ocrQuestionNumber)
  132. ->update([
  133. 'question_bank_id' => $questionBankId,
  134. 'generation_status' => 'completed',
  135. 'generation_task_id' => $taskId,
  136. 'generation_error' => null,
  137. ]);
  138. if ($updated) {
  139. $updatedCount++;
  140. Log::info('Updated OCR question association', [
  141. 'ocr_record_id' => $ocrRecordId,
  142. 'question_number' => $ocrQuestionNumber,
  143. 'question_bank_id' => $questionBankId,
  144. 'question_code' => $questionCode,
  145. ]);
  146. } else {
  147. Log::warning('No OCR question result found for association', [
  148. 'ocr_record_id' => $ocrRecordId,
  149. 'question_number' => $ocrQuestionNumber,
  150. ]);
  151. }
  152. }
  153. } catch (\Exception $e) {
  154. Log::error('Failed to update OCR question association', [
  155. 'mapping' => $mapping,
  156. 'error' => $e->getMessage(),
  157. ]);
  158. }
  159. }
  160. Log::info('OCR question association completed', [
  161. 'ocr_record_id' => $ocrRecordId,
  162. 'task_id' => $taskId,
  163. 'total_mappings' => count($mappings),
  164. 'updated_count' => $updatedCount,
  165. ]);
  166. // 更新OCR记录的整体状态为已完成
  167. try {
  168. DB::table('ocr_records')
  169. ->where('id', $ocrRecordId)
  170. ->update([
  171. 'status' => 'completed',
  172. 'processed_at' => now(),
  173. 'updated_at' => now(),
  174. ]);
  175. Log::info('Updated OCR record status to completed', [
  176. 'ocr_record_id' => $ocrRecordId,
  177. 'task_id' => $taskId,
  178. ]);
  179. } catch (\Exception $e) {
  180. Log::error('Failed to update OCR record status', [
  181. 'ocr_record_id' => $ocrRecordId,
  182. 'error' => $e->getMessage(),
  183. ]);
  184. }
  185. } elseif ($status === 'failed') {
  186. // 更新所有相关的OCR题目结果为失败状态
  187. try {
  188. $updated = DB::table('ocr_question_results')
  189. ->where('ocr_record_id', $ocrRecordId)
  190. ->where('generation_status', 'pending') // 只更新待处理的
  191. ->update([
  192. 'generation_status' => 'failed',
  193. 'generation_task_id' => $taskId,
  194. 'generation_error' => $data['error'] ?? 'Unknown error',
  195. ]);
  196. Log::info('Updated OCR questions to failed status', [
  197. 'ocr_record_id' => $ocrRecordId,
  198. 'task_id' => $taskId,
  199. 'updated_count' => $updated,
  200. 'error' => $data['error'] ?? 'Unknown error',
  201. ]);
  202. // 更新OCR记录的状态为失败
  203. DB::table('ocr_records')
  204. ->where('id', $ocrRecordId)
  205. ->update([
  206. 'status' => 'failed',
  207. 'error_message' => $data['error'] ?? 'Question generation failed',
  208. 'updated_at' => now(),
  209. ]);
  210. Log::info('Updated OCR record status to failed', [
  211. 'ocr_record_id' => $ocrRecordId,
  212. 'task_id' => $taskId,
  213. 'error' => $data['error'] ?? 'Unknown error',
  214. ]);
  215. } catch (\Exception $e) {
  216. Log::error('Failed to update OCR questions to failed status', [
  217. 'ocr_record_id' => $ocrRecordId,
  218. 'error' => $e->getMessage(),
  219. ]);
  220. }
  221. }
  222. return response()->json([
  223. 'success' => true,
  224. 'message' => 'OCR callback received and processed',
  225. 'data' => [
  226. 'task_id' => $taskId,
  227. 'ocr_record_id' => $ocrRecordId,
  228. 'status' => $status,
  229. 'cache_key' => $cacheKey,
  230. 'associations_processed' => $status === 'completed' ? count($data['question_mappings'] ?? []) : 0,
  231. ],
  232. ]);
  233. } catch (\Exception $e) {
  234. Log::error('OCR callback processing failed: '.$e->getMessage());
  235. Log::error('Exception details: '.$e->getTraceAsString());
  236. return response()->json([
  237. 'success' => false,
  238. 'error' => 'Callback processing failed: '.$e->getMessage(),
  239. ], 500);
  240. }
  241. })->name('api.ocr.callback');
  242. // 获取题目生成回调结果
  243. Route::get('/questions/callback/{taskId}', function (string $taskId) {
  244. // ✅ 优先从缓存读取(跨域友好)
  245. $callbackData = cache($taskId);
  246. if ($callbackData) {
  247. // 清除已读取的回调数据
  248. cache()->forget($taskId);
  249. session()->forget('question_gen_callback_'.$taskId);
  250. return response()->json($callbackData);
  251. }
  252. // 备选:从session读取
  253. $sessionData = session('question_gen_callback_'.$taskId);
  254. if ($sessionData) {
  255. // 清除已读取的回调数据
  256. session()->forget('question_gen_callback_'.$taskId);
  257. return response()->json($sessionData);
  258. }
  259. // 未收到回调
  260. return response()->json(['status' => 'pending'], 202);
  261. })->name('api.questions.callback.get');
  262. // 获取OCR题目生成回调结果
  263. Route::get('/ocr-question-callback/{ocrRecordId}/{taskId}', function (int $ocrRecordId, string $taskId) {
  264. $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
  265. $callbackData = cache($cacheKey);
  266. if ($callbackData) {
  267. // 清除已读取的回调数据
  268. cache()->forget($cacheKey);
  269. return response()->json([
  270. 'success' => true,
  271. 'data' => $callbackData,
  272. ]);
  273. }
  274. return response()->json([
  275. 'success' => false,
  276. 'status' => 'pending',
  277. 'message' => 'OCR callback not received yet',
  278. ], 202);
  279. })->name('api.ocr.callback.get');
  280. // 题目相关 API
  281. Route::get('/questions', function (QuestionServiceApi $service) {
  282. try {
  283. $page = (int) request()->get('page', 1);
  284. $perPage = (int) request()->get('per_page', 25);
  285. $filters = [
  286. 'kp_code' => request()->get('kp_code'),
  287. 'difficulty' => request()->get('difficulty'),
  288. 'search' => request()->get('search'),
  289. ];
  290. $response = $service->listQuestions($page, $perPage, $filters);
  291. return response()->json($response);
  292. } catch (\Exception $e) {
  293. \Log::error('Failed to fetch questions: '.$e->getMessage());
  294. return response()->json([
  295. 'data' => [],
  296. 'meta' => [
  297. 'page' => 1,
  298. 'per_page' => 25,
  299. 'total' => 0,
  300. 'total_pages' => 0,
  301. ],
  302. 'error' => $e->getMessage(),
  303. ], 500);
  304. }
  305. });
  306. // 获取题目统计信息
  307. Route::get('/questions/statistics', function (QuestionServiceApi $service) {
  308. try {
  309. $stats = $service->getStatistics();
  310. return response()->json($stats);
  311. } catch (\Exception $e) {
  312. \Log::error('Failed to get question statistics: '.$e->getMessage());
  313. return response()->json(['error' => $e->getMessage()], 500);
  314. }
  315. });
  316. // 语义搜索题目
  317. Route::post('/questions/search', function (QuestionServiceApi $service) {
  318. try {
  319. $data = request()->only(['query', 'limit']);
  320. $results = $service->searchQuestions($data['query'], $data['limit'] ?? 20);
  321. return response()->json($results);
  322. } catch (\Exception $e) {
  323. \Log::error('Question search failed: '.$e->getMessage());
  324. return response()->json(['error' => $e->getMessage()], 500);
  325. }
  326. });
  327. // 获取单个题目详情
  328. Route::get('/questions/{id}', function (int $id, QuestionServiceApi $service) {
  329. try {
  330. $question = $service->getQuestionById($id);
  331. if (! $question) {
  332. return response()->json(['error' => 'Question not found'], 404);
  333. }
  334. return response()->json($question);
  335. } catch (\Exception $e) {
  336. \Log::error("Failed to get question {$id}: ".$e->getMessage());
  337. return response()->json(['error' => $e->getMessage()], 500);
  338. }
  339. });
  340. // AI 生成题目
  341. Route::post('/questions/generate', function (QuestionServiceApi $service) {
  342. try {
  343. $data = request()->only(['kp_code', 'keyword', 'count', 'strategy']);
  344. $result = $service->generateQuestions($data);
  345. return response()->json($result);
  346. } catch (\Exception $e) {
  347. \Log::error('Question generation failed: '.$e->getMessage());
  348. return response()->json([
  349. 'success' => false,
  350. 'message' => $e->getMessage(),
  351. ], 500);
  352. }
  353. });
  354. // 删除题目
  355. Route::delete('/questions/{id}', function (int $id, QuestionServiceApi $service) {
  356. try {
  357. $deleted = $service->deleteQuestion($id);
  358. return response()->json([
  359. 'success' => $deleted,
  360. 'message' => $deleted ? 'Question deleted' : 'Failed to delete',
  361. ]);
  362. } catch (\Exception $e) {
  363. \Log::error("Failed to delete question {$id}: ".$e->getMessage());
  364. return response()->json([
  365. 'success' => false,
  366. 'message' => $e->getMessage(),
  367. ], 500);
  368. }
  369. });
  370. use App\Http\Controllers\Api\KnowledgePointTreeController;
  371. // 获取知识点树形结构(从 MySQL 数据库)
  372. Route::get('/knowledge-points', [KnowledgePointTreeController::class, 'index'])
  373. ->name('api.knowledge-points.index');
  374. // 智能出卷对外接口:生成试卷并返回PDF/判卷地址
  375. Route::post('/intelligent-exams', [IntelligentExamController::class, 'store'])
  376. ->withoutMiddleware([
  377. Authenticate::class,
  378. 'auth',
  379. 'auth:sanctum',
  380. 'auth:api',
  381. ])
  382. ->name('api.intelligent-exams.store');
  383. // 智能出卷任务状态查询
  384. Route::get('/intelligent-exams/status/{taskId}', [IntelligentExamController::class, 'status'])
  385. ->withoutMiddleware([
  386. Authenticate::class,
  387. 'auth',
  388. 'auth:sanctum',
  389. 'auth:api',
  390. ])
  391. ->name('api.intelligent-exams.status');
  392. // 学情报告对外接口:生成并返回学情报告 PDF
  393. Route::post('/exam-analysis/report', [ExamAnalysisApiController::class, 'store'])
  394. ->withoutMiddleware([
  395. Authenticate::class,
  396. 'auth',
  397. 'auth:sanctum',
  398. 'auth:api',
  399. ])
  400. ->name('api.exam-analysis.report');
  401. // 学情报告任务状态查询
  402. Route::get('/exam-analysis/status/{taskId}', [ExamAnalysisApiController::class, 'status'])
  403. ->withoutMiddleware([
  404. Authenticate::class,
  405. 'auth',
  406. 'auth:sanctum',
  407. 'auth:api',
  408. ])
  409. ->name('api.exam-analysis.status');
  410. // 获取PDF报告URL(查询指定试卷的报告状态)
  411. Route::get('/exam-analysis/pdf/{paper_id}', [ExamAnalysisApiController::class, 'getPdfUrl'])
  412. ->withoutMiddleware([
  413. Authenticate::class,
  414. 'auth',
  415. 'auth:sanctum',
  416. 'auth:api',
  417. ])
  418. ->name('api.exam-analysis.pdf');
  419. /*
  420. |--------------------------------------------------------------------------
  421. | 错题本 API 路由
  422. |--------------------------------------------------------------------------
  423. */
  424. // 获取错题列表
  425. Route::get('/mistake-book', [MistakeBookController::class, 'listMistakes'])
  426. ->withoutMiddleware([
  427. Authenticate::class,
  428. 'auth',
  429. 'auth:sanctum',
  430. 'auth:api',
  431. ])
  432. ->name('api.mistake-book.list');
  433. // 新增错题
  434. Route::post('/mistake-book', [MistakeBookController::class, 'addMistake'])
  435. ->withoutMiddleware([
  436. Authenticate::class,
  437. 'auth',
  438. 'auth:sanctum',
  439. 'auth:api',
  440. ])
  441. ->name('api.mistake-book.create');
  442. // 获取单条错题详情
  443. Route::get('/mistake-book/{mistakeId}', [MistakeBookController::class, 'getMistakeDetail'])
  444. ->withoutMiddleware([
  445. Authenticate::class,
  446. 'auth',
  447. 'auth:sanctum',
  448. 'auth:api',
  449. ])
  450. ->whereNumber('mistakeId')
  451. ->name('api.mistake-book.detail');
  452. // 获取错题统计概要
  453. Route::get('/mistake-book/summary', [MistakeBookController::class, 'getSummary'])
  454. ->withoutMiddleware([
  455. Authenticate::class,
  456. 'auth',
  457. 'auth:sanctum',
  458. 'auth:api',
  459. ])
  460. ->name('api.mistake-book.summary');
  461. // 获取错误模式分析
  462. Route::get('/mistake-book/analytics/mistake-pattern', [MistakeBookController::class, 'getMistakePatterns'])
  463. ->withoutMiddleware([
  464. Authenticate::class,
  465. 'auth',
  466. 'auth:sanctum',
  467. 'auth:api',
  468. ])
  469. ->name('api.mistake-book.patterns');
  470. // 收藏/取消收藏错题
  471. Route::post('/mistake-book/{mistakeId}/favorite', [MistakeBookController::class, 'toggleFavorite'])
  472. ->withoutMiddleware([
  473. Authenticate::class,
  474. 'auth',
  475. 'auth:sanctum',
  476. 'auth:api',
  477. ])
  478. ->name('api.mistake-book.favorite');
  479. // 标记错题已复习
  480. Route::post('/mistake-book/{mistakeId}/review', [MistakeBookController::class, 'markReviewed'])
  481. ->withoutMiddleware([
  482. Authenticate::class,
  483. 'auth',
  484. 'auth:sanctum',
  485. 'auth:api',
  486. ])
  487. ->name('api.mistake-book.review');
  488. // 加入重练清单
  489. Route::post('/mistake-book/{mistakeId}/retry-list', [MistakeBookController::class, 'addToRetryList'])
  490. ->withoutMiddleware([
  491. Authenticate::class,
  492. 'auth',
  493. 'auth:sanctum',
  494. 'auth:api',
  495. ])
  496. ->name('api.mistake-book.retry-list');
  497. // 推荐练习题
  498. Route::post('/mistake-book/recommend-practice', [MistakeBookController::class, 'recommendPractice'])
  499. ->withoutMiddleware([
  500. Authenticate::class,
  501. 'auth',
  502. 'auth:sanctum',
  503. 'auth:api',
  504. ])
  505. ->name('api.mistake-book.recommend-practice');
  506. // 获取错题本快照数据(仪表板用)
  507. Route::get('/mistake-book/snapshot', [MistakeBookController::class, 'getSnapshot'])
  508. ->withoutMiddleware([
  509. Authenticate::class,
  510. 'auth',
  511. 'auth:sanctum',
  512. 'auth:api',
  513. ])
  514. ->name('api.mistake-book.snapshot');
  515. /*
  516. |--------------------------------------------------------------------------
  517. | 错题复习状态管理 API 路由
  518. |--------------------------------------------------------------------------
  519. */
  520. Route::post('/mistake-book/{mistakeId}/review-status', [MistakeBookController::class, 'updateReviewStatus'])
  521. ->withoutMiddleware([
  522. Authenticate::class,
  523. 'auth',
  524. 'auth:sanctum',
  525. 'auth:api',
  526. ])
  527. ->name('api.mistake-book.review-status.update');
  528. Route::get('/mistake-book/{mistakeId}/review-status', [MistakeBookController::class, 'getReviewStatus'])
  529. ->withoutMiddleware([
  530. Authenticate::class,
  531. 'auth',
  532. 'auth:sanctum',
  533. 'auth:api',
  534. ])
  535. ->name('api.mistake-book.review-status.get');
  536. Route::post('/mistake-book/{mistakeId}/increment-review', [MistakeBookController::class, 'incrementReview'])
  537. ->withoutMiddleware([
  538. Authenticate::class,
  539. 'auth',
  540. 'auth:sanctum',
  541. 'auth:api',
  542. ])
  543. ->name('api.mistake-book.increment-review');
  544. Route::post('/mistake-book/{mistakeId}/reset-review', [MistakeBookController::class, 'resetReview'])
  545. ->withoutMiddleware([
  546. Authenticate::class,
  547. 'auth',
  548. 'auth:sanctum',
  549. 'auth:api',
  550. ])
  551. ->name('api.mistake-book.reset-review');
  552. /*
  553. |--------------------------------------------------------------------------
  554. | 错题批量操作 API 路由
  555. |--------------------------------------------------------------------------
  556. */
  557. Route::post('/mistake-book/batch-operation', [MistakeBookController::class, 'batchOperation'])
  558. ->withoutMiddleware([
  559. Authenticate::class,
  560. 'auth',
  561. 'auth:sanctum',
  562. 'auth:api',
  563. ])
  564. ->name('api.mistake-book.batch-operation');
  565. Route::post('/mistake-book/batch/mark-reviewed', [MistakeBookController::class, 'batchMarkReviewed'])
  566. ->withoutMiddleware([
  567. Authenticate::class,
  568. 'auth',
  569. 'auth:sanctum',
  570. 'auth:api',
  571. ])
  572. ->name('api.mistake-book.batch.mark-reviewed');
  573. Route::post('/mistake-book/batch/mark-mastered', [MistakeBookController::class, 'batchMarkMastered'])
  574. ->withoutMiddleware([
  575. Authenticate::class,
  576. 'auth',
  577. 'auth:sanctum',
  578. 'auth:api',
  579. ])
  580. ->name('api.mistake-book.batch.mark-mastered');
  581. Route::post('/mistake-book/batch/add-to-retry-list', [MistakeBookController::class, 'batchAddToRetryList'])
  582. ->withoutMiddleware([
  583. Authenticate::class,
  584. 'auth',
  585. 'auth:sanctum',
  586. 'auth:api',
  587. ])
  588. ->name('api.mistake-book.batch.add-to-retry-list');
  589. Route::post('/mistake-book/batch/remove-from-retry-list', [MistakeBookController::class, 'batchRemoveFromRetryList'])
  590. ->withoutMiddleware([
  591. Authenticate::class,
  592. 'auth',
  593. 'auth:sanctum',
  594. 'auth:api',
  595. ])
  596. ->name('api.mistake-book.batch.remove-from-retry-list');
  597. Route::post('/mistake-book/batch/set-error-type', [MistakeBookController::class, 'batchSetErrorType'])
  598. ->withoutMiddleware([
  599. Authenticate::class,
  600. 'auth',
  601. 'auth:sanctum',
  602. 'auth:api',
  603. ])
  604. ->name('api.mistake-book.batch.set-error-type');
  605. Route::post('/mistake-book/batch/set-importance', [MistakeBookController::class, 'batchSetImportance'])
  606. ->withoutMiddleware([
  607. Authenticate::class,
  608. 'auth',
  609. 'auth:sanctum',
  610. 'auth:api',
  611. ])
  612. ->name('api.mistake-book.batch.set-importance');
  613. Route::post('/mistake-book/batch/toggle-favorite', [MistakeBookController::class, 'batchToggleFavorite'])
  614. ->withoutMiddleware([
  615. Authenticate::class,
  616. 'auth',
  617. 'auth:sanctum',
  618. 'auth:api',
  619. ])
  620. ->name('api.mistake-book.batch.toggle-favorite');
  621. /*
  622. |--------------------------------------------------------------------------
  623. | 知识点掌握情况 API 路由
  624. |--------------------------------------------------------------------------
  625. */
  626. use App\Http\Controllers\Api\KnowledgeMasteryController;
  627. // 获取学生知识点掌握情况统计
  628. Route::get('/knowledge-mastery/stats/{studentId}', [KnowledgeMasteryController::class, 'stats'])
  629. ->where('studentId', '[0-9]+') // 限制为数字
  630. ->withoutMiddleware([
  631. Authenticate::class,
  632. 'auth',
  633. 'auth:sanctum',
  634. 'auth:api',
  635. ])
  636. ->name('api.knowledge-mastery.stats');
  637. // 获取学生知识点掌握摘要
  638. Route::get('/knowledge-mastery/summary/{studentId}', [KnowledgeMasteryController::class, 'summary'])
  639. ->where('studentId', '[0-9]+') // 限制为数字
  640. ->withoutMiddleware([
  641. Authenticate::class,
  642. 'auth',
  643. 'auth:sanctum',
  644. 'auth:api',
  645. ])
  646. ->name('api.knowledge-mastery.summary');
  647. // 获取学生知识点图谱数据
  648. Route::get('/knowledge-mastery/graph/{studentId}', [KnowledgeMasteryController::class, 'graph'])
  649. ->where('studentId', '[0-9]+') // 限制为数字
  650. ->withoutMiddleware([
  651. Authenticate::class,
  652. 'auth',
  653. 'auth:sanctum',
  654. 'auth:api',
  655. ])
  656. ->name('api.knowledge-mastery.graph');
  657. // 获取学生知识点图谱快照列表
  658. Route::get('/knowledge-mastery/graph/snapshots/{studentId}', [KnowledgeMasteryController::class, 'graphSnapshots'])
  659. ->where('studentId', '[0-9]+') // 限制为数字
  660. ->withoutMiddleware([
  661. Authenticate::class,
  662. 'auth',
  663. 'auth:sanctum',
  664. 'auth:api',
  665. ])
  666. ->name('api.knowledge-mastery.graph.snapshots');
  667. // 获取学生知识点快照列表(简化路径)
  668. Route::get('/knowledge-mastery/snapshots/{studentId}', [KnowledgeMasteryController::class, 'snapshots'])
  669. ->where('studentId', '[0-9]+') // 限制为数字
  670. ->withoutMiddleware([
  671. Authenticate::class,
  672. 'auth',
  673. 'auth:sanctum',
  674. 'auth:api',
  675. ])
  676. ->name('api.knowledge-mastery.snapshots');
  677. // 创建知识点掌握度快照
  678. Route::post('/knowledge-mastery/snapshot/{studentId}', [KnowledgeMasteryController::class, 'createSnapshot'])
  679. ->where('studentId', '[0-9]+') // 限制为数字
  680. ->withoutMiddleware([
  681. Authenticate::class,
  682. 'auth',
  683. 'auth:sanctum',
  684. 'auth:api',
  685. ])
  686. ->name('api.knowledge-mastery.snapshot.create');
  687. /*
  688. |--------------------------------------------------------------------------
  689. | 教材管理 API 路由
  690. |--------------------------------------------------------------------------
  691. */
  692. // 获取教材列表(按年级排序)
  693. Route::get('/textbooks', [TextbookApiController::class, 'index'])
  694. ->name('api.textbooks.index');
  695. // 根据年级获取教材
  696. Route::get('/textbooks/grade/{grade}', [TextbookApiController::class, 'getByGrade'])
  697. ->name('api.textbooks.by-grade');
  698. // 获取教材系列列表(必须在 {id} 路由之前定义)
  699. Route::get('/textbooks/series', [TextbookApiController::class, 'getSeries'])
  700. ->name('api.textbooks.series');
  701. // 获取年级枚举
  702. Route::get('/textbooks/grades', [TextbookApiController::class, 'getGradeEnums'])
  703. ->name('api.textbooks.grades');
  704. // 获取单个教材详情
  705. Route::get('/textbooks/{id}', [TextbookApiController::class, 'show'])
  706. ->name('api.textbooks.show');
  707. // 获取教材目录
  708. Route::get('/textbooks/{id}/catalog', [TextbookApiController::class, 'getCatalog'])
  709. ->name('api.textbooks.catalog');
  710. /*
  711. |--------------------------------------------------------------------------
  712. | MathRecSys 集成 API 路由
  713. |--------------------------------------------------------------------------
  714. */
  715. use App\Http\Controllers\Api\StudentController;
  716. // 健康检查
  717. Route::get('/mathrecsys/health', [StudentController::class, 'checkServiceHealth'])->name('api.mathrecsys.health');
  718. // 学生相关 API
  719. Route::prefix('mathrecsys/students')->name('api.mathrecsys.students.')->group(function () {
  720. // 获取学生完整信息
  721. Route::get('{studentId}', [StudentController::class, 'show'])
  722. ->where('studentId', '[0-9]+') // 限制为数字
  723. ->name('show');
  724. // 获取个性化推荐
  725. Route::get('{studentId}/recommendations', [StudentController::class, 'getRecommendations'])
  726. ->where('studentId', '[0-9]+') // 限制为数字
  727. ->name('recommendations');
  728. // 获取学习轨迹
  729. Route::get('{studentId}/trajectory', [StudentController::class, 'getTrajectory'])
  730. ->where('studentId', '[0-9]+') // 限制为数字
  731. ->name('trajectory');
  732. // 获取学习建议
  733. Route::get('{studentId}/suggestions', [StudentController::class, 'getSuggestions'])
  734. ->where('studentId', '[0-9]+') // 限制为数字
  735. ->name('suggestions');
  736. // 智能分析题目
  737. Route::post('{studentId}/analyze', [StudentController::class, 'analyzeQuestion'])
  738. ->where('studentId', '[0-9]+') // 限制为数字
  739. ->name('analyze');
  740. // 更新掌握度
  741. Route::put('{studentId}/mastery', [StudentController::class, 'updateMastery'])
  742. ->where('studentId', '[0-9]+') // 限制为数字
  743. ->name('update-mastery');
  744. // 【新增】获取学生知识点掌握详情(直接查询MySQL)
  745. Route::get('{studentId}/knowledge-points/detail', [StudentKnowledgeController::class, 'getKnowledgePointsDetail'])
  746. ->where('studentId', '[0-9]+')
  747. ->name('knowledge-points.detail');
  748. // 【新增】获取学生知识点层级关系
  749. Route::get('{studentId}/knowledge-points/hierarchy', [StudentKnowledgeController::class, 'getKnowledgeHierarchy'])
  750. ->where('studentId', '[0-9]+')
  751. ->name('knowledge-points.hierarchy');
  752. });
  753. // 【前端直连】学生知识点详情API(直接查询MySQL)
  754. Route::prefix('students')->name('students.')->group(function () {
  755. Route::get('{studentId}/knowledge-points/detail', [StudentKnowledgeController::class, 'getKnowledgePointsDetail'])
  756. ->where('studentId', '[0-9]+')
  757. ->name('knowledge-points.detail');
  758. Route::get('{studentId}/knowledge-points/hierarchy', [StudentKnowledgeController::class, 'getKnowledgeHierarchy'])
  759. ->where('studentId', '[0-9]+')
  760. ->name('knowledge-points.hierarchy');
  761. });
  762. // 班级分析 API
  763. Route::prefix('mathrecsys/classes')->name('api.mathrecsys.classes.')->group(function () {
  764. Route::get('{classId}/analysis', [StudentController::class, 'classAnalysis'])
  765. ->where('classId', '[0-9]+') // 限制为数字
  766. ->name('analysis');
  767. });
  768. // 测试 API
  769. Route::get('/mathrecsys/test', function () {
  770. return response()->json([
  771. 'success' => true,
  772. 'message' => 'MathRecSys API integration is working',
  773. 'timestamp' => now()->toISOString(),
  774. ]);
  775. })->name('api.mathrecsys.test');
  776. // 测试OCR题目生成API调用
  777. Route::post('/test-ocr-generation', function () {
  778. try {
  779. $service = new \App\Services\QuestionBankService;
  780. // 模拟前端传递的OCR题目数据
  781. $questions = [
  782. [
  783. 'id' => 1,
  784. 'content' => '计算:2+3-4',
  785. ],
  786. [
  787. 'id' => 2,
  788. 'content' => '解方程:x+5=10',
  789. ],
  790. ];
  791. Log::info('开始测试OCR题目生成', [
  792. 'questions_count' => count($questions),
  793. 'ocr_record_id' => 12,
  794. ]);
  795. // 使用异步API,系统自动生成回调URL
  796. $response = $service->generateQuestionsFromOcrAsync(
  797. $questions,
  798. '高一',
  799. '数学',
  800. 12, // OCR记录ID
  801. null, // 让系统自动生成回调URL
  802. 'api.ocr.callback' // 回调路由名称
  803. );
  804. Log::info('OCR题目生成响应', [
  805. 'response' => $response,
  806. 'status' => $response['status'] ?? 'unknown',
  807. 'task_id' => $response['task_id'] ?? 'N/A',
  808. ]);
  809. return response()->json([
  810. 'success' => true,
  811. 'message' => 'OCR题目生成测试完成',
  812. 'data' => $response,
  813. ]);
  814. } catch (\Exception $e) {
  815. Log::error('测试OCR题目生成失败', [
  816. 'error' => $e->getMessage(),
  817. 'trace' => $e->getTraceAsString(),
  818. ]);
  819. return response()->json([
  820. 'success' => false,
  821. 'error' => $e->getMessage(),
  822. ], 500);
  823. }
  824. })->name('api.test.ocr.generation');
  825. /*
  826. |--------------------------------------------------------------------------
  827. | 学生作答分析 API 路由
  828. |--------------------------------------------------------------------------
  829. */
  830. // 提交学生作答结果
  831. Route::post('/student-answers/analyze', [StudentAnswerAnalysisController::class, 'submitAnswers'])
  832. ->withoutMiddleware([
  833. Authenticate::class,
  834. 'auth',
  835. 'auth:sanctum',
  836. 'auth:api',
  837. ])
  838. ->name('api.student-answers.analyze');
  839. // 查询分析任务状态
  840. Route::get('/student-answers/analysis/status/{taskId}', [StudentAnswerAnalysisController::class, 'getAnalysisStatus'])
  841. ->withoutMiddleware([
  842. Authenticate::class,
  843. 'auth',
  844. 'auth:sanctum',
  845. 'auth:api',
  846. ])
  847. ->name('api.student-answers.analysis.status');
  848. // 获取学生学习历史
  849. Route::get('/student-answers/history/{studentId}', [StudentAnswerAnalysisController::class, 'getStudentLearningHistory'])
  850. ->withoutMiddleware([
  851. Authenticate::class,
  852. 'auth',
  853. 'auth:sanctum',
  854. 'auth:api',
  855. ])
  856. ->name('api.student-answers.history');
  857. /*
  858. |--------------------------------------------------------------------------
  859. | 考试答题分析 API 路由(步骤级分析)
  860. |--------------------------------------------------------------------------
  861. */
  862. use App\Http\Controllers\Api\ExamAnswerAnalysisController;
  863. use App\Http\Controllers\Api\HealthCheckController;
  864. use App\Http\Controllers\Api\PaperSubmitAnalysisController;
  865. // 分析考试答题数据
  866. Route::post('/exam-answer-analysis', [ExamAnswerAnalysisController::class, 'analyze'])
  867. ->withoutMiddleware([
  868. Authenticate::class,
  869. 'auth',
  870. 'auth:sanctum',
  871. 'auth:api',
  872. ])
  873. ->name('api.exam-answer-analysis.analyze');
  874. // 获取分析结果
  875. Route::get('/exam-answer-analysis/{student_id}/{paper_id}', [ExamAnswerAnalysisController::class, 'getAnalysisResult'])
  876. ->withoutMiddleware([
  877. Authenticate::class,
  878. 'auth',
  879. 'auth:sanctum',
  880. 'auth:api',
  881. ])
  882. ->where('student_id', '.*')
  883. ->where('paper_id', '.*')
  884. ->name('api.exam-answer-analysis.result');
  885. // 获取学生历史分析记录
  886. Route::get('/exam-answer-analysis/history/{student_id}', [ExamAnswerAnalysisController::class, 'getHistory'])
  887. ->withoutMiddleware([
  888. Authenticate::class,
  889. 'auth',
  890. 'auth:sanctum',
  891. 'auth:api',
  892. ])
  893. ->where('student_id', '.*')
  894. ->name('api.exam-answer-analysis.history');
  895. // 获取知识点掌握度趋势
  896. Route::get('/exam-answer-analysis/mastery-trend/{student_id}', [ExamAnswerAnalysisController::class, 'getMasteryTrend'])
  897. ->withoutMiddleware([
  898. Authenticate::class,
  899. 'auth',
  900. 'auth:sanctum',
  901. 'auth:api',
  902. ])
  903. ->where('student_id', '.*')
  904. ->name('api.exam-answer-analysis.mastery-trend');
  905. // 获取智能出卷推荐
  906. Route::get('/exam-answer-analysis/smart-quiz/{student_id}', [ExamAnswerAnalysisController::class, 'getSmartQuizRecommendation'])
  907. ->withoutMiddleware([
  908. Authenticate::class,
  909. 'auth',
  910. 'auth:sanctum',
  911. 'auth:api',
  912. ])
  913. ->where('student_id', '.*')
  914. ->name('api.exam-answer-analysis.smart-quiz');
  915. // 导出分析报告
  916. Route::get('/exam-answer-analysis/export/{student_id}/{paper_id}', [ExamAnswerAnalysisController::class, 'export'])
  917. ->withoutMiddleware([
  918. Authenticate::class,
  919. 'auth',
  920. 'auth:sanctum',
  921. 'auth:api',
  922. ])
  923. ->where('student_id', '.*')
  924. ->where('paper_id', '.*')
  925. ->name('api.exam-answer-analysis.export');
  926. // 批量分析多个学生的考试数据
  927. Route::post('/exam-answer-analysis/batch', [ExamAnswerAnalysisController::class, 'batchAnalyze'])
  928. ->withoutMiddleware([
  929. Authenticate::class,
  930. 'auth',
  931. 'auth:sanctum',
  932. 'auth:api',
  933. ])
  934. ->name('api.exam-answer-analysis.batch');
  935. Route::get('/tasks/status/{taskId}', function (string $taskId) {
  936. $task = app(\App\Services\TaskManager::class)->getTaskStatus($taskId);
  937. if (! $task) {
  938. return response()->json([
  939. 'success' => false,
  940. 'message' => '任务不存在',
  941. ], 404);
  942. }
  943. return response()->json([
  944. 'success' => true,
  945. 'data' => $task,
  946. ]);
  947. })->name('api.tasks.status');
  948. /*
  949. |--------------------------------------------------------------------------
  950. | 试卷提交分析 API 路由(前端提交答题数据)
  951. |--------------------------------------------------------------------------
  952. */
  953. // 提交试卷答题数据进行分析
  954. Route::post('/paper-submit-analysis', [PaperSubmitAnalysisController::class, 'analyze'])
  955. ->withoutMiddleware([
  956. Authenticate::class,
  957. 'auth',
  958. 'auth:sanctum',
  959. 'auth:api',
  960. ])
  961. ->name('api.paper-submit-analysis.analyze');
  962. // 获取试卷分析结果
  963. Route::get('/paper-submit-analysis/{paperId}', [PaperSubmitAnalysisController::class, 'getResult'])
  964. ->withoutMiddleware([
  965. Authenticate::class,
  966. 'auth',
  967. 'auth:sanctum',
  968. 'auth:api',
  969. ])
  970. ->name('api.paper-submit-analysis.result');
  971. /*
  972. |--------------------------------------------------------------------------
  973. | 健康检查 API 路由
  974. |--------------------------------------------------------------------------
  975. */
  976. // 检查系统健康状态
  977. Route::get('/health', [HealthCheckController::class, 'index'])
  978. ->withoutMiddleware([
  979. Authenticate::class,
  980. 'auth',
  981. 'auth:sanctum',
  982. 'auth:api',
  983. ])
  984. ->name('api.health.index');
  985. /*
  986. |--------------------------------------------------------------------------
  987. | 学生学习进度 API 路由
  988. |--------------------------------------------------------------------------
  989. */
  990. use App\Http\Controllers\Api\QuestionPdfController;
  991. use App\Http\Controllers\Api\StudentProgressController;
  992. // 获取单个学生学习进度
  993. Route::get('/students/{studentId}/learning-progress', [StudentProgressController::class, 'show'])
  994. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  995. ->name('api.students.learning-progress.show');
  996. // 批量获取学生学习进度
  997. Route::post('/students/learning-progress/batch', [StudentProgressController::class, 'batch'])
  998. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  999. ->name('api.students.learning-progress.batch');
  1000. /*
  1001. |--------------------------------------------------------------------------
  1002. | 题目PDF生成 API 路由
  1003. |--------------------------------------------------------------------------
  1004. */
  1005. // 根据指定题目ID生成PDF
  1006. Route::post('/questions/pdf', [QuestionPdfController::class, 'generate'])
  1007. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  1008. ->name('api.questions.pdf.generate');
  1009. /*
  1010. |--------------------------------------------------------------------------
  1011. | 试卷 PDF 重新生成 API 路由
  1012. |--------------------------------------------------------------------------
  1013. */
  1014. // 批量重新生成 PDF:按日期遍历 completed_at=NULL 的卷子(需在带 paper_id 的路由之前)
  1015. Route::post('/papers/regenerate-batch', [\App\Http\Controllers\ExamPdfController::class, 'regeneratePdfBatch'])
  1016. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  1017. ->name('api.papers.regenerate-batch');
  1018. // 批量重新生成 PDF:按卷子 ID 数组投递队列
  1019. Route::post('/papers/regenerate-batch-by-ids', [\App\Http\Controllers\ExamPdfController::class, 'regeneratePdfBatchByIds'])
  1020. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  1021. ->name('api.papers.regenerate-batch-by-ids');
  1022. // 重新生成统一 PDF(卷子 + 判卷)
  1023. Route::post('/papers/{paper_id}/regenerate', [\App\Http\Controllers\ExamPdfController::class, 'regeneratePdf'])
  1024. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  1025. ->where('paper_id', '^paper_\d+$')
  1026. ->name('api.papers.regenerate');
  1027. // 重新生成试卷 PDF(不含答案)
  1028. Route::post('/papers/{paper_id}/regenerate-exam', [\App\Http\Controllers\ExamPdfController::class, 'regenerateExamPdf'])
  1029. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  1030. ->where('paper_id', '^paper_\d+$')
  1031. ->name('api.papers.regenerate-exam');
  1032. // 重新生成判卷 PDF(含答案)
  1033. Route::post('/papers/{paper_id}/regenerate-grading', [\App\Http\Controllers\ExamPdfController::class, 'regenerateGradingPdf'])
  1034. ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
  1035. ->where('paper_id', '^paper_\d+$')
  1036. ->name('api.papers.regenerate-grading');
  1037. /*
  1038. |--------------------------------------------------------------------------
  1039. | 以下为旧代码(已迁移到 Controller,保留注释供参考)
  1040. |--------------------------------------------------------------------------
  1041. */
  1042. // [已迁移] 获取学生学习进度
  1043. /*
  1044. Route::get('/students/{studentId}/learning-progress', function (string $studentId) {
  1045. try {
  1046. Log::info('获取学生学习进度', ['student_id' => $studentId]);
  1047. // 1. 获取所有父知识点代码(需要排除的)
  1048. $parentKpCodes = [];
  1049. try {
  1050. $parentCodes = DB::connection('remote_mysql')
  1051. ->table('knowledge_points')
  1052. ->whereNotNull('parent_kp_code')
  1053. ->distinct()
  1054. ->pluck('parent_kp_code')
  1055. ->toArray();
  1056. $parentKpCodes = array_filter($parentCodes);
  1057. } catch (\Exception $e) {
  1058. Log::warning('获取父知识点代码失败', ['error' => $e->getMessage()]);
  1059. }
  1060. // 2. 获取学生所有知识点的掌握度数据(合并两个表)
  1061. $mergedData = [];
  1062. try {
  1063. // 从 student_knowledge_mastery 表获取数据
  1064. $detailedData = DB::connection('remote_mysql')
  1065. ->table('student_knowledge_mastery')
  1066. ->where('student_id', $studentId)
  1067. ->select([
  1068. 'kp_code',
  1069. 'mastery_level',
  1070. 'total_attempts',
  1071. 'correct_attempts',
  1072. 'updated_at'
  1073. ])
  1074. ->get()
  1075. ->toArray();
  1076. foreach ($detailedData as $item) {
  1077. $mergedData[$item->kp_code] = [
  1078. 'kp_code' => $item->kp_code,
  1079. 'mastery_level' => (float) $item->mastery_level,
  1080. 'total_attempts' => $item->total_attempts,
  1081. 'correct_attempts' => $item->correct_attempts,
  1082. 'source_table' => 'student_knowledge_mastery',
  1083. 'updated_at' => $item->updated_at
  1084. ];
  1085. }
  1086. } catch (\Exception $e) {
  1087. Log::warning('从 student_knowledge_mastery 表获取数据失败', [
  1088. 'student_id' => $studentId,
  1089. 'error' => $e->getMessage()
  1090. ]);
  1091. }
  1092. try {
  1093. // 从 student_mastery 表获取数据(补充或覆盖)
  1094. $simpleData = DB::connection('remote_mysql')
  1095. ->table('student_mastery')
  1096. ->where('student_id', $studentId)
  1097. ->select([
  1098. 'kp as kp_code',
  1099. 'mastery',
  1100. 'attempts as total_attempts',
  1101. 'correct as correct_attempts',
  1102. 'updated_at'
  1103. ])
  1104. ->get()
  1105. ->toArray();
  1106. foreach ($simpleData as $item) {
  1107. $kpCode = $item->kp_code;
  1108. $masteryLevel = (float) $item->mastery;
  1109. // 如果已存在,优先使用 mastery_level 更高的数据
  1110. if (isset($mergedData[$kpCode])) {
  1111. if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
  1112. $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
  1113. $mergedData[$kpCode]['source_table'] = 'student_mastery (updated)';
  1114. }
  1115. } else {
  1116. $mergedData[$kpCode] = [
  1117. 'kp_code' => $kpCode,
  1118. 'mastery_level' => $masteryLevel,
  1119. 'total_attempts' => $item->total_attempts ?? 0,
  1120. 'correct_attempts' => $item->correct_attempts ?? 0,
  1121. 'source_table' => 'student_mastery',
  1122. 'updated_at' => $item->updated_at ?? null
  1123. ];
  1124. }
  1125. }
  1126. } catch (\Exception $e) {
  1127. Log::warning('从 student_mastery 表获取数据失败', [
  1128. 'student_id' => $studentId,
  1129. 'error' => $e->getMessage()
  1130. ]);
  1131. }
  1132. // 3. 获取学生所有知识点掌握度数据(只使用student_knowledge_mastery表,因为student_mastery表为空)
  1133. $allMasteryData = collect($mergedData);
  1134. if ($allMasteryData->isEmpty()) {
  1135. return response()->json([
  1136. 'success' => false,
  1137. 'error' => '该学生没有掌握度数据'
  1138. ], 400);
  1139. }
  1140. // 4. 获取知识图谱中所有知识点,找出真正的子知识点(叶子节点)
  1141. $allKps = DB::connection('remote_mysql')
  1142. ->table('knowledge_points')
  1143. ->select(['kp_code', 'parent_kp_code'])
  1144. ->get();
  1145. // 找出叶子节点(没有任何其他知识点以它作为父节点)
  1146. $kpCodes = $allKps->pluck('kp_code')->toArray();
  1147. $parentCodes = $allKps->whereNotNull('parent_kp_code')->pluck('parent_kp_code')->unique()->toArray();
  1148. $leafKpCodes = array_values(array_diff($kpCodes, $parentCodes));
  1149. // 5. 筛选出学生掌握的子知识点数据
  1150. $childMasteryData = $allMasteryData->filter(function($item) use ($leafKpCodes) {
  1151. return in_array($item['kp_code'], $leafKpCodes);
  1152. });
  1153. if ($childMasteryData->isEmpty()) {
  1154. return response()->json([
  1155. 'success' => false,
  1156. 'error' => '该学生没有子知识点的掌握度数据'
  1157. ], 400);
  1158. }
  1159. // 6. 计算分子:学生已掌握的子知识点掌握度总和
  1160. $totalChildMasterySum = $childMasteryData->sum('mastery_level');
  1161. // 7. 计算分母:知识图谱中所有子知识点的最大可能总分
  1162. $maxChildScore = count($leafKpCodes) * 1.0; // 每个子知识点最高1分
  1163. $learningProgress = $maxChildScore > 0 ? ($totalChildMasterySum / $maxChildScore) : 0.0;
  1164. // 8. 统计信息
  1165. $statistics = [
  1166. 'total_knowledge_points' => count($kpCodes),
  1167. 'child_knowledge_points' => count($leafKpCodes),
  1168. 'student_mastered_child_count' => $childMasteryData->count(),
  1169. 'student_mastered_child_percentage' => round(($childMasteryData->count() / count($leafKpCodes)) * 100, 2),
  1170. 'child_mastery_sum' => round($totalChildMasterySum, 4),
  1171. 'max_child_score' => round($maxChildScore, 4),
  1172. 'learning_progress_percentage' => round($learningProgress * 100, 2),
  1173. 'data_source' => implode(', ', $allMasteryData->pluck('source_table')->unique()->toArray()),
  1174. 'child_mastery_max' => round($childMasteryData->max('mastery_level'), 4),
  1175. 'child_mastery_min' => round($childMasteryData->min('mastery_level'), 4),
  1176. 'child_mastery_avg' => round($childMasteryData->avg('mastery_level'), 4),
  1177. ];
  1178. $result = [
  1179. 'student_id' => $studentId,
  1180. 'learning_progress' => round($learningProgress, 6),
  1181. 'learning_progress_percentage' => round($learningProgress * 100, 2),
  1182. 'child_mastery_sum' => round($totalChildMasterySum, 4),
  1183. 'child_knowledge_points' => $childMasteryData->values()->toArray(),
  1184. 'statistics' => $statistics,
  1185. 'calculated_at' => now()->toISOString()
  1186. ];
  1187. Log::info('学生学习进度计算成功', [
  1188. 'student_id' => $studentId,
  1189. 'learning_progress' => $learningProgress,
  1190. 'child_mastery_sum' => $totalChildMasterySum,
  1191. 'child_knowledge_points_count' => count($leafKpCodes),
  1192. 'student_mastered_child_count' => $childMasteryData->count()
  1193. ]);
  1194. return response()->json([
  1195. 'success' => true,
  1196. 'data' => $result,
  1197. 'message' => '学习进度计算成功'
  1198. ]);
  1199. } catch (\Exception $e) {
  1200. Log::error('计算学生学习进度失败', [
  1201. 'student_id' => $studentId,
  1202. 'error' => $e->getMessage(),
  1203. 'trace' => $e->getTraceAsString()
  1204. ]);
  1205. return response()->json([
  1206. 'success' => false,
  1207. 'message' => '计算学习进度失败: ' . $e->getMessage()
  1208. ], 500);
  1209. }
  1210. })
  1211. ->withoutMiddleware([
  1212. Authenticate::class,
  1213. 'auth',
  1214. 'auth:sanctum',
  1215. 'auth:api',
  1216. ])
  1217. ->name('api.students.learning-progress.get');
  1218. // 批量获取学生学习进度(解决 N+1 问题)
  1219. Route::post('/students/learning-progress/batch', function (\Illuminate\Http\Request $request) {
  1220. try {
  1221. $studentIds = $request->input('student_ids', []);
  1222. if (empty($studentIds)) {
  1223. return response()->json([
  1224. 'success' => false,
  1225. 'error' => 'student_ids 不能为空'
  1226. ], 400);
  1227. }
  1228. if (count($studentIds) > 100) {
  1229. return response()->json([
  1230. 'success' => false,
  1231. 'error' => '单次最多查询 100 个学生'
  1232. ], 400);
  1233. }
  1234. Log::info('批量获取学生学习进度', ['student_ids' => $studentIds, 'count' => count($studentIds)]);
  1235. // 1. 获取知识图谱结构(所有学生共用)
  1236. $allKps = DB::connection('remote_mysql')
  1237. ->table('knowledge_points')
  1238. ->select(['kp_code', 'parent_kp_code'])
  1239. ->get();
  1240. // 找出叶子节点
  1241. $kpCodes = $allKps->pluck('kp_code')->toArray();
  1242. $parentCodes = $allKps->whereNotNull('parent_kp_code')->pluck('parent_kp_code')->unique()->toArray();
  1243. $leafKpCodes = array_values(array_diff($kpCodes, $parentCodes));
  1244. $leafKpCodesSet = array_flip($leafKpCodes); // 用于快速查找
  1245. $maxChildScore = count($leafKpCodes) * 1.0;
  1246. // 2. 批量获取 student_knowledge_mastery 数据
  1247. $detailedData = DB::connection('remote_mysql')
  1248. ->table('student_knowledge_mastery')
  1249. ->whereIn('student_id', $studentIds)
  1250. ->select(['student_id', 'kp_code', 'mastery_level', 'total_attempts', 'correct_attempts', 'updated_at'])
  1251. ->get()
  1252. ->groupBy('student_id');
  1253. // 3. 批量获取 student_mastery 数据
  1254. $simpleData = DB::connection('remote_mysql')
  1255. ->table('student_mastery')
  1256. ->whereIn('student_id', $studentIds)
  1257. ->select(['student_id', 'kp as kp_code', 'mastery', 'attempts as total_attempts', 'correct as correct_attempts', 'updated_at'])
  1258. ->get()
  1259. ->groupBy('student_id');
  1260. // 4. 为每个学生计算学习进度
  1261. $results = [];
  1262. foreach ($studentIds as $studentId) {
  1263. $studentId = (string) $studentId;
  1264. // 合并该学生的掌握度数据
  1265. $mergedData = [];
  1266. // 从 student_knowledge_mastery 获取
  1267. if (isset($detailedData[$studentId])) {
  1268. foreach ($detailedData[$studentId] as $item) {
  1269. $mergedData[$item->kp_code] = [
  1270. 'kp_code' => $item->kp_code,
  1271. 'mastery_level' => (float) $item->mastery_level,
  1272. ];
  1273. }
  1274. }
  1275. // 从 student_mastery 补充或更新
  1276. if (isset($simpleData[$studentId])) {
  1277. foreach ($simpleData[$studentId] as $item) {
  1278. $kpCode = $item->kp_code;
  1279. $masteryLevel = (float) $item->mastery;
  1280. if (isset($mergedData[$kpCode])) {
  1281. if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
  1282. $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
  1283. }
  1284. } else {
  1285. $mergedData[$kpCode] = [
  1286. 'kp_code' => $kpCode,
  1287. 'mastery_level' => $masteryLevel,
  1288. ];
  1289. }
  1290. }
  1291. }
  1292. // 计算学习进度
  1293. if (empty($mergedData)) {
  1294. $results[$studentId] = [
  1295. 'student_id' => $studentId,
  1296. 'learning_progress' => 0,
  1297. 'learning_progress_percentage' => 0,
  1298. 'mastered_child_count' => 0,
  1299. 'total_child_count' => count($leafKpCodes),
  1300. 'has_data' => false,
  1301. ];
  1302. continue;
  1303. }
  1304. // 筛选叶子节点并计算
  1305. $childMasterySum = 0;
  1306. $masteredChildCount = 0;
  1307. foreach ($mergedData as $item) {
  1308. if (isset($leafKpCodesSet[$item['kp_code']])) {
  1309. $childMasterySum += $item['mastery_level'];
  1310. $masteredChildCount++;
  1311. }
  1312. }
  1313. $learningProgress = $maxChildScore > 0 ? ($childMasterySum / $maxChildScore) : 0.0;
  1314. $results[$studentId] = [
  1315. 'student_id' => $studentId,
  1316. 'learning_progress' => round($learningProgress, 6),
  1317. 'learning_progress_percentage' => round($learningProgress * 100, 2),
  1318. 'mastered_child_count' => $masteredChildCount,
  1319. 'total_child_count' => count($leafKpCodes),
  1320. 'child_mastery_sum' => round($childMasterySum, 4),
  1321. 'has_data' => true,
  1322. ];
  1323. }
  1324. Log::info('批量学习进度计算完成', ['count' => count($results)]);
  1325. return response()->json([
  1326. 'success' => true,
  1327. 'data' => $results,
  1328. 'meta' => [
  1329. 'total_students' => count($studentIds),
  1330. 'total_child_knowledge_points' => count($leafKpCodes),
  1331. 'calculated_at' => now()->toISOString(),
  1332. ]
  1333. ]);
  1334. } catch (\Exception $e) {
  1335. Log::error('批量计算学习进度失败', [
  1336. 'error' => $e->getMessage(),
  1337. 'trace' => $e->getTraceAsString()
  1338. ]);
  1339. return response()->json([
  1340. 'success' => false,
  1341. 'message' => '批量计算学习进度失败: ' . $e->getMessage()
  1342. ], 500);
  1343. }
  1344. })
  1345. ->withoutMiddleware([
  1346. Authenticate::class,
  1347. 'auth',
  1348. 'auth:sanctum',
  1349. 'auth:api',
  1350. ])
  1351. ->name('api.students.learning-progress.batch');
  1352. */
  1353. /*
  1354. |--------------------------------------------------------------------------
  1355. | 注意:知识点详情API已被注释
  1356. |--------------------------------------------------------------------------
  1357. |
  1358. | 由于路由冲突问题,此API暂时被注释。可使用以下替代接口:
  1359. | - api/mathrecsys/students/{studentId}/knowledge-points/detail
  1360. | - api/students/{studentId}/knowledge-points/detail
  1361. |
  1362. */
  1363. // 获取学生知识点掌握度详情(测试版)
  1364. // Route::get('/students/{studentId}/knowledge-points', function (string $studentId) {
  1365. // // 实现代码...
  1366. // })
  1367. // ->withoutMiddleware([
  1368. // Authenticate::class,
  1369. // 'auth',
  1370. // 'auth:sanctum',
  1371. // 'auth:api',
  1372. // ])
  1373. // ->name('api.students.knowledge-points.details');