{}]/', $stem)) { return 'choice'; } } // 4. 根据题干内容判断 - 填空题(有下划线) if (is_string($stem) && (strpos($stem, '____') !== false || strpos($stem, '______') !== false || strpos($stem, '______') !== false)) { return 'fill'; } // 5. 根据题干内容判断(更精确的启发式) if (is_string($stem)) { // 有证明、解答、计算、化简等明确关键词的是解答题 if (preg_match('/证明|求解|计算|化简|求证|分析|解答|画出|解方程|不等式/', $stem)) { return 'answer'; } // 短题目且包含"下列"可能是选择题(但要谨慎) if (preg_match('/下列/', $stem) && mb_strlen($stem) < 80) { // 如果没有运算符号,更可能是选择题 if (!preg_match('/[+\-*/=<>{}]/', $stem)) { return 'choice'; } } } // 默认是解答题 return 'answer'; } /** * 从题目内容中提取选项 */ private function extractOptions(string $content): array { // 匹配 A. B. C. D. 格式的选项 if (preg_match_all('/([A-D])\.\s*(.+?)(?=[A-D]\.|$)/s', $content, $matches, PREG_SET_ORDER)) { $options = []; foreach ($matches as $match) { $optionText = trim($match[2]); // 移除末尾的换行和空白 $optionText = preg_replace('/\s+$/', '', $optionText); $options[] = $optionText; } return $options; } return []; } /** * 分离题干内容和选项 */ private function separateStemAndOptions(string $content): array { // 如果没有选项,直接返回 if (!preg_match('/[A-D]\.\s+/m', $content)) { return [$content, []]; } // 提取选项 $options = $this->extractOptions($content); // 提取题干(选项前的部分) $stem = preg_replace('/[A-D]\.\s+.+?(?=[A-D]\.|$)/s', '', $content); $stem = trim($stem); // 移除末尾的括号或空白 $stem = preg_replace('/()\s*$/', '', $stem); $stem = trim($stem); return [$stem, $options]; } /** * 根据题型获取默认分数 */ 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) { // 使用 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 ?? '题目内容缺失', // 如果有存储题目文本 'difficulty' => $pq->difficulty ?? 0.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' => $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) ]); } }