= 2) { return 'choice'; } // 检查是否有"( )"或"( )"括号,这通常是选择题的标志 if (preg_match('/(\s*)|\(\s*\)/', $stem) && (strpos($stem, 'A.') !== false || strpos($stem, 'B.') !== false || strpos($stem, 'C.') !== false || strpos($stem, 'D.') !== false)) { return 'choice'; } } // 2. 根据技能点判断 if (is_array($skills)) { $skillsStr = implode(',', $skills); if (strpos($skillsStr, '选择题') !== false) return 'choice'; if (strpos($skillsStr, '填空题') !== false) return 'fill'; if (strpos($skillsStr, '解答题') !== false) return 'answer'; } // 3. 根据题目已有类型字段判断(作为后备) if (!empty($question['question_type'])) { $type = strtolower(trim($question['question_type'])); if (in_array($type, ['choice', '选择题', 'choice question'])) return 'choice'; if (in_array($type, ['fill', '填空题', 'fill in the blank'])) return 'fill'; if (in_array($type, ['answer', '解答题', 'calculation', '简答题'])) return 'answer'; } if (!empty($question['type'])) { $type = strtolower(trim($question['type'])); if (in_array($type, ['choice', '选择题', 'choice question'])) return 'choice'; if (in_array($type, ['fill', '填空题', 'fill in the blank'])) return 'fill'; if (in_array($type, ['answer', '解答题', 'calculation', '简答题'])) return 'answer'; } // 4. 根据标签判断 if (is_string($tags)) { if (strpos($tags, '选择') !== false || strpos($tags, '选择题') !== false) { return 'choice'; } if (strpos($tags, '填空') !== false || strpos($tags, '填空题') !== false) { return 'fill'; } if (strpos($tags, '解答') !== false || strpos($tags, '简答') !== false || strpos($tags, '证明') !== false) { return 'answer'; } } // 5. 填空题特征:连续下划线或明显的填空括号 if (is_string($stem)) { // 检查填空题特征:连续下划线 if (preg_match('/_{3,}/', $stem) || strpos($stem, '____') !== false) { return 'fill'; } // 空括号填空 if (preg_match('/(\s*)/', $stem) || preg_match('/\(\s*\)/', $stem)) { return 'fill'; } } // 6. 根据题干内容关键词判断 if (is_string($stem)) { // 有证明、解答、计算、求证等关键词的是解答题 if (preg_match('/(证明|求证|解方程|计算:|求解|推导|说明理由)/', $stem)) { return 'answer'; } } // 默认是解答题(更安全的默认值) return 'answer'; } /** * 从题目内容中提取选项 */ private function extractOptions(string $content): array { $options = []; // 1. 尝试匹配多种格式的选项:A. / A、/ A: / A.(中文句点)/ A.(无空格) // 支持格式:A.-1 / A. -1 / A、-1 / A:-1 $pattern = '/([A-D])[\.、:.:]\s*(.+?)(?=\s*[A-D][\.、:.:]|$)/su'; if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $optionText = trim($match[2]); // 移除末尾的换行和空白 $optionText = preg_replace('/\s+$/', '', $optionText); // 清理 LaTeX 格式但保留内容 $optionText = preg_replace('/^\$\$\s*/', '', $optionText); $optionText = preg_replace('/\s*\$\$$/', '', $optionText); if (!empty($optionText)) { $options[] = $optionText; } } } // 2. 如果上面没提取到,尝试按换行分割 if (empty($options)) { $lines = preg_split('/[\r\n]+/', $content); foreach ($lines as $line) { $line = trim($line); if (preg_match('/^([A-D])[\.、:.:]\s*(.+)$/u', $line, $match)) { $optionText = trim($match[2]); if (!empty($optionText)) { $options[] = $optionText; } } } } Log::debug('选项提取结果', [ 'content_preview' => mb_substr($content, 0, 150), 'options_count' => count($options), 'options' => $options ]); return $options; } /** * 分离题干内容和选项 */ private function separateStemAndOptions(string $content): array { // 检测是否有选项(支持多种格式) $hasOptions = preg_match('/[A-D][\.、:.:]/u', $content); if (!$hasOptions) { return [$content, []]; } // 提取选项 $options = $this->extractOptions($content); // 如果提取到选项,分离题干 if (!empty($options)) { // 找到第一个选项的位置,之前的内容是题干 if (preg_match('/^(.+?)(?=[A-D][\.、:.:])/su', $content, $match)) { $stem = trim($match[1]); } else { // 如果正则失败,尝试按位置分割 $stem = $content; foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) { $pos = mb_strpos($content, $marker); if ($pos !== false && $pos > 0) { $stem = trim(mb_substr($content, 0, $pos)); break; } } } // 移除末尾的括号或空白 $stem = preg_replace('/()\s*$/', '', $stem); $stem = trim($stem); return [$stem, $options]; } return [$content, []]; } /** * 根据题型获取默认分数 */ private function getQuestionScore(string $type): int { switch ($type) { case 'choice': return 5; // 选择题5分 case 'fill': return 5; // 填空题5分 case 'answer': return 10; // 解答题10分 default: return 5; } } /** * 获取学生信息 */ private function getStudentInfo(?string $studentId): array { if (!$studentId) { return [ 'name' => '未知学生', 'grade' => '未知年级', 'class' => '未知班级' ]; } try { $student = DB::table('students') ->where('student_id', $studentId) ->first(); if ($student) { return [ 'name' => $student->name ?? $studentId, 'grade' => $student->grade ?? '未知', 'class' => $student->class ?? '未知' ]; } } catch (\Exception $e) { Log::warning('获取学生信息失败', [ 'student_id' => $studentId, 'error' => $e->getMessage() ]); } return [ 'name' => $studentId, 'grade' => '未知', 'class' => '未知' ]; } /** * 为 PDF 预览筛选题目(简化版) */ private function selectBestQuestionsForPdf(array $questions, int $targetCount, string $difficultyCategory): array { if (count($questions) <= $targetCount) { return $questions; } // 1. 按题型分类题目 $categorizedQuestions = [ 'choice' => [], 'fill' => [], 'answer' => [], ]; foreach ($questions as $question) { $type = $this->determineQuestionType($question); if (!isset($categorizedQuestions[$type])) { $type = 'answer'; } $categorizedQuestions[$type][] = $question; } // 2. 默认题型配比 $typeRatio = [ '选择题' => 50, // 50% '填空题' => 30, // 30% '解答题' => 20, // 20% ]; // 3. 根据配比选择题目 $selectedQuestions = []; foreach ($typeRatio as $type => $ratio) { $typeKey = $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer'); $countForType = floor($targetCount * $ratio / 100); if ($countForType > 0 && !empty($categorizedQuestions[$typeKey])) { $availableCount = count($categorizedQuestions[$typeKey]); $takeCount = min($countForType, $availableCount, $targetCount - count($selectedQuestions)); // 随机选择题目 $keys = array_keys($categorizedQuestions[$typeKey]); shuffle($keys); $selectedKeys = array_slice($keys, 0, $takeCount); foreach ($selectedKeys as $key) { $selectedQuestions[] = $categorizedQuestions[$typeKey][$key]; } } } // 4. 如果数量不足,随机补充 while (count($selectedQuestions) < $targetCount) { $randomQuestion = $questions[array_rand($questions)]; if (!in_array($randomQuestion, $selectedQuestions)) { $selectedQuestions[] = $randomQuestion; } } // 5. 限制数量并打乱 shuffle($selectedQuestions); return array_slice($selectedQuestions, 0, $targetCount); } /** * 获取教师信息 */ private function getTeacherInfo(?string $teacherId): array { if (!$teacherId) { return [ 'name' => '未知教师' ]; } try { $teacher = DB::table('teachers') ->where('teacher_id', $teacherId) ->first(); if ($teacher) { return [ 'name' => $teacher->name ?? $teacherId ]; } } catch (\Exception $e) { Log::warning('获取教师信息失败', [ 'teacher_id' => $teacherId, 'error' => $e->getMessage() ]); } return [ 'name' => $teacherId ]; } public function show(Request $request, $paper_id) { // 获取是否显示答案的参数,默认为true $includeAnswer = $request->query('answer', 'true') !== 'false'; // 使用 Eloquent 模型获取试卷数据 $paper = \App\Models\Paper::where('paper_id', $paper_id)->first(); if (!$paper) { // 尝试从缓存中获取生成的试卷数据(用于 demo 试卷) $cached = Cache::get('generated_exam_' . $paper_id); if ($cached) { Log::info('从缓存获取试卷数据', [ 'paper_id' => $paper_id, 'cached_count' => count($cached['questions'] ?? []), 'cached_question_types' => array_column($cached['questions'] ?? [], 'question_type') ]); // 构造临时 Paper 对象 $paper = (object)[ 'paper_id' => $paper_id, 'paper_name' => $cached['paper_name'] ?? 'Demo Paper', 'student_id' => $cached['student_id'] ?? null, 'teacher_id' => $cached['teacher_id'] ?? null, ]; // 对于 demo 试卷,需要检查题目数量并限制为用户要求的数量 $questionsData = $cached['questions'] ?? []; $totalQuestions = $cached['total_questions'] ?? count($questionsData); $difficultyCategory = $cached['difficulty_category'] ?? '中等'; // 为 demo 试卷获取完整的题目详情(包括选项) if (!empty($questionsData)) { $questionBankService = app(QuestionBankService::class); $questionIds = array_column($questionsData, 'id'); $questionsResponse = $questionBankService->getQuestionsByIds($questionIds); $responseData = $questionsResponse['data'] ?? []; if (!empty($responseData)) { $responseDataMap = []; foreach ($responseData as $respQ) { $responseDataMap[$respQ['id']] = $respQ; } // 合并题库数据 $questionsData = array_map(function($q) use ($responseDataMap) { if (isset($responseDataMap[$q['id']])) { $apiData = $responseDataMap[$q['id']]; $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? ''; // 分离题干和选项 list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent); $q['stem'] = $stem; $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? ''; $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? ''; $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? ''; $q['options'] = $apiData['options'] ?? $extractedOptions; // 优先使用API选项,备选提取的选项 } return $q; }, $questionsData); } } if (count($questionsData) > $totalQuestions) { Log::info('PDF预览时发现题目过多,进行筛选', [ 'paper_id' => $paper_id, 'cached_count' => count($questionsData), 'required_count' => $totalQuestions ]); $questionsData = $this->selectBestQuestionsForPdf($questionsData, $totalQuestions, $difficultyCategory); Log::info('筛选后题目数据', [ 'paper_id' => $paper_id, 'filtered_count' => count($questionsData), 'filtered_types' => array_column($questionsData, 'question_type') ]); } } else { abort(404, '试卷未找到'); } } else { // 获取试卷题目 $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id) ->orderBy('question_number') ->get(); Log::info('从数据库获取题目', [ 'paper_id' => $paper_id, 'question_count' => $paperQuestions->count() ]); // 将 paper_questions 表的数据转换为题库格式 $questionsData = []; foreach ($paperQuestions as $pq) { $questionsData[] = [ 'id' => $pq->question_bank_id, 'kp_code' => $pq->knowledge_point, 'question_type' => $pq->question_type ?? 'answer', // 包含题目类型 'stem' => $pq->question_text ?? '题目内容缺失', // 如果有存储题目文本 'solution' => $pq->solution ?? '', // 保存解题思路! 'answer' => $pq->correct_answer ?? '', // 保存正确答案 'difficulty' => $pq->difficulty ?? 0.5, 'score' => $pq->score ?? 5, // 包含已计算的分值 'tags' => '', 'content' => $pq->question_text ?? '', ]; } Log::info('paper_questions表原始数据', [ 'paper_id' => $paper_id, 'sample_questions' => array_slice($questionsData, 0, 3), 'all_types' => array_column($questionsData, 'question_type') ]); // 如果需要完整题目详情(stem等),可以从题库获取 // 但要严格限制只获取这8道题 if (!empty($questionsData)) { $questionBankService = app(QuestionBankService::class); $questionIds = array_column($questionsData, 'id'); $questionsResponse = $questionBankService->getQuestionsByIds($questionIds); $responseData = $questionsResponse['data'] ?? []; // 确保只返回请求的ID对应的题目,并保留数据库中的 question_type if (!empty($responseData)) { // 创建题库返回数据的映射 $responseDataMap = []; foreach ($responseData as $respQ) { $responseDataMap[$respQ['id']] = $respQ; } // 遍历所有数据库中的题目,合并题库返回的数据 $questionsData = array_map(function($q) use ($responseDataMap, $paperQuestions) { // 从题库API获取的详细数据(如果有) if (isset($responseDataMap[$q['id']])) { $apiData = $responseDataMap[$q['id']]; $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失'; // 分离题干和选项 list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent); // 合并数据,优先使用题库API的 stem、answer、solution、options $q['stem'] = $stem; $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? ''; $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? ''; $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? ''; $q['options'] = $apiData['options'] ?? $extractedOptions; // 优先使用API选项,备选提取的选项 } // 从数据库 paper_questions 表中获取 question_type(已在前面设置,这里确保有值) if (!isset($q['question_type']) || empty($q['question_type'])) { $dbQuestion = $paperQuestions->firstWhere('question_bank_id', $q['id']); if ($dbQuestion && $dbQuestion->question_type) { $q['question_type'] = $dbQuestion->question_type; } } return $q; }, $questionsData); } } } // 按题型分类(使用标准的中学数学试卷格式) $questions = ['choice' => [], 'fill' => [], 'answer' => []]; foreach ($questionsData as $q) { // 题库API返回的是 stem 字段,不是 content $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失'; // 分离题干和选项 list($content, $extractedOptions) = $this->separateStemAndOptions($rawContent); // 如果从题库API获取了选项,优先使用 $options = $q['options'] ?? $extractedOptions; $answer = $q['answer'] ?? ''; $solution = $q['solution'] ?? ''; // 优先使用 question_type 字段,如果没有则根据内容智能判断 $type = $q['question_type'] ?? $this->determineQuestionType($q); // 详细调试:记录题目类型判断结果 Log::info('题目类型判断', [ 'question_id' => $q['id'] ?? '', 'has_question_type' => isset($q['question_type']), 'question_type_value' => $q['question_type'] ?? null, 'tags' => $q['tags'] ?? '', 'stem_length' => mb_strlen($content), 'stem_preview' => mb_substr($content, 0, 100), 'has_extracted_options' => !empty($extractedOptions), 'extracted_options_count' => count($extractedOptions), 'has_api_options' => isset($q['options']) && !empty($q['options']), 'api_options_count' => isset($q['options']) ? count($q['options']) : 0, 'final_options_count' => count($options), 'determined_type' => $type ]); if (!isset($questions[$type])) { $type = 'answer'; } $qData = (object)[ 'id' => $q['id'] ?? $q['question_bank_id'] ?? null, 'content' => $content, 'answer' => $answer, 'solution' => $solution, 'difficulty' => $q['difficulty'] ?? 0.5, 'kp_code' => $q['kp_code'] ?? '', 'tags' => $q['tags'] ?? '', 'options' => $options, // 使用分离后的选项 'score' => $q['score'] ?? $this->getQuestionScore($type), // 优先使用生成时分配的分数 ]; $questions[$type][] = $qData; } // 调试:记录最终分类结果 Log::info('最终分类结果', [ 'paper_id' => $paper_id, 'choice_count' => count($questions['choice']), 'fill_count' => count($questions['fill']), 'answer_count' => count($questions['answer']), 'total' => count($questions['choice']) + count($questions['fill']) + count($questions['answer']) ]); // 渲染视图 return view('pdf.exam-paper', [ 'paper' => $paper, 'questions' => $questions, 'student' => $this->getStudentInfo($paper->student_id), 'teacher' => $this->getTeacherInfo($paper->teacher_id), 'includeAnswer' => $includeAnswer ]); } /** * 判卷视图:题目前带方框,题后附“正确答案+解题思路” */ public function showGrading(Request $request, $paper_id) { // 复用现有逻辑获取题目分类 $includeAnswer = true; // 直接调用 show 的前置逻辑(简化复用) $request->merge(['answer' => 'true']); // 复用 show() 内逻辑获取 questions/paper // 为避免重复代码,简单调用 showData 方法(拆分为私有方法?暂直接重用现有方法流程) // 这里直接复制 show 的主体以保持兼容 // 使用 Eloquent 模型获取试卷数据 $paper = \App\Models\Paper::where('paper_id', $paper_id)->first(); if (!$paper) { $cached = Cache::get('generated_exam_' . $paper_id); if (!$cached) { abort(404, '试卷未找到'); } $paper = (object)[ 'paper_id' => $paper_id, 'paper_name' => $cached['paper_name'] ?? 'Demo Paper', 'student_id' => $cached['student_id'] ?? null, 'teacher_id' => $cached['teacher_id'] ?? null, ]; $questionsData = $cached['questions'] ?? []; $totalQuestions = $cached['total_questions'] ?? count($questionsData); $difficultyCategory = $cached['difficulty_category'] ?? '中等'; if (!empty($questionsData)) { $questionBankService = app(QuestionBankService::class); $questionIds = array_column($questionsData, 'id'); $questionsResponse = $questionBankService->getQuestionsByIds($questionIds); $responseData = $questionsResponse['data'] ?? []; if (!empty($responseData)) { $responseDataMap = []; foreach ($responseData as $respQ) { $responseDataMap[$respQ['id']] = $respQ; } $questionsData = array_map(function($q) use ($responseDataMap) { if (isset($responseDataMap[$q['id']])) { $apiData = $responseDataMap[$q['id']]; $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? ''; list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent); $q['stem'] = $stem; $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? ''; $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? ''; $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? ''; $q['options'] = $apiData['options'] ?? $extractedOptions; } return $q; }, $questionsData); } } if (count($questionsData) > $totalQuestions) { $questionsData = $this->selectBestQuestionsForPdf($questionsData, $totalQuestions, $difficultyCategory); } } else { $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id) ->orderBy('question_number') ->get(); $questionsData = []; foreach ($paperQuestions as $pq) { $questionsData[] = [ 'id' => $pq->question_bank_id, 'kp_code' => $pq->knowledge_point, 'question_type' => $pq->question_type ?? 'answer', 'stem' => $pq->question_text ?? '题目内容缺失', 'solution' => $pq->solution ?? '', // 保存解题思路! 'answer' => $pq->correct_answer ?? '', // 保存正确答案 'difficulty' => $pq->difficulty ?? 0.5, 'score' => $pq->score ?? 5, 'tags' => '', 'content' => $pq->question_text ?? '', ]; } if (!empty($questionsData)) { $questionBankService = app(QuestionBankService::class); $questionIds = array_column($questionsData, 'id'); $questionsResponse = $questionBankService->getQuestionsByIds($questionIds); $responseData = $questionsResponse['data'] ?? []; if (!empty($responseData)) { $responseDataMap = []; foreach ($responseData as $respQ) { $responseDataMap[$respQ['id']] = $respQ; } $questionsData = array_map(function($q) use ($responseDataMap, $paperQuestions) { if (isset($responseDataMap[$q['id']])) { $apiData = $responseDataMap[$q['id']]; $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失'; list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent); $q['stem'] = $stem; $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? ''; $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? ''; $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? ''; $q['options'] = $apiData['options'] ?? $extractedOptions; } if (!isset($q['question_type']) || empty($q['question_type'])) { $dbQuestion = $paperQuestions->firstWhere('question_bank_id', $q['id']); if ($dbQuestion && $dbQuestion->question_type) { $q['question_type'] = $dbQuestion->question_type; } } return $q; }, $questionsData); } } } $questions = ['choice' => [], 'fill' => [], 'answer' => []]; foreach ($questionsData as $q) { $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失'; list($content, $extractedOptions) = $this->separateStemAndOptions($rawContent); $options = $q['options'] ?? $extractedOptions; $answer = $q['answer'] ?? ''; $solution = $q['solution'] ?? ''; $type = $q['question_type'] ?? $this->determineQuestionType($q); if (!isset($questions[$type])) { $type = 'answer'; } $qData = (object)[ 'id' => $q['id'] ?? $q['question_bank_id'] ?? null, 'content' => $content, 'answer' => $answer, 'solution' => $solution, 'difficulty' => $q['difficulty'] ?? 0.5, 'kp_code' => $q['kp_code'] ?? '', 'tags' => $q['tags'] ?? '', 'options' => $options, 'score' => $q['score'] ?? $this->getQuestionScore($type), ]; $questions[$type][] = $qData; } return view('pdf.exam-grading', [ 'paper' => $paper, 'questions' => $questions, 'student' => $this->getStudentInfo($paper->student_id), 'teacher' => $this->getTeacherInfo($paper->teacher_id), 'includeAnswer' => true, ]); } }