1, // 筑基 '进阶', '中等', '2' => 2, // 提分 '培优', '3' => 3, // 培优 '竞赛', '4' => 4, // 竞赛 default => 2, // 默认中级(提分) }; } /** * 获取试卷的基准难度 */ private function getExamBaseDifficulty(string $paperId): int { try { // 从papers表获取difficulty_category $paper = DB::table('papers') ->where('paper_id', $paperId) ->first(); if (!$paper || empty($paper->difficulty_category)) { Log::warning('未找到试卷或难度分类,使用默认难度', [ 'paper_id' => $paperId, 'difficulty_category' => $paper->difficulty_category ?? null, ]); return 2; // 默认中级(提分) } $difficultyLevel = $this->mapDifficultyCategoryToLevel($paper->difficulty_category); Log::info('获取试卷基准难度', [ 'paper_id' => $paperId, 'difficulty_category' => $paper->difficulty_category, 'difficulty_level' => $difficultyLevel, ]); return $difficultyLevel; } catch (\Exception $e) { Log::error('获取试卷基准难度失败,使用默认难度', [ 'paper_id' => $paperId, 'error' => $e->getMessage(), ]); return 2; // 默认中级(提分) } } /** * 更新父节点掌握度(基于子节点平均值) */ private function updateParentMastery(string $studentId, string $kpCode): void { try { // 获取知识点的父节点 $knowledgePoint = DB::table('knowledge_points') ->where('kp_code', $kpCode) ->first(); if (!$knowledgePoint || empty($knowledgePoint->parent_kp_code)) { return; // 没有父节点,无需更新 } $parentKpCode = $knowledgePoint->parent_kp_code; // 使用MasteryCalculator计算父节点掌握度 $parentMastery = $this->masteryCalculator->calculateParentMastery($studentId, $parentKpCode); // 保存到数据库(作为独立记录) DB::table('student_knowledge_mastery') ->updateOrInsert( ['student_id' => $studentId, 'kp_code' => $parentKpCode], [ 'mastery_level' => $parentMastery, 'confidence_level' => 0.8, // 父节点置信度默认较高 'total_attempts' => DB::raw('COALESCE(total_attempts, 0)'), 'correct_attempts' => DB::raw('COALESCE(correct_attempts, 0)'), 'mastery_trend' => 'calculated', // 标记为计算得出,非直接测量 'last_mastery_update' => now(), 'updated_at' => now(), 'notes' => '父节点掌握度(基于子节点平均值计算)', ] ); Log::info('父节点掌握度已更新', [ 'student_id' => $studentId, 'child_kp_code' => $kpCode, 'parent_kp_code' => $parentKpCode, 'parent_mastery' => $parentMastery, ]); // 递归更新更上层的父节点 $this->updateParentMastery($studentId, $parentKpCode); } catch (\Exception $e) { Log::error('更新父节点掌握度失败', [ 'student_id' => $studentId, 'kp_code' => $kpCode, 'error' => $e->getMessage(), ]); } } /** * 接收学生作答结果并进行分析 * * @param Request $request * @return JsonResponse */ public function submitAnswers(Request $request): JsonResponse { // 优先从JSON body获取参数,支持向后兼容 $payload = $request->json()->all(); if (empty($payload)) { $payload = $request->all(); } // student_id类型转换:支持数字和字符串输入 if (isset($payload['student_id'])) { $payload['student_id'] = (string) $payload['student_id']; } // 【修复】验证参数 - 支持questions数组和is_correct数组格式 $validator = validator($payload, [ 'paper_id' => 'required|string', 'student_id' => 'required|string|min:1', 'questions' => 'required|array', 'questions.*.question_bank_id' => 'required|integer', 'questions.*.student_answer' => 'nullable|string', 'questions.*.is_correct' => 'required|array', // 支持数组格式(多小题/步骤) 'questions.*.teacher_comment' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => '参数错误', 'errors' => $validator->errors(), ], 422); } $data = $validator->validated(); try { // 使用TaskManager创建异步任务 $taskId = $this->taskManager->createTask( TaskManager::TASK_TYPE_ANALYSIS, array_merge($data, ['type' => 'answer_analysis']) ); Log::info('StudentAnswerAnalysisController: 收到作答结果', [ 'task_id' => $taskId, 'paper_id' => $data['paper_id'], 'student_id' => $data['student_id'], 'question_count' => count($data['questions']), ]); // 触发后台分析处理 $this->processAnswerAnalysis($taskId, $data); return response()->json([ 'success' => true, 'message' => '作答结果已提交,正在分析中...', 'data' => [ 'task_id' => $taskId, 'paper_id' => $data['paper_id'], 'student_id' => $data['student_id'], 'status' => 'processing', 'created_at' => now()->toISOString(), ], ]); } catch (\Exception $e) { Log::error('提交作答结果失败', [ 'paper_id' => $data['paper_id'] ?? 'unknown', 'student_id' => $data['student_id'] ?? 'unknown', 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => '提交失败:' . $e->getMessage(), ], 500); } } /** * 查询分析任务状态 */ public function getAnalysisStatus(string $taskId): JsonResponse { try { $task = $this->taskManager->getTaskStatus($taskId); if (!$task) { return response()->json([ 'success' => false, 'message' => '任务不存在', ], 404); } return response()->json([ 'success' => true, 'data' => $task, ]); } catch (\Exception $e) { Log::error('查询分析状态失败', [ 'task_id' => $taskId, 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => '查询失败:' . $e->getMessage(), ], 500); } } /** * 处理作答分析(后台任务) */ private function processAnswerAnalysis(string $taskId, array $data): void { try { $this->taskManager->updateTaskProgress($taskId, 10, '正在处理缺题(默认正确)...'); // 处理缺题:对于没有提交的题目,默认标记为正确 $allAnswers = $this->processMissingQuestions($data); $this->taskManager->updateTaskProgress($taskId, 30, '正在保存作答记录...'); // 保存作答记录到数据库(包含缺题) $answerRecord = $this->answerAnalysisService->saveAnswerRecord([ ...$data, 'answers' => $allAnswers, ]); $this->taskManager->updateTaskProgress($taskId, 50, '正在分析每道题(包括缺题处理)...'); // 简化分析:不调用AI,直接使用基础分析 $questionAnalyses = []; foreach ($allAnswers as $answer) { $questionAnalyses[] = [ 'question_id' => $answer['question_id'], 'question_number' => $answer['question_number'] ?? null, 'kp_code' => $answer['knowledge_point'] ?? null, 'student_answer' => $answer['student_answer'] ?? '', 'correct_answer' => $answer['correct_answer'] ?? '', 'is_correct' => $answer['is_correct'], 'score_obtained' => (float) ($answer['score'] ?? 0), 'max_score' => (float) ($answer['max_score'] ?? 10), 'difficulty' => 0.5, 'is_missing' => $answer['is_missing'] ?? false, 'model_used' => 'simple-rules', ]; } $this->taskManager->updateTaskProgress($taskId, 60, '正在计算掌握度(包括子知识点动态加减和父节点平均)...'); // 【新算法】使用MasteryCalculator计算掌握度 // 1. 获取学案基准难度 $examBaseDifficulty = $this->getExamBaseDifficulty($data['paper_id']); // 2. 为每个知识点计算掌握度 $masteryResults = []; foreach ($questionAnalyses as $analysis) { if (empty($analysis['kp_code']) || $analysis['is_missing']) { continue; // 跳过没有知识点或缺题的题目 } $kpCode = $analysis['kp_code']; if (!isset($masteryResults[$kpCode])) { $masteryResults[$kpCode] = []; } $masteryResults[$kpCode][] = [ 'question_id' => $analysis['question_id'], 'is_correct' => $analysis['is_correct'], 'question_difficulty' => $analysis['difficulty'] ?? 0.5, ]; } // 3. 调用MasteryCalculator计算每个知识点的掌握度 foreach ($masteryResults as $kpCode => $attempts) { $this->masteryCalculator->calculateMasteryLevel( $data['student_id'], $kpCode, $attempts, $examBaseDifficulty ); // 4. 计算父节点掌握度(子节点平均值) $this->updateParentMastery($data['student_id'], $kpCode); } $this->taskManager->updateTaskProgress($taskId, 65, '正在保存分析结果...'); // 准备分析结果数据 $analysisData = [ 'question_results' => $questionAnalyses, 'total_questions' => count($questionAnalyses), 'correct_count' => count(array_filter($questionAnalyses, function($q) { return $q['is_correct'] ?? false; })), 'wrong_count' => count(array_filter($questionAnalyses, function($q) { return !($q['is_correct'] ?? true); })), 'model_used' => $questionAnalyses[0]['model_used'] ?? 'unknown', 'exam_base_difficulty' => $examBaseDifficulty, ]; // 保存分析结果 $this->answerAnalysisService->saveAnalysisResults($answerRecord, $analysisData, $questionAnalyses); $this->taskManager->updateTaskProgress($taskId, 80, '正在生成掌握度快照...'); // 生成掌握度快照(记录每次分析的掌握度变化) $masterySnapshot = $this->answerAnalysisService->createMasterySnapshot( $data['student_id'], $data['paper_id'], $answerRecord['record_id'] ); $this->taskManager->updateTaskProgress($taskId, 90, '正在生成学情分析报告...'); // 生成学情分析报告PDF $reportUrl = $this->generateLearningReport($taskId, $data, $answerRecord, $questionAnalyses, $masterySnapshot); // 标记任务完成 $this->taskManager->markTaskCompleted($taskId, [ 'answer_record_id' => $answerRecord['record_id'], 'analysis_id' => 'analysis_' . uniqid(), 'mastery_snapshot_id' => $masterySnapshot['snapshot_id'] ?? null, 'correct_count' => $answerRecord['correct_count'], 'wrong_count' => $answerRecord['wrong_count'], 'overall_mastery' => $masterySnapshot['overall_mastery'] ?? null, // 'report_url' => $reportUrl, // 临时禁用PDF报告 ]); Log::info('作答分析完成', [ 'task_id' => $taskId, 'paper_id' => $data['paper_id'], 'student_id' => $data['student_id'], 'answer_record_id' => $answerRecord['record_id'], ]); // 发送回调通知 $this->taskManager->sendCallback($taskId); } catch (\Exception $e) { Log::error('作答分析失败', [ 'task_id' => $taskId, 'paper_id' => $data['paper_id'], 'student_id' => $data['student_id'], 'error' => $e->getMessage(), ]); $this->taskManager->markTaskFailed($taskId, $e->getMessage()); } } /** * 获取题目文本内容 */ private function getQuestionText(string $questionId): string { try { // 这里可以调用 QuestionBankService 获取题目内容 // 目前返回空字符串,让AI分析基于学生答案进行分析 return ''; } catch (\Exception $e) { Log::warning('获取题目文本失败', [ 'question_id' => $questionId, 'error' => $e->getMessage(), ]); return ''; } } /** * 处理缺题逻辑:对于没有提交的题目,默认标记为正确 * 通过paper_id查询paper_question表获取总题数 */ private function processMissingQuestions(array $data): array { // 【修复】处理questions数组格式 $questions = $data['questions'] ?? []; $submittedQuestionIds = array_column($questions, 'question_bank_id'); // 将questions转换为answers格式以便后续处理 $answers = []; foreach ($questions as $q) { // 计算is_correct数组的平均值(判断整体是否正确) $isCorrectArray = $q['is_correct'] ?? []; $correctSteps = array_sum($isCorrectArray); $totalSteps = count($isCorrectArray); $isOverallCorrect = $totalSteps > 0 && ($correctSteps / $totalSteps) >= 0.6; // 60%以上步骤正确视为正确 $answers[] = [ 'question_id' => $q['question_bank_id'], 'question_number' => null, 'is_correct' => $isOverallCorrect, 'student_answer' => $q['student_answer'] ?? '', 'correct_answer' => '', 'score' => $correctSteps * 2, // 假设每个步骤2分 'max_score' => $totalSteps * 2, 'knowledge_point' => null, 'question_type' => 'mixed', 'is_missing' => false, 'is_correct_array' => $isCorrectArray, // 保留原始数组 'answer_time' => $data['answer_time'] ?? now(), ]; } // 获取缺题列表 $missingQuestions = $data['missing_questions'] ?? []; // 如果没有提供missing_questions,通过paper_id查询paper_question表 if (empty($missingQuestions)) { try { // 通过paper_id查询paper_question表获取题目总数 $totalQuestions = DB::table('paper_questions') ->where('paper_id', $data['paper_id']) ->count(); Log::info('从paper_question表获取题目总数', [ 'paper_id' => $data['paper_id'], 'total_questions' => $totalQuestions, 'submitted_count' => count($submittedQuestionIds), ]); // 自动生成缺题列表(根据paper_question表的题目编号) $allQuestionIds = DB::table('paper_questions') ->where('paper_id', $data['paper_id']) ->pluck('question_id') ->toArray(); foreach ($allQuestionIds as $questionId) { if (!in_array($questionId, $submittedQuestionIds)) { $missingQuestions[] = $questionId; } } } catch (\Exception $e) { Log::warning('查询paper_question表失败,跳过缺题处理', [ 'paper_id' => $data['paper_id'], 'error' => $e->getMessage(), ]); } } // 【修复】处理缺题和学生未作答的题目 foreach ($missingQuestions as $missingQuestionId) { // 使用 Model 获取缺题的详细信息 $questionDetails = \App\Models\PaperQuestion::where('paper_id', $data['paper_id']) ->where('question_id', $missingQuestionId) ->first(); // 获取知识点和分数信息 $kpCode = $questionDetails?->knowledge_point; if (empty($kpCode)) { // 【修复】不允许使用默认知识点,必须明确指定 Log::warning('StudentAnswerAnalysisController: 缺题缺少知识点信息,跳过掌握度计算', [ 'question_id' => $missingQuestionId, 'paper_id' => $data['paper_id'], ]); continue; // 跳过该缺题,不参与掌握度计算 } $maxScore = floatval($questionDetails?->score ?? 10); $questionType = $questionDetails?->question_type ?? 'missing'; // 【关键】区分缺题和学生未作答: // 1. 缺题(API请求中标记的):按正确计算,100%得分 // 2. 学生没作答的题目:按30%得分率计算 $isTrulyMissing = in_array($missingQuestionId, $data['missing_questions'] ?? []); $scoreRate = $isTrulyMissing ? 1.0 : 0.3; // 缺题100%,未作答30% $score = $maxScore * $scoreRate; $isCorrect = ($scoreRate >= 0.6); // 按60%及格线判断 $answers[] = [ 'question_id' => $missingQuestionId, 'question_number' => $questionDetails?->question_number ?? $missingQuestionId, 'is_correct' => $isCorrect, 'student_answer' => '[缺题]', 'correct_answer' => '[未作答]', 'score' => $score, 'max_score' => $maxScore, 'knowledge_point' => $kpCode, // 保留知识点信息,参与掌握度计算 'question_type' => $questionType, 'is_missing' => $isTrulyMissing, // 真正缺题标记 'missing_note' => $isTrulyMissing ? '缺题,按100%得分率计算' : '学生未作答,按30%得分率计算掌握度', 'answer_time' => $data['answer_time'] ?? now(), ]; } // 【新增】处理已提交但学生未作答的题目(student_answer为空或null) foreach ($answers as &$answer) { if (!empty($answer['student_answer']) && $answer['student_answer'] !== '[缺题]') { continue; // 已作答的题目跳过 } // 学生未作答的题目,按30%得分率计算 if (empty($answer['student_answer']) || $answer['student_answer'] === '') { $maxScore = floatval($answer['max_score'] ?? 10); $scoreRate = 0.3; $score = $maxScore * $scoreRate; $answer['score'] = $score; $answer['is_correct'] = false; // 未作答视为错误 $answer['missing_note'] = '学生未作答,按30%得分率计算掌握度'; } } Log::info('缺题处理完成', [ 'paper_id' => $data['paper_id'], 'submitted_count' => count($data['questions'] ?? []), 'missing_count' => count($missingQuestions), 'total_count' => count($answers), ]); return $answers; } /** * 生成学情分析报告并异步生成PDF */ private function generateLearningReport( string $taskId, array $data, array $answerRecord, array $questionAnalyses, ?array $masterySnapshot ): ?string { try { // 构建报告数据 $reportData = [ 'task_id' => $taskId, 'paper_id' => $data['paper_id'], 'student_id' => $data['student_id'], 'submit_time' => now()->toISOString(), 'answer_record' => $answerRecord, 'question_analyses' => $questionAnalyses, 'mastery_snapshot' => $masterySnapshot, 'report_type' => 'learning_analysis', ]; // 创建异步任务生成PDF $pdfTaskId = $this->taskManager->createTask( TaskManager::TASK_TYPE_PDF, array_merge($reportData, ['type' => 'learning_report']) ); Log::info('学情分析报告任务已创建', [ 'pdf_task_id' => $pdfTaskId, 'paper_id' => $data['paper_id'], 'student_id' => $data['student_id'], ]); // 返回报告URL(异步生成) return route('api.reports.learning', [ 'task_id' => $pdfTaskId, 'student_id' => $data['student_id'], ]); } catch (\Exception $e) { Log::error('生成学情分析报告失败', [ 'task_id' => $taskId, 'error' => $e->getMessage(), ]); return null; } } /** * 获取学生学习历史 */ public function getStudentLearningHistory(string $studentId): JsonResponse { try { $history = $this->answerAnalysisService->getStudentLearningHistory($studentId); return response()->json([ 'success' => true, 'data' => $history, ]); } catch (\Exception $e) { Log::error('获取学习历史失败', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => '获取失败:' . $e->getMessage(), ], 500); } } }