api.php 48 KB

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