| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576 |
- <?php
- use App\Http\Controllers\Api\AbilityEvaluateController;
- use App\Http\Controllers\Api\ExamAnalysisApiController;
- use App\Http\Controllers\Api\IntelligentExamController;
- use App\Http\Controllers\Api\KnowledgeRecommendController;
- use App\Http\Controllers\Api\MistakeBookController;
- use App\Http\Controllers\Api\PaperAssembleController;
- use App\Http\Controllers\Api\PaperJsonController;
- use App\Http\Controllers\Api\PreQuestionApiController;
- use App\Http\Controllers\Api\QuestionRandomController;
- use App\Http\Controllers\Api\QuestionSearchController;
- use App\Http\Controllers\Api\QuestionSolutionController;
- use App\Http\Controllers\Api\StudentAnswerAnalysisController;
- use App\Http\Controllers\Api\StudentKnowledgeController;
- use App\Http\Controllers\Api\TextbookApiController;
- use App\Services\QuestionServiceApi;
- use Illuminate\Auth\Middleware\Authenticate;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Route;
- /*
- |--------------------------------------------------------------------------
- | 题库管理 API 路由
- |--------------------------------------------------------------------------
- */
- // 给 Python/内部服务消费的“筛选题库”API(需要 X-Internal-Token)
- Route::middleware('internal.token')->get('/pre-questions', [PreQuestionApiController::class, 'index'])
- ->name('api.pre-questions.index');
- Route::get('/questions/search', QuestionSearchController::class)->name('api.questions.search');
- Route::get('/questions/random', QuestionRandomController::class)->name('api.questions.random');
- Route::post('/papers/assemble', PaperAssembleController::class)->name('api.papers.assemble');
- Route::get('/papers/{paperId}/json', [PaperJsonController::class, 'show'])->name('api.papers.json');
- Route::get('/questions/{id}/solution', QuestionSolutionController::class)->name('api.questions.solution');
- Route::get('/knowledge/recommend', KnowledgeRecommendController::class)->name('api.knowledge.recommend');
- Route::post('/abilities/evaluate', AbilityEvaluateController::class)->name('api.abilities.evaluate');
- // 接收题目生成回调
- Route::post('/questions/callback', function () {
- try {
- $data = request()->all();
- Log::info('Received question generation callback', $data);
- // 验证回调数据
- if (! isset($data['task_id']) || ! isset($data['status'])) {
- return response()->json(['error' => 'Invalid callback data'], 400);
- }
- // 处理回调数据并存储通知到session
- if ($data['status'] === 'completed') {
- $result = $data['result'] ?? [];
- $total = $result['total'] ?? $data['total'] ?? ($result['saved'] ?? 0);
- $kpCode = $result['kp_code'] ?? $data['kp_code'] ?? '';
- // 将成功通知存储到session,供下次页面刷新时显示
- session()->flash('notification', [
- 'type' => 'success',
- 'title' => '✅ 题目生成完成',
- 'body' => "任务 ID: {$data['task_id']}\n生成题目: {$total} 道".($kpCode ? "\n知识点: {$kpCode}" : ''),
- 'color' => 'success',
- ]);
- Log::info('题目生成成功通知已存储', [
- 'task_id' => $data['task_id'],
- 'total' => $total,
- 'kp_code' => $kpCode,
- ]);
- } elseif ($data['status'] === 'failed') {
- $error = $data['error'] ?? '未知错误';
- // 将失败通知存储到session
- session()->flash('notification', [
- 'type' => 'error',
- 'title' => '❌ 题目生成失败',
- 'body' => "任务 ID: {$data['task_id']}\n错误: {$error}",
- 'color' => 'danger',
- ]);
- Log::error('题目生成失败通知已存储', [
- 'task_id' => $data['task_id'],
- 'error' => $error,
- ]);
- }
- return response()->json([
- 'success' => true,
- 'message' => 'Callback received and notification stored',
- 'status' => $data['status'],
- ]);
- } catch (\Exception $e) {
- Log::error('Callback processing failed: '.$e->getMessage());
- return response()->json(['error' => $e->getMessage()], 500);
- }
- })->name('api.questions.callback');
- // 接收OCR题目生成回调
- Route::post('/ocr-question-callback', function () {
- try {
- $data = request()->all();
- Log::info('Received OCR question generation callback', $data);
- // 验证必要的回调数据
- if (! isset($data['task_id']) || ! isset($data['status']) || ! isset($data['ocr_record_id'])) {
- Log::error('OCR callback missing required fields', $data);
- return response()->json([
- 'success' => false,
- 'error' => 'Missing required fields: task_id, status, ocr_record_id',
- ], 400);
- }
- $taskId = $data['task_id'];
- $ocrRecordId = $data['ocr_record_id'];
- $status = $data['status'];
- // 将回调结果存储到缓存中,供前端查询(保留30秒)
- $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
- cache([$cacheKey => $data], now()->addSeconds(30));
- Log::info("OCR callback cached with key: {$cacheKey}", [
- 'ocr_record_id' => $ocrRecordId,
- 'task_id' => $taskId,
- 'status' => $status,
- 'total_generated' => $data['result']['total_generated'] ?? 0,
- 'total_saved' => $data['result']['total_saved'] ?? 0,
- ]);
- // 处理题目关联逻辑
- if ($status === 'completed') {
- $updatedCount = 0;
- // 从result中提取question_mappings(QuestionBank API将它放在result字段中)
- $mappings = $data['result']['question_mappings'] ?? $data['question_mappings'] ?? [];
- Log::info('Processing OCR question associations', [
- 'ocr_record_id' => $ocrRecordId,
- 'task_id' => $taskId,
- 'mappings_count' => count($mappings),
- ]);
- // 更新ocr_question_results表中的关联关系
- foreach ($mappings as $mapping) {
- try {
- $ocrQuestionNumber = $mapping['ocr_question_number'] ?? null;
- $questionBankId = $mapping['question_bank_id'] ?? null;
- $questionCode = $mapping['question_code'] ?? null;
- if ($ocrQuestionNumber && $questionBankId) {
- // 查找对应的OCR题目结果并更新
- $updated = DB::table('ocr_question_results')
- ->where('ocr_record_id', $ocrRecordId)
- ->where('question_number', $ocrQuestionNumber)
- ->update([
- 'question_bank_id' => $questionBankId,
- 'generation_status' => 'completed',
- 'generation_task_id' => $taskId,
- 'generation_error' => null,
- ]);
- if ($updated) {
- $updatedCount++;
- Log::info('Updated OCR question association', [
- 'ocr_record_id' => $ocrRecordId,
- 'question_number' => $ocrQuestionNumber,
- 'question_bank_id' => $questionBankId,
- 'question_code' => $questionCode,
- ]);
- } else {
- Log::warning('No OCR question result found for association', [
- 'ocr_record_id' => $ocrRecordId,
- 'question_number' => $ocrQuestionNumber,
- ]);
- }
- }
- } catch (\Exception $e) {
- Log::error('Failed to update OCR question association', [
- 'mapping' => $mapping,
- 'error' => $e->getMessage(),
- ]);
- }
- }
- Log::info('OCR question association completed', [
- 'ocr_record_id' => $ocrRecordId,
- 'task_id' => $taskId,
- 'total_mappings' => count($mappings),
- 'updated_count' => $updatedCount,
- ]);
- // 更新OCR记录的整体状态为已完成
- try {
- DB::table('ocr_records')
- ->where('id', $ocrRecordId)
- ->update([
- 'status' => 'completed',
- 'processed_at' => now(),
- 'updated_at' => now(),
- ]);
- Log::info('Updated OCR record status to completed', [
- 'ocr_record_id' => $ocrRecordId,
- 'task_id' => $taskId,
- ]);
- } catch (\Exception $e) {
- Log::error('Failed to update OCR record status', [
- 'ocr_record_id' => $ocrRecordId,
- 'error' => $e->getMessage(),
- ]);
- }
- } elseif ($status === 'failed') {
- // 更新所有相关的OCR题目结果为失败状态
- try {
- $updated = DB::table('ocr_question_results')
- ->where('ocr_record_id', $ocrRecordId)
- ->where('generation_status', 'pending') // 只更新待处理的
- ->update([
- 'generation_status' => 'failed',
- 'generation_task_id' => $taskId,
- 'generation_error' => $data['error'] ?? 'Unknown error',
- ]);
- Log::info('Updated OCR questions to failed status', [
- 'ocr_record_id' => $ocrRecordId,
- 'task_id' => $taskId,
- 'updated_count' => $updated,
- 'error' => $data['error'] ?? 'Unknown error',
- ]);
- // 更新OCR记录的状态为失败
- DB::table('ocr_records')
- ->where('id', $ocrRecordId)
- ->update([
- 'status' => 'failed',
- 'error_message' => $data['error'] ?? 'Question generation failed',
- 'updated_at' => now(),
- ]);
- Log::info('Updated OCR record status to failed', [
- 'ocr_record_id' => $ocrRecordId,
- 'task_id' => $taskId,
- 'error' => $data['error'] ?? 'Unknown error',
- ]);
- } catch (\Exception $e) {
- Log::error('Failed to update OCR questions to failed status', [
- 'ocr_record_id' => $ocrRecordId,
- 'error' => $e->getMessage(),
- ]);
- }
- }
- return response()->json([
- 'success' => true,
- 'message' => 'OCR callback received and processed',
- 'data' => [
- 'task_id' => $taskId,
- 'ocr_record_id' => $ocrRecordId,
- 'status' => $status,
- 'cache_key' => $cacheKey,
- 'associations_processed' => $status === 'completed' ? count($data['question_mappings'] ?? []) : 0,
- ],
- ]);
- } catch (\Exception $e) {
- Log::error('OCR callback processing failed: '.$e->getMessage());
- Log::error('Exception details: '.$e->getTraceAsString());
- return response()->json([
- 'success' => false,
- 'error' => 'Callback processing failed: '.$e->getMessage(),
- ], 500);
- }
- })->name('api.ocr.callback');
- // 获取题目生成回调结果
- Route::get('/questions/callback/{taskId}', function (string $taskId) {
- // ✅ 优先从缓存读取(跨域友好)
- $callbackData = cache($taskId);
- if ($callbackData) {
- // 清除已读取的回调数据
- cache()->forget($taskId);
- session()->forget('question_gen_callback_'.$taskId);
- return response()->json($callbackData);
- }
- // 备选:从session读取
- $sessionData = session('question_gen_callback_'.$taskId);
- if ($sessionData) {
- // 清除已读取的回调数据
- session()->forget('question_gen_callback_'.$taskId);
- return response()->json($sessionData);
- }
- // 未收到回调
- return response()->json(['status' => 'pending'], 202);
- })->name('api.questions.callback.get');
- // 获取OCR题目生成回调结果
- Route::get('/ocr-question-callback/{ocrRecordId}/{taskId}', function (int $ocrRecordId, string $taskId) {
- $cacheKey = "ocr_callback_{$ocrRecordId}_{$taskId}";
- $callbackData = cache($cacheKey);
- if ($callbackData) {
- // 清除已读取的回调数据
- cache()->forget($cacheKey);
- return response()->json([
- 'success' => true,
- 'data' => $callbackData,
- ]);
- }
- return response()->json([
- 'success' => false,
- 'status' => 'pending',
- 'message' => 'OCR callback not received yet',
- ], 202);
- })->name('api.ocr.callback.get');
- // 题目相关 API
- Route::get('/questions', function (QuestionServiceApi $service) {
- try {
- $page = (int) request()->get('page', 1);
- $perPage = (int) request()->get('per_page', 25);
- $filters = [
- 'kp_code' => request()->get('kp_code'),
- 'difficulty' => request()->get('difficulty'),
- 'search' => request()->get('search'),
- ];
- $response = $service->listQuestions($page, $perPage, $filters);
- return response()->json($response);
- } catch (\Exception $e) {
- \Log::error('Failed to fetch questions: '.$e->getMessage());
- return response()->json([
- 'data' => [],
- 'meta' => [
- 'page' => 1,
- 'per_page' => 25,
- 'total' => 0,
- 'total_pages' => 0,
- ],
- 'error' => $e->getMessage(),
- ], 500);
- }
- });
- // 获取题目统计信息
- Route::get('/questions/statistics', function (QuestionServiceApi $service) {
- try {
- $stats = $service->getStatistics();
- return response()->json($stats);
- } catch (\Exception $e) {
- \Log::error('Failed to get question statistics: '.$e->getMessage());
- return response()->json(['error' => $e->getMessage()], 500);
- }
- });
- // 语义搜索题目
- Route::post('/questions/search', function (QuestionServiceApi $service) {
- try {
- $data = request()->only(['query', 'limit']);
- $results = $service->searchQuestions($data['query'], $data['limit'] ?? 20);
- return response()->json($results);
- } catch (\Exception $e) {
- \Log::error('Question search failed: '.$e->getMessage());
- return response()->json(['error' => $e->getMessage()], 500);
- }
- });
- // 获取单个题目详情
- Route::get('/questions/{id}', function (int $id, QuestionServiceApi $service) {
- try {
- $question = $service->getQuestionById($id);
- if (! $question) {
- return response()->json(['error' => 'Question not found'], 404);
- }
- return response()->json($question);
- } catch (\Exception $e) {
- \Log::error("Failed to get question {$id}: ".$e->getMessage());
- return response()->json(['error' => $e->getMessage()], 500);
- }
- });
- // AI 生成题目
- Route::post('/questions/generate', function (QuestionServiceApi $service) {
- try {
- $data = request()->only(['kp_code', 'keyword', 'count', 'strategy']);
- $result = $service->generateQuestions($data);
- return response()->json($result);
- } catch (\Exception $e) {
- \Log::error('Question generation failed: '.$e->getMessage());
- return response()->json([
- 'success' => false,
- 'message' => $e->getMessage(),
- ], 500);
- }
- });
- // 删除题目
- Route::delete('/questions/{id}', function (int $id, QuestionServiceApi $service) {
- try {
- $deleted = $service->deleteQuestion($id);
- return response()->json([
- 'success' => $deleted,
- 'message' => $deleted ? 'Question deleted' : 'Failed to delete',
- ]);
- } catch (\Exception $e) {
- \Log::error("Failed to delete question {$id}: ".$e->getMessage());
- return response()->json([
- 'success' => false,
- 'message' => $e->getMessage(),
- ], 500);
- }
- });
- use App\Http\Controllers\Api\KnowledgePointTreeController;
- // 获取知识点树形结构(从 MySQL 数据库)
- Route::get('/knowledge-points', [KnowledgePointTreeController::class, 'index'])
- ->name('api.knowledge-points.index');
- // 智能出卷对外接口:生成试卷并返回PDF/判卷地址
- Route::post('/intelligent-exams', [IntelligentExamController::class, 'store'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.intelligent-exams.store');
- // 智能出卷任务状态查询
- Route::get('/intelligent-exams/status/{taskId}', [IntelligentExamController::class, 'status'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.intelligent-exams.status');
- // 学情报告对外接口:生成并返回学情报告 PDF
- Route::post('/exam-analysis/report', [ExamAnalysisApiController::class, 'store'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.exam-analysis.report');
- // 学情报告任务状态查询
- Route::get('/exam-analysis/status/{taskId}', [ExamAnalysisApiController::class, 'status'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.exam-analysis.status');
- // 获取PDF报告URL(查询指定试卷的报告状态)
- Route::get('/exam-analysis/pdf/{paper_id}', [ExamAnalysisApiController::class, 'getPdfUrl'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.exam-analysis.pdf');
- /*
- |--------------------------------------------------------------------------
- | 错题本 API 路由
- |--------------------------------------------------------------------------
- */
- // 获取错题列表
- Route::get('/mistake-book', [MistakeBookController::class, 'listMistakes'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.list');
- // 新增错题
- Route::post('/mistake-book', [MistakeBookController::class, 'addMistake'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.create');
- // 获取单条错题详情
- Route::get('/mistake-book/{mistakeId}', [MistakeBookController::class, 'getMistakeDetail'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->whereNumber('mistakeId')
- ->name('api.mistake-book.detail');
- // 获取错题统计概要
- Route::get('/mistake-book/summary', [MistakeBookController::class, 'getSummary'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.summary');
- // 获取错误模式分析
- Route::get('/mistake-book/analytics/mistake-pattern', [MistakeBookController::class, 'getMistakePatterns'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.patterns');
- // 收藏/取消收藏错题
- Route::post('/mistake-book/{mistakeId}/favorite', [MistakeBookController::class, 'toggleFavorite'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.favorite');
- // 标记错题已复习
- Route::post('/mistake-book/{mistakeId}/review', [MistakeBookController::class, 'markReviewed'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.review');
- // 加入重练清单
- Route::post('/mistake-book/{mistakeId}/retry-list', [MistakeBookController::class, 'addToRetryList'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.retry-list');
- // 推荐练习题
- Route::post('/mistake-book/recommend-practice', [MistakeBookController::class, 'recommendPractice'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.recommend-practice');
- // 获取错题本快照数据(仪表板用)
- Route::get('/mistake-book/snapshot', [MistakeBookController::class, 'getSnapshot'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.snapshot');
- /*
- |--------------------------------------------------------------------------
- | 错题复习状态管理 API 路由
- |--------------------------------------------------------------------------
- */
- Route::post('/mistake-book/{mistakeId}/review-status', [MistakeBookController::class, 'updateReviewStatus'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.review-status.update');
- Route::get('/mistake-book/{mistakeId}/review-status', [MistakeBookController::class, 'getReviewStatus'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.review-status.get');
- Route::post('/mistake-book/{mistakeId}/increment-review', [MistakeBookController::class, 'incrementReview'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.increment-review');
- Route::post('/mistake-book/{mistakeId}/reset-review', [MistakeBookController::class, 'resetReview'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.reset-review');
- /*
- |--------------------------------------------------------------------------
- | 错题批量操作 API 路由
- |--------------------------------------------------------------------------
- */
- Route::post('/mistake-book/batch-operation', [MistakeBookController::class, 'batchOperation'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch-operation');
- Route::post('/mistake-book/batch/mark-reviewed', [MistakeBookController::class, 'batchMarkReviewed'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.mark-reviewed');
- Route::post('/mistake-book/batch/mark-mastered', [MistakeBookController::class, 'batchMarkMastered'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.mark-mastered');
- Route::post('/mistake-book/batch/add-to-retry-list', [MistakeBookController::class, 'batchAddToRetryList'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.add-to-retry-list');
- Route::post('/mistake-book/batch/remove-from-retry-list', [MistakeBookController::class, 'batchRemoveFromRetryList'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.remove-from-retry-list');
- Route::post('/mistake-book/batch/set-error-type', [MistakeBookController::class, 'batchSetErrorType'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.set-error-type');
- Route::post('/mistake-book/batch/set-importance', [MistakeBookController::class, 'batchSetImportance'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.set-importance');
- Route::post('/mistake-book/batch/toggle-favorite', [MistakeBookController::class, 'batchToggleFavorite'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.mistake-book.batch.toggle-favorite');
- /*
- |--------------------------------------------------------------------------
- | 知识点掌握情况 API 路由
- |--------------------------------------------------------------------------
- */
- use App\Http\Controllers\Api\KnowledgeMasteryController;
- // 获取学生知识点掌握情况统计
- Route::get('/knowledge-mastery/stats/{studentId}', [KnowledgeMasteryController::class, 'stats'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.knowledge-mastery.stats');
- // 获取学生知识点掌握摘要
- Route::get('/knowledge-mastery/summary/{studentId}', [KnowledgeMasteryController::class, 'summary'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.knowledge-mastery.summary');
- // 获取学生知识点图谱数据
- Route::get('/knowledge-mastery/graph/{studentId}', [KnowledgeMasteryController::class, 'graph'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.knowledge-mastery.graph');
- // 获取学生知识点图谱快照列表
- Route::get('/knowledge-mastery/graph/snapshots/{studentId}', [KnowledgeMasteryController::class, 'graphSnapshots'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.knowledge-mastery.graph.snapshots');
- // 获取学生知识点快照列表(简化路径)
- Route::get('/knowledge-mastery/snapshots/{studentId}', [KnowledgeMasteryController::class, 'snapshots'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.knowledge-mastery.snapshots');
- // 创建知识点掌握度快照
- Route::post('/knowledge-mastery/snapshot/{studentId}', [KnowledgeMasteryController::class, 'createSnapshot'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.knowledge-mastery.snapshot.create');
- /*
- |--------------------------------------------------------------------------
- | 教材管理 API 路由
- |--------------------------------------------------------------------------
- */
- // 获取教材列表(按年级排序)
- Route::get('/textbooks', [TextbookApiController::class, 'index'])
- ->name('api.textbooks.index');
- // 根据年级获取教材
- Route::get('/textbooks/grade/{grade}', [TextbookApiController::class, 'getByGrade'])
- ->name('api.textbooks.by-grade');
- // 获取教材系列列表(必须在 {id} 路由之前定义)
- Route::get('/textbooks/series', [TextbookApiController::class, 'getSeries'])
- ->name('api.textbooks.series');
- // 获取年级枚举
- Route::get('/textbooks/grades', [TextbookApiController::class, 'getGradeEnums'])
- ->name('api.textbooks.grades');
- // 获取单个教材详情
- Route::get('/textbooks/{id}', [TextbookApiController::class, 'show'])
- ->name('api.textbooks.show');
- // 获取教材目录
- Route::get('/textbooks/{id}/catalog', [TextbookApiController::class, 'getCatalog'])
- ->name('api.textbooks.catalog');
- /*
- |--------------------------------------------------------------------------
- | MathRecSys 集成 API 路由
- |--------------------------------------------------------------------------
- */
- use App\Http\Controllers\Api\StudentController;
- // 健康检查
- Route::get('/mathrecsys/health', [StudentController::class, 'checkServiceHealth'])->name('api.mathrecsys.health');
- // 学生相关 API
- Route::prefix('mathrecsys/students')->name('api.mathrecsys.students.')->group(function () {
- // 获取学生完整信息
- Route::get('{studentId}', [StudentController::class, 'show'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->name('show');
- // 获取个性化推荐
- Route::get('{studentId}/recommendations', [StudentController::class, 'getRecommendations'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->name('recommendations');
- // 获取学习轨迹
- Route::get('{studentId}/trajectory', [StudentController::class, 'getTrajectory'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->name('trajectory');
- // 获取学习建议
- Route::get('{studentId}/suggestions', [StudentController::class, 'getSuggestions'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->name('suggestions');
- // 智能分析题目
- Route::post('{studentId}/analyze', [StudentController::class, 'analyzeQuestion'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->name('analyze');
- // 更新掌握度
- Route::put('{studentId}/mastery', [StudentController::class, 'updateMastery'])
- ->where('studentId', '[0-9]+') // 限制为数字
- ->name('update-mastery');
- // 【新增】获取学生知识点掌握详情(直接查询MySQL)
- Route::get('{studentId}/knowledge-points/detail', [StudentKnowledgeController::class, 'getKnowledgePointsDetail'])
- ->where('studentId', '[0-9]+')
- ->name('knowledge-points.detail');
- // 【新增】获取学生知识点层级关系
- Route::get('{studentId}/knowledge-points/hierarchy', [StudentKnowledgeController::class, 'getKnowledgeHierarchy'])
- ->where('studentId', '[0-9]+')
- ->name('knowledge-points.hierarchy');
- });
- // 【前端直连】学生知识点详情API(直接查询MySQL)
- Route::prefix('students')->name('students.')->group(function () {
- Route::get('{studentId}/knowledge-points/detail', [StudentKnowledgeController::class, 'getKnowledgePointsDetail'])
- ->where('studentId', '[0-9]+')
- ->name('knowledge-points.detail');
- Route::get('{studentId}/knowledge-points/hierarchy', [StudentKnowledgeController::class, 'getKnowledgeHierarchy'])
- ->where('studentId', '[0-9]+')
- ->name('knowledge-points.hierarchy');
- });
- // 班级分析 API
- Route::prefix('mathrecsys/classes')->name('api.mathrecsys.classes.')->group(function () {
- Route::get('{classId}/analysis', [StudentController::class, 'classAnalysis'])
- ->where('classId', '[0-9]+') // 限制为数字
- ->name('analysis');
- });
- // 测试 API
- Route::get('/mathrecsys/test', function () {
- return response()->json([
- 'success' => true,
- 'message' => 'MathRecSys API integration is working',
- 'timestamp' => now()->toISOString(),
- ]);
- })->name('api.mathrecsys.test');
- // 测试OCR题目生成API调用
- Route::post('/test-ocr-generation', function () {
- try {
- $service = new \App\Services\QuestionBankService;
- // 模拟前端传递的OCR题目数据
- $questions = [
- [
- 'id' => 1,
- 'content' => '计算:2+3-4',
- ],
- [
- 'id' => 2,
- 'content' => '解方程:x+5=10',
- ],
- ];
- Log::info('开始测试OCR题目生成', [
- 'questions_count' => count($questions),
- 'ocr_record_id' => 12,
- ]);
- // 使用异步API,系统自动生成回调URL
- $response = $service->generateQuestionsFromOcrAsync(
- $questions,
- '高一',
- '数学',
- 12, // OCR记录ID
- null, // 让系统自动生成回调URL
- 'api.ocr.callback' // 回调路由名称
- );
- Log::info('OCR题目生成响应', [
- 'response' => $response,
- 'status' => $response['status'] ?? 'unknown',
- 'task_id' => $response['task_id'] ?? 'N/A',
- ]);
- return response()->json([
- 'success' => true,
- 'message' => 'OCR题目生成测试完成',
- 'data' => $response,
- ]);
- } catch (\Exception $e) {
- Log::error('测试OCR题目生成失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- ]);
- return response()->json([
- 'success' => false,
- 'error' => $e->getMessage(),
- ], 500);
- }
- })->name('api.test.ocr.generation');
- /*
- |--------------------------------------------------------------------------
- | 学生作答分析 API 路由
- |--------------------------------------------------------------------------
- */
- // 提交学生作答结果
- Route::post('/student-answers/analyze', [StudentAnswerAnalysisController::class, 'submitAnswers'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.student-answers.analyze');
- // 查询分析任务状态
- Route::get('/student-answers/analysis/status/{taskId}', [StudentAnswerAnalysisController::class, 'getAnalysisStatus'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.student-answers.analysis.status');
- // 获取学生学习历史
- Route::get('/student-answers/history/{studentId}', [StudentAnswerAnalysisController::class, 'getStudentLearningHistory'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.student-answers.history');
- /*
- |--------------------------------------------------------------------------
- | 考试答题分析 API 路由(步骤级分析)
- |--------------------------------------------------------------------------
- */
- use App\Http\Controllers\Api\ExamAnswerAnalysisController;
- use App\Http\Controllers\Api\HealthCheckController;
- use App\Http\Controllers\Api\PaperSubmitAnalysisController;
- // 分析考试答题数据
- Route::post('/exam-answer-analysis', [ExamAnswerAnalysisController::class, 'analyze'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.exam-answer-analysis.analyze');
- // 获取分析结果
- Route::get('/exam-answer-analysis/{student_id}/{paper_id}', [ExamAnswerAnalysisController::class, 'getAnalysisResult'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->where('student_id', '.*')
- ->where('paper_id', '.*')
- ->name('api.exam-answer-analysis.result');
- // 获取学生历史分析记录
- Route::get('/exam-answer-analysis/history/{student_id}', [ExamAnswerAnalysisController::class, 'getHistory'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->where('student_id', '.*')
- ->name('api.exam-answer-analysis.history');
- // 获取知识点掌握度趋势
- Route::get('/exam-answer-analysis/mastery-trend/{student_id}', [ExamAnswerAnalysisController::class, 'getMasteryTrend'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->where('student_id', '.*')
- ->name('api.exam-answer-analysis.mastery-trend');
- // 获取智能出卷推荐
- Route::get('/exam-answer-analysis/smart-quiz/{student_id}', [ExamAnswerAnalysisController::class, 'getSmartQuizRecommendation'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->where('student_id', '.*')
- ->name('api.exam-answer-analysis.smart-quiz');
- // 导出分析报告
- Route::get('/exam-answer-analysis/export/{student_id}/{paper_id}', [ExamAnswerAnalysisController::class, 'export'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->where('student_id', '.*')
- ->where('paper_id', '.*')
- ->name('api.exam-answer-analysis.export');
- // 批量分析多个学生的考试数据
- Route::post('/exam-answer-analysis/batch', [ExamAnswerAnalysisController::class, 'batchAnalyze'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.exam-answer-analysis.batch');
- Route::get('/tasks/status/{taskId}', function (string $taskId) {
- $task = app(\App\Services\TaskManager::class)->getTaskStatus($taskId);
- if (! $task) {
- return response()->json([
- 'success' => false,
- 'message' => '任务不存在',
- ], 404);
- }
- return response()->json([
- 'success' => true,
- 'data' => $task,
- ]);
- })->name('api.tasks.status');
- /*
- |--------------------------------------------------------------------------
- | 试卷提交分析 API 路由(前端提交答题数据)
- |--------------------------------------------------------------------------
- */
- // 提交试卷答题数据进行分析
- Route::post('/paper-submit-analysis', [PaperSubmitAnalysisController::class, 'analyze'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.paper-submit-analysis.analyze');
- // 获取试卷分析结果
- Route::get('/paper-submit-analysis/{paperId}', [PaperSubmitAnalysisController::class, 'getResult'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.paper-submit-analysis.result');
- /*
- |--------------------------------------------------------------------------
- | 健康检查 API 路由
- |--------------------------------------------------------------------------
- */
- // 检查系统健康状态
- Route::get('/health', [HealthCheckController::class, 'index'])
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.health.index');
- /*
- |--------------------------------------------------------------------------
- | 学生学习进度 API 路由
- |--------------------------------------------------------------------------
- */
- use App\Http\Controllers\Api\QuestionPdfController;
- use App\Http\Controllers\Api\StudentProgressController;
- // 获取单个学生学习进度
- Route::get('/students/{studentId}/learning-progress', [StudentProgressController::class, 'show'])
- ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
- ->name('api.students.learning-progress.show');
- // 批量获取学生学习进度
- Route::post('/students/learning-progress/batch', [StudentProgressController::class, 'batch'])
- ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
- ->name('api.students.learning-progress.batch');
- /*
- |--------------------------------------------------------------------------
- | 题目PDF生成 API 路由
- |--------------------------------------------------------------------------
- */
- // 根据指定题目ID生成PDF
- Route::post('/questions/pdf', [QuestionPdfController::class, 'generate'])
- ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
- ->name('api.questions.pdf.generate');
- /*
- |--------------------------------------------------------------------------
- | 试卷 PDF 重新生成 API 路由
- |--------------------------------------------------------------------------
- */
- // 重新生成统一 PDF(卷子 + 判卷)
- Route::post('/papers/{paper_id}/regenerate', [\App\Http\Controllers\ExamPdfController::class, 'regeneratePdf'])
- ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
- ->where('paper_id', '^paper_\d+$')
- ->name('api.papers.regenerate');
- // 重新生成试卷 PDF(不含答案)
- Route::post('/papers/{paper_id}/regenerate-exam', [\App\Http\Controllers\ExamPdfController::class, 'regenerateExamPdf'])
- ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
- ->where('paper_id', '^paper_\d+$')
- ->name('api.papers.regenerate-exam');
- // 重新生成判卷 PDF(含答案)
- Route::post('/papers/{paper_id}/regenerate-grading', [\App\Http\Controllers\ExamPdfController::class, 'regenerateGradingPdf'])
- ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
- ->where('paper_id', '^paper_\d+$')
- ->name('api.papers.regenerate-grading');
- /*
- |--------------------------------------------------------------------------
- | 以下为旧代码(已迁移到 Controller,保留注释供参考)
- |--------------------------------------------------------------------------
- */
- // [已迁移] 获取学生学习进度
- /*
- Route::get('/students/{studentId}/learning-progress', function (string $studentId) {
- try {
- Log::info('获取学生学习进度', ['student_id' => $studentId]);
- // 1. 获取所有父知识点代码(需要排除的)
- $parentKpCodes = [];
- try {
- $parentCodes = DB::connection('remote_mysql')
- ->table('knowledge_points')
- ->whereNotNull('parent_kp_code')
- ->distinct()
- ->pluck('parent_kp_code')
- ->toArray();
- $parentKpCodes = array_filter($parentCodes);
- } catch (\Exception $e) {
- Log::warning('获取父知识点代码失败', ['error' => $e->getMessage()]);
- }
- // 2. 获取学生所有知识点的掌握度数据(合并两个表)
- $mergedData = [];
- try {
- // 从 student_knowledge_mastery 表获取数据
- $detailedData = DB::connection('remote_mysql')
- ->table('student_knowledge_mastery')
- ->where('student_id', $studentId)
- ->select([
- 'kp_code',
- 'mastery_level',
- 'total_attempts',
- 'correct_attempts',
- 'updated_at'
- ])
- ->get()
- ->toArray();
- foreach ($detailedData as $item) {
- $mergedData[$item->kp_code] = [
- 'kp_code' => $item->kp_code,
- 'mastery_level' => (float) $item->mastery_level,
- 'total_attempts' => $item->total_attempts,
- 'correct_attempts' => $item->correct_attempts,
- 'source_table' => 'student_knowledge_mastery',
- 'updated_at' => $item->updated_at
- ];
- }
- } catch (\Exception $e) {
- Log::warning('从 student_knowledge_mastery 表获取数据失败', [
- 'student_id' => $studentId,
- 'error' => $e->getMessage()
- ]);
- }
- try {
- // 从 student_mastery 表获取数据(补充或覆盖)
- $simpleData = DB::connection('remote_mysql')
- ->table('student_mastery')
- ->where('student_id', $studentId)
- ->select([
- 'kp as kp_code',
- 'mastery',
- 'attempts as total_attempts',
- 'correct as correct_attempts',
- 'updated_at'
- ])
- ->get()
- ->toArray();
- foreach ($simpleData as $item) {
- $kpCode = $item->kp_code;
- $masteryLevel = (float) $item->mastery;
- // 如果已存在,优先使用 mastery_level 更高的数据
- if (isset($mergedData[$kpCode])) {
- if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
- $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
- $mergedData[$kpCode]['source_table'] = 'student_mastery (updated)';
- }
- } else {
- $mergedData[$kpCode] = [
- 'kp_code' => $kpCode,
- 'mastery_level' => $masteryLevel,
- 'total_attempts' => $item->total_attempts ?? 0,
- 'correct_attempts' => $item->correct_attempts ?? 0,
- 'source_table' => 'student_mastery',
- 'updated_at' => $item->updated_at ?? null
- ];
- }
- }
- } catch (\Exception $e) {
- Log::warning('从 student_mastery 表获取数据失败', [
- 'student_id' => $studentId,
- 'error' => $e->getMessage()
- ]);
- }
- // 3. 获取学生所有知识点掌握度数据(只使用student_knowledge_mastery表,因为student_mastery表为空)
- $allMasteryData = collect($mergedData);
- if ($allMasteryData->isEmpty()) {
- return response()->json([
- 'success' => false,
- 'error' => '该学生没有掌握度数据'
- ], 400);
- }
- // 4. 获取知识图谱中所有知识点,找出真正的子知识点(叶子节点)
- $allKps = DB::connection('remote_mysql')
- ->table('knowledge_points')
- ->select(['kp_code', 'parent_kp_code'])
- ->get();
- // 找出叶子节点(没有任何其他知识点以它作为父节点)
- $kpCodes = $allKps->pluck('kp_code')->toArray();
- $parentCodes = $allKps->whereNotNull('parent_kp_code')->pluck('parent_kp_code')->unique()->toArray();
- $leafKpCodes = array_values(array_diff($kpCodes, $parentCodes));
- // 5. 筛选出学生掌握的子知识点数据
- $childMasteryData = $allMasteryData->filter(function($item) use ($leafKpCodes) {
- return in_array($item['kp_code'], $leafKpCodes);
- });
- if ($childMasteryData->isEmpty()) {
- return response()->json([
- 'success' => false,
- 'error' => '该学生没有子知识点的掌握度数据'
- ], 400);
- }
- // 6. 计算分子:学生已掌握的子知识点掌握度总和
- $totalChildMasterySum = $childMasteryData->sum('mastery_level');
- // 7. 计算分母:知识图谱中所有子知识点的最大可能总分
- $maxChildScore = count($leafKpCodes) * 1.0; // 每个子知识点最高1分
- $learningProgress = $maxChildScore > 0 ? ($totalChildMasterySum / $maxChildScore) : 0.0;
- // 8. 统计信息
- $statistics = [
- 'total_knowledge_points' => count($kpCodes),
- 'child_knowledge_points' => count($leafKpCodes),
- 'student_mastered_child_count' => $childMasteryData->count(),
- 'student_mastered_child_percentage' => round(($childMasteryData->count() / count($leafKpCodes)) * 100, 2),
- 'child_mastery_sum' => round($totalChildMasterySum, 4),
- 'max_child_score' => round($maxChildScore, 4),
- 'learning_progress_percentage' => round($learningProgress * 100, 2),
- 'data_source' => implode(', ', $allMasteryData->pluck('source_table')->unique()->toArray()),
- 'child_mastery_max' => round($childMasteryData->max('mastery_level'), 4),
- 'child_mastery_min' => round($childMasteryData->min('mastery_level'), 4),
- 'child_mastery_avg' => round($childMasteryData->avg('mastery_level'), 4),
- ];
- $result = [
- 'student_id' => $studentId,
- 'learning_progress' => round($learningProgress, 6),
- 'learning_progress_percentage' => round($learningProgress * 100, 2),
- 'child_mastery_sum' => round($totalChildMasterySum, 4),
- 'child_knowledge_points' => $childMasteryData->values()->toArray(),
- 'statistics' => $statistics,
- 'calculated_at' => now()->toISOString()
- ];
- Log::info('学生学习进度计算成功', [
- 'student_id' => $studentId,
- 'learning_progress' => $learningProgress,
- 'child_mastery_sum' => $totalChildMasterySum,
- 'child_knowledge_points_count' => count($leafKpCodes),
- 'student_mastered_child_count' => $childMasteryData->count()
- ]);
- return response()->json([
- 'success' => true,
- 'data' => $result,
- 'message' => '学习进度计算成功'
- ]);
- } catch (\Exception $e) {
- Log::error('计算学生学习进度失败', [
- 'student_id' => $studentId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- return response()->json([
- 'success' => false,
- 'message' => '计算学习进度失败: ' . $e->getMessage()
- ], 500);
- }
- })
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.students.learning-progress.get');
- // 批量获取学生学习进度(解决 N+1 问题)
- Route::post('/students/learning-progress/batch', function (\Illuminate\Http\Request $request) {
- try {
- $studentIds = $request->input('student_ids', []);
- if (empty($studentIds)) {
- return response()->json([
- 'success' => false,
- 'error' => 'student_ids 不能为空'
- ], 400);
- }
- if (count($studentIds) > 100) {
- return response()->json([
- 'success' => false,
- 'error' => '单次最多查询 100 个学生'
- ], 400);
- }
- Log::info('批量获取学生学习进度', ['student_ids' => $studentIds, 'count' => count($studentIds)]);
- // 1. 获取知识图谱结构(所有学生共用)
- $allKps = DB::connection('remote_mysql')
- ->table('knowledge_points')
- ->select(['kp_code', 'parent_kp_code'])
- ->get();
- // 找出叶子节点
- $kpCodes = $allKps->pluck('kp_code')->toArray();
- $parentCodes = $allKps->whereNotNull('parent_kp_code')->pluck('parent_kp_code')->unique()->toArray();
- $leafKpCodes = array_values(array_diff($kpCodes, $parentCodes));
- $leafKpCodesSet = array_flip($leafKpCodes); // 用于快速查找
- $maxChildScore = count($leafKpCodes) * 1.0;
- // 2. 批量获取 student_knowledge_mastery 数据
- $detailedData = DB::connection('remote_mysql')
- ->table('student_knowledge_mastery')
- ->whereIn('student_id', $studentIds)
- ->select(['student_id', 'kp_code', 'mastery_level', 'total_attempts', 'correct_attempts', 'updated_at'])
- ->get()
- ->groupBy('student_id');
- // 3. 批量获取 student_mastery 数据
- $simpleData = DB::connection('remote_mysql')
- ->table('student_mastery')
- ->whereIn('student_id', $studentIds)
- ->select(['student_id', 'kp as kp_code', 'mastery', 'attempts as total_attempts', 'correct as correct_attempts', 'updated_at'])
- ->get()
- ->groupBy('student_id');
- // 4. 为每个学生计算学习进度
- $results = [];
- foreach ($studentIds as $studentId) {
- $studentId = (string) $studentId;
- // 合并该学生的掌握度数据
- $mergedData = [];
- // 从 student_knowledge_mastery 获取
- if (isset($detailedData[$studentId])) {
- foreach ($detailedData[$studentId] as $item) {
- $mergedData[$item->kp_code] = [
- 'kp_code' => $item->kp_code,
- 'mastery_level' => (float) $item->mastery_level,
- ];
- }
- }
- // 从 student_mastery 补充或更新
- if (isset($simpleData[$studentId])) {
- foreach ($simpleData[$studentId] as $item) {
- $kpCode = $item->kp_code;
- $masteryLevel = (float) $item->mastery;
- if (isset($mergedData[$kpCode])) {
- if ($masteryLevel > $mergedData[$kpCode]['mastery_level']) {
- $mergedData[$kpCode]['mastery_level'] = $masteryLevel;
- }
- } else {
- $mergedData[$kpCode] = [
- 'kp_code' => $kpCode,
- 'mastery_level' => $masteryLevel,
- ];
- }
- }
- }
- // 计算学习进度
- if (empty($mergedData)) {
- $results[$studentId] = [
- 'student_id' => $studentId,
- 'learning_progress' => 0,
- 'learning_progress_percentage' => 0,
- 'mastered_child_count' => 0,
- 'total_child_count' => count($leafKpCodes),
- 'has_data' => false,
- ];
- continue;
- }
- // 筛选叶子节点并计算
- $childMasterySum = 0;
- $masteredChildCount = 0;
- foreach ($mergedData as $item) {
- if (isset($leafKpCodesSet[$item['kp_code']])) {
- $childMasterySum += $item['mastery_level'];
- $masteredChildCount++;
- }
- }
- $learningProgress = $maxChildScore > 0 ? ($childMasterySum / $maxChildScore) : 0.0;
- $results[$studentId] = [
- 'student_id' => $studentId,
- 'learning_progress' => round($learningProgress, 6),
- 'learning_progress_percentage' => round($learningProgress * 100, 2),
- 'mastered_child_count' => $masteredChildCount,
- 'total_child_count' => count($leafKpCodes),
- 'child_mastery_sum' => round($childMasterySum, 4),
- 'has_data' => true,
- ];
- }
- Log::info('批量学习进度计算完成', ['count' => count($results)]);
- return response()->json([
- 'success' => true,
- 'data' => $results,
- 'meta' => [
- 'total_students' => count($studentIds),
- 'total_child_knowledge_points' => count($leafKpCodes),
- 'calculated_at' => now()->toISOString(),
- ]
- ]);
- } catch (\Exception $e) {
- Log::error('批量计算学习进度失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- return response()->json([
- 'success' => false,
- 'message' => '批量计算学习进度失败: ' . $e->getMessage()
- ], 500);
- }
- })
- ->withoutMiddleware([
- Authenticate::class,
- 'auth',
- 'auth:sanctum',
- 'auth:api',
- ])
- ->name('api.students.learning-progress.batch');
- */
- /*
- |--------------------------------------------------------------------------
- | 注意:知识点详情API已被注释
- |--------------------------------------------------------------------------
- |
- | 由于路由冲突问题,此API暂时被注释。可使用以下替代接口:
- | - api/mathrecsys/students/{studentId}/knowledge-points/detail
- | - api/students/{studentId}/knowledge-points/detail
- |
- */
- // 获取学生知识点掌握度详情(测试版)
- // Route::get('/students/{studentId}/knowledge-points', function (string $studentId) {
- // // 实现代码...
- // })
- // ->withoutMiddleware([
- // Authenticate::class,
- // 'auth',
- // 'auth:sanctum',
- // 'auth:api',
- // ])
- // ->name('api.students.knowledge-points.details');
|