Quellcode durchsuchen

fix:修改多出组卷样式

yemeishu vor 2 Wochen
Ursprung
Commit
87b9a5575c

+ 279 - 88
app/Http/Controllers/ExamPdfController.php

@@ -4,8 +4,8 @@ namespace App\Http\Controllers;
 
 use App\Services\QuestionBankService;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
 class ExamPdfController extends Controller
@@ -24,7 +24,7 @@ class ExamPdfController extends Controller
         }
 
         // 如果是对象格式 {"A": "值1", "B": "值2", ...}
-        if (is_array($options) && !isset($options[0])) {
+        if (is_array($options) && ! isset($options[0])) {
             return array_values($options);
         }
 
@@ -42,6 +42,7 @@ class ExamPdfController extends Controller
                     $normalized[] = is_array($opt) ? (string) reset($opt) : (string) $opt;
                 }
             }
+
             return $normalized;
         }
 
@@ -88,24 +89,42 @@ class ExamPdfController extends Controller
         // 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';
+            if (strpos($skillsStr, '选择题') !== false) {
+                return 'choice';
+            }
+            if (strpos($skillsStr, '填空题') !== false) {
+                return 'fill';
+            }
+            if (strpos($skillsStr, '解答题') !== false) {
+                return 'answer';
+            }
         }
 
         // 3. 根据题目已有类型字段判断(作为后备)
-        if (!empty($question['question_type'])) {
+        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 (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'])) {
+        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';
+            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. 根据标签判断
@@ -205,7 +224,7 @@ class ExamPdfController extends Controller
                 // 清理 LaTeX 格式但保留内容
                 $optionText = preg_replace('/^\$\$\s*/', '', $optionText);
                 $optionText = preg_replace('/\s*\$\$$/', '', $optionText);
-                if (!empty($optionText)) {
+                if (! empty($optionText)) {
                     $options[] = $optionText;
                 }
             }
@@ -219,7 +238,7 @@ class ExamPdfController extends Controller
                 // 【修复】行首匹配选项标记
                 if (preg_match('/^([A-D])[\.、:.:]\s*(.+)$/u', $line, $match)) {
                     $optionText = trim($match[2]);
-                    if (!empty($optionText)) {
+                    if (! empty($optionText)) {
                         $options[] = $optionText;
                     }
                 }
@@ -229,7 +248,7 @@ class ExamPdfController extends Controller
         Log::debug('选项提取结果', [
             'content_preview' => mb_substr($content, 0, 150),
             'options_count' => count($options),
-            'options' => $options
+            'options' => $options,
         ]);
 
         return $options;
@@ -246,7 +265,7 @@ class ExamPdfController extends Controller
         // 【修复】检测是否有选项时,要求选项标记在行首或空白后
         $hasOptions = preg_match('/(?:^|\s)[A-D][\.、:.:]/u', $contentWithoutSvg);
 
-        if (!$hasOptions) {
+        if (! $hasOptions) {
             return [$content, []];
         }
 
@@ -254,7 +273,7 @@ class ExamPdfController extends Controller
         $options = $this->extractOptions($content);
 
         // 如果提取到选项,分离题干
-        if (!empty($options)) {
+        if (! empty($options)) {
             // 【修复】找到第一个选项的位置,要求选项标记在行首或空白后
             if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $contentWithoutSvg, $match)) {
                 $stem = trim($match[1]);
@@ -265,7 +284,7 @@ class ExamPdfController extends Controller
                     // 使用更精确的方法:找到第一个有效选项标记的位置
                     foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) {
                         // 只匹配在空白后的选项标记
-                        if (preg_match('/\s' . preg_quote($marker, '/') . '/', $content, $m, PREG_OFFSET_CAPTURE)) {
+                        if (preg_match('/\s'.preg_quote($marker, '/').'/', $content, $m, PREG_OFFSET_CAPTURE)) {
                             $pos = $m[0][1];
                             if ($pos > 0) {
                                 $stem = trim(mb_substr($content, 0, $pos));
@@ -279,7 +298,7 @@ class ExamPdfController extends Controller
                 $stem = $content;
                 foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) {
                     // 【修复】只匹配在空白后的选项标记
-                    if (preg_match('/\s' . preg_quote($marker, '/') . '/', $content, $m, PREG_OFFSET_CAPTURE)) {
+                    if (preg_match('/\s'.preg_quote($marker, '/').'/', $content, $m, PREG_OFFSET_CAPTURE)) {
                         $pos = $m[0][1];
                         if ($pos > 0) {
                             $stem = trim(mb_substr($content, 0, $pos));
@@ -321,11 +340,11 @@ class ExamPdfController extends Controller
      */
     private function getStudentInfo(?string $studentId): array
     {
-        if (!$studentId) {
+        if (! $studentId) {
             return [
                 'name' => '未知学生',
                 'grade' => '未知年级',
-                'class' => '未知班级'
+                'class' => '未知班级',
             ];
         }
 
@@ -338,20 +357,20 @@ class ExamPdfController extends Controller
                 return [
                     'name' => $student->name ?? $studentId,
                     'grade' => $student->grade ?? '未知',
-                    'class' => $student->class ?? '未知'
+                    'class' => $student->class ?? '未知',
                 ];
             }
         } catch (\Exception $e) {
             Log::warning('获取学生信息失败', [
                 'student_id' => $studentId,
-                'error' => $e->getMessage()
+                'error' => $e->getMessage(),
             ]);
         }
 
         return [
             'name' => $studentId,
             'grade' => '未知',
-            'class' => '未知'
+            'class' => '未知',
         ];
     }
 
@@ -373,7 +392,7 @@ class ExamPdfController extends Controller
 
         foreach ($questions as $question) {
             $type = $this->determineQuestionType($question);
-            if (!isset($categorizedQuestions[$type])) {
+            if (! isset($categorizedQuestions[$type])) {
                 $type = 'answer';
             }
             $categorizedQuestions[$type][] = $question;
@@ -392,7 +411,7 @@ class ExamPdfController extends Controller
             $typeKey = $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer');
             $countForType = floor($targetCount * $ratio / 100);
 
-            if ($countForType > 0 && !empty($categorizedQuestions[$typeKey])) {
+            if ($countForType > 0 && ! empty($categorizedQuestions[$typeKey])) {
                 $availableCount = count($categorizedQuestions[$typeKey]);
                 $takeCount = min($countForType, $availableCount, $targetCount - count($selectedQuestions));
 
@@ -410,13 +429,14 @@ class ExamPdfController extends Controller
         // 4. 如果数量不足,随机补充
         while (count($selectedQuestions) < $targetCount) {
             $randomQuestion = $questions[array_rand($questions)];
-            if (!in_array($randomQuestion, $selectedQuestions)) {
+            if (! in_array($randomQuestion, $selectedQuestions)) {
                 $selectedQuestions[] = $randomQuestion;
             }
         }
 
         // 5. 限制数量并打乱
         shuffle($selectedQuestions);
+
         return array_slice($selectedQuestions, 0, $targetCount);
     }
 
@@ -425,9 +445,9 @@ class ExamPdfController extends Controller
      */
     private function getTeacherInfo(?string $teacherId): array
     {
-        if (!$teacherId) {
+        if (! $teacherId) {
             return [
-                'name' => '未知教师'
+                'name' => '未知教师',
             ];
         }
 
@@ -438,18 +458,18 @@ class ExamPdfController extends Controller
 
             if ($teacher) {
                 return [
-                    'name' => $teacher->name ?? $teacherId
+                    'name' => $teacher->name ?? $teacherId,
                 ];
             }
         } catch (\Exception $e) {
             Log::warning('获取教师信息失败', [
                 'teacher_id' => $teacherId,
-                'error' => $e->getMessage()
+                'error' => $e->getMessage(),
             ]);
         }
 
         return [
-            'name' => $teacherId
+            'name' => $teacherId,
         ];
     }
 
@@ -460,18 +480,18 @@ class ExamPdfController extends Controller
         // 使用 Eloquent 模型获取试卷数据
         $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
 
-        if (!$paper) {
+        if (! $paper) {
             // 尝试从缓存中获取生成的试卷数据(用于 demo 试卷)
-            $cached = Cache::get('generated_exam_' . $paper_id);
+            $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')
+                    'cached_question_types' => array_column($cached['questions'] ?? [], 'question_type'),
                 ]);
 
                 // 构造临时 Paper 对象
-                $paper = (object)[
+                $paper = (object) [
                     'paper_id' => $paper_id,
                     'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
                     'student_id' => $cached['student_id'] ?? null,
@@ -484,26 +504,26 @@ class ExamPdfController extends Controller
                 $difficultyCategory = $cached['difficulty_category'] ?? '中等';
 
                 // 为 demo 试卷获取完整的题目详情(包括选项)
-                if (!empty($questionsData)) {
+                if (! empty($questionsData)) {
                     $questionBankService = app(QuestionBankService::class);
                     $questionIds = array_column($questionsData, 'id');
                     $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
                     $responseData = $questionsResponse['data'] ?? [];
 
-                    if (!empty($responseData)) {
+                    if (! empty($responseData)) {
                         $responseDataMap = [];
                         foreach ($responseData as $respQ) {
                             $responseDataMap[$respQ['id']] = $respQ;
                         }
 
                         // 合并题库数据
-                        $questionsData = array_map(function($q) use ($responseDataMap) {
+                        $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);
+                                [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
 
                                 $q['stem'] = $stem;
                                 $q['content'] = $stem; // 同时设置content字段
@@ -514,23 +534,24 @@ class ExamPdfController extends Controller
                                 // 优先使用API选项,支持多种数据格式
                                 $apiOptions = $apiData['options'] ?? null;
 
-                                if (!empty($apiOptions)) {
+                                if (! empty($apiOptions)) {
                                     // 标准化options格式为数组值列表
                                     $q['options'] = $this->normalizeOptions($apiOptions);
                                     Log::debug('使用标准化API options', [
                                         'question_id' => $q['id'],
                                         'raw_options' => $apiOptions,
-                                        'normalized_options' => $q['options']
+                                        'normalized_options' => $q['options'],
                                     ]);
                                 } else {
                                     // 备选:从题干中提取的选项
                                     $q['options'] = $extractedOptions;
                                     Log::debug('使用提取的options', [
                                         'question_id' => $q['id'],
-                                        'extracted_options' => $extractedOptions
+                                        'extracted_options' => $extractedOptions,
                                     ]);
                                 }
                             }
+
                             return $q;
                         }, $questionsData);
                     }
@@ -540,13 +561,13 @@ class ExamPdfController extends Controller
                     Log::info('PDF预览时发现题目过多,进行筛选', [
                         'paper_id' => $paper_id,
                         'cached_count' => count($questionsData),
-                        'required_count' => $totalQuestions
+                        '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')
+                        'filtered_types' => array_column($questionsData, 'question_type'),
                     ]);
                 }
             } else {
@@ -560,7 +581,7 @@ class ExamPdfController extends Controller
 
             Log::info('从数据库获取题目', [
                 'paper_id' => $paper_id,
-                'question_count' => $paperQuestions->count()
+                'question_count' => $paperQuestions->count(),
             ]);
 
             // 将 paper_questions 表的数据转换为题库格式
@@ -584,12 +605,12 @@ class ExamPdfController extends Controller
             Log::info('paper_questions表原始数据', [
                 'paper_id' => $paper_id,
                 'sample_questions' => array_slice($questionsData, 0, 3),
-                'all_types' => array_column($questionsData, 'question_type')
+                'all_types' => array_column($questionsData, 'question_type'),
             ]);
 
             // 如果需要完整题目详情(stem等),可以从题库获取
             // 但要严格限制只获取这8道题
-            if (!empty($questionsData)) {
+            if (! empty($questionsData)) {
                 $questionBankService = app(QuestionBankService::class);
                 $questionIds = array_column($questionsData, 'id');
 
@@ -597,7 +618,7 @@ class ExamPdfController extends Controller
                 $responseData = $questionsResponse['data'] ?? [];
 
                 // 确保只返回请求的ID对应的题目,并保留数据库中的 question_type
-                if (!empty($responseData)) {
+                if (! empty($responseData)) {
                     // 创建题库返回数据的映射
                     $responseDataMap = [];
                     foreach ($responseData as $respQ) {
@@ -605,14 +626,14 @@ class ExamPdfController extends Controller
                     }
 
                     // 遍历所有数据库中的题目,合并题库返回的数据
-                    $questionsData = array_map(function($q) use ($responseDataMap, $paperQuestions) {
+                    $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);
+                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
 
                             // 合并数据,优先使用题库API的 stem、answer、solution、options
                             $q['stem'] = $stem;
@@ -624,26 +645,26 @@ class ExamPdfController extends Controller
                             // 优先使用API选项,支持多种数据格式
                             $apiOptions = $apiData['options'] ?? null;
 
-                            if (!empty($apiOptions)) {
+                            if (! empty($apiOptions)) {
                                 // 标准化options格式为数组值列表
                                 $q['options'] = $this->normalizeOptions($apiOptions);
                                 Log::debug('使用标准化API options', [
                                     'question_id' => $q['id'],
                                     'raw_options' => $apiOptions,
-                                    'normalized_options' => $q['options']
+                                    'normalized_options' => $q['options'],
                                 ]);
                             } else {
                                 // 备选:从题干中提取的选项
                                 $q['options'] = $extractedOptions;
                                 Log::debug('使用提取的options', [
                                     'question_id' => $q['id'],
-                                    'extracted_options' => $extractedOptions
+                                    'extracted_options' => $extractedOptions,
                                 ]);
                             }
                         }
 
                         // 从数据库 paper_questions 表中获取 question_type(已在前面设置,这里确保有值)
-                        if (!isset($q['question_type']) || empty($q['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;
@@ -663,7 +684,7 @@ class ExamPdfController extends Controller
             $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
 
             // 分离题干和选项
-            list($content, $extractedOptions) = $this->separateStemAndOptions($rawContent);
+            [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent);
 
             // 如果从题库API获取了选项,优先使用
             $options = $q['options'] ?? $extractedOptions;
@@ -684,15 +705,15 @@ class ExamPdfController extends Controller
                 'tags' => $q['tags'] ?? '',
                 'stem_length' => mb_strlen($content),
                 'stem_preview' => mb_substr($content, 0, 100),
-                'has_extracted_options' => !empty($extractedOptions),
+                'has_extracted_options' => ! empty($extractedOptions),
                 'extracted_options_count' => count($extractedOptions),
-                'has_api_options' => isset($q['options']) && !empty($q['options']),
+                '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
+                'determined_type' => $type,
             ]);
 
-            if (!isset($questions[$type])) {
+            if (! isset($questions[$type])) {
                 $type = 'answer';
             }
 
@@ -722,10 +743,11 @@ class ExamPdfController extends Controller
 
         // 【关键】确保每个题型内的题目按 question_number 排序
         foreach (['choice', 'fill', 'answer'] as $type) {
-            if (!empty($questions[$type])) {
-                usort($questions[$type], function($a, $b) {
+            if (! empty($questions[$type])) {
+                usort($questions[$type], function ($a, $b) {
                     $aNum = $a->question_number ?? 0;
                     $bNum = $b->question_number ?? 0;
+
                     return $aNum <=> $bNum;
                 });
             }
@@ -737,17 +759,18 @@ class ExamPdfController extends Controller
             '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'])
+            'total' => count($questions['choice']) + count($questions['fill']) + count($questions['answer']),
         ]);
 
         // 渲染视图
         $viewName = $includeAnswer ? 'pdf.exam-grading' : 'pdf.exam-paper';
+
         return view($viewName, [
             'paper' => $paper,
             'questions' => $questions,
             'student' => $this->getStudentInfo($paper->student_id),
             'teacher' => $this->getTeacherInfo($paper->teacher_id),
-            'includeAnswer' => $includeAnswer
+            'includeAnswer' => $includeAnswer,
         ]);
     }
 
@@ -766,12 +789,12 @@ class ExamPdfController extends Controller
 
         // 使用 Eloquent 模型获取试卷数据
         $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
-        if (!$paper) {
-            $cached = Cache::get('generated_exam_' . $paper_id);
-            if (!$cached) {
+        if (! $paper) {
+            $cached = Cache::get('generated_exam_'.$paper_id);
+            if (! $cached) {
                 abort(404, '试卷未找到');
             }
-            $paper = (object)[
+            $paper = (object) [
                 'paper_id' => $paper_id,
                 'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
                 'student_id' => $cached['student_id'] ?? null,
@@ -780,21 +803,21 @@ class ExamPdfController extends Controller
             $questionsData = $cached['questions'] ?? [];
             $totalQuestions = $cached['total_questions'] ?? count($questionsData);
             $difficultyCategory = $cached['difficulty_category'] ?? '中等';
-            if (!empty($questionsData)) {
+            if (! empty($questionsData)) {
                 $questionBankService = app(QuestionBankService::class);
                 $questionIds = array_column($questionsData, 'id');
                 $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
                 $responseData = $questionsResponse['data'] ?? [];
-                if (!empty($responseData)) {
+                if (! empty($responseData)) {
                     $responseDataMap = [];
                     foreach ($responseData as $respQ) {
                         $responseDataMap[$respQ['id']] = $respQ;
                     }
-                    $questionsData = array_map(function($q) use ($responseDataMap) {
+                    $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);
+                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
                             $q['stem'] = $stem;
                             $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
@@ -804,23 +827,24 @@ class ExamPdfController extends Controller
                             // 优先使用API选项,支持多种数据格式
                             $apiOptions = $apiData['options'] ?? null;
 
-                            if (!empty($apiOptions)) {
+                            if (! empty($apiOptions)) {
                                 // 标准化options格式为数组值列表
                                 $q['options'] = $this->normalizeOptions($apiOptions);
                                 Log::debug('使用标准化API options', [
                                     'question_id' => $q['id'],
                                     'raw_options' => $apiOptions,
-                                    'normalized_options' => $q['options']
+                                    'normalized_options' => $q['options'],
                                 ]);
                             } else {
                                 // 备选:从题干中提取的选项
                                 $q['options'] = $extractedOptions;
                                 Log::debug('使用提取的options', [
                                     'question_id' => $q['id'],
-                                    'extracted_options' => $extractedOptions
+                                    'extracted_options' => $extractedOptions,
                                 ]);
                             }
                         }
+
                         return $q;
                     }, $questionsData);
                 }
@@ -848,21 +872,21 @@ class ExamPdfController extends Controller
                     'content' => $pq->question_text ?? '',
                 ];
             }
-            if (!empty($questionsData)) {
+            if (! empty($questionsData)) {
                 $questionBankService = app(QuestionBankService::class);
                 $questionIds = array_column($questionsData, 'id');
                 $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
                 $responseData = $questionsResponse['data'] ?? [];
-                if (!empty($responseData)) {
+                if (! empty($responseData)) {
                     $responseDataMap = [];
                     foreach ($responseData as $respQ) {
                         $responseDataMap[$respQ['id']] = $respQ;
                     }
-                    $questionsData = array_map(function($q) use ($responseDataMap, $paperQuestions) {
+                    $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);
+                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
                             $q['stem'] = $stem;
                             $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
@@ -872,29 +896,30 @@ class ExamPdfController extends Controller
                             // 优先使用API选项,支持多种数据格式
                             $apiOptions = $apiData['options'] ?? null;
 
-                            if (!empty($apiOptions)) {
+                            if (! empty($apiOptions)) {
                                 // 标准化options格式为数组值列表
                                 $q['options'] = $this->normalizeOptions($apiOptions);
                                 Log::debug('使用标准化API options', [
                                     'question_id' => $q['id'],
                                     'raw_options' => $apiOptions,
-                                    'normalized_options' => $q['options']
+                                    'normalized_options' => $q['options'],
                                 ]);
                             } else {
                                 // 备选:从题干中提取的选项
                                 $q['options'] = $extractedOptions;
                                 Log::debug('使用提取的options', [
                                     'question_id' => $q['id'],
-                                    'extracted_options' => $extractedOptions
+                                    'extracted_options' => $extractedOptions,
                                 ]);
                             }
                         }
-                        if (!isset($q['question_type']) || empty($q['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);
                 }
@@ -904,14 +929,14 @@ class ExamPdfController extends Controller
         $questions = ['choice' => [], 'fill' => [], 'answer' => []];
         foreach ($questionsData as $q) {
             $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
-            list($content, $extractedOptions) = $this->separateStemAndOptions($rawContent);
+            [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent);
             $options = $q['options'] ?? $extractedOptions;
             $answer = $q['answer'] ?? '';
             $solution = $q['solution'] ?? '';
             $type = isset($q['question_type'])
                 ? $this->normalizeQuestionTypeValue((string) $q['question_type'])
                 : $this->determineQuestionType($q);
-            if (!isset($questions[$type])) {
+            if (! isset($questions[$type])) {
                 $type = 'answer';
             }
 
@@ -941,10 +966,11 @@ class ExamPdfController extends Controller
 
         // 【关键】确保每个题型内的题目按 question_number 排序
         foreach (['choice', 'fill', 'answer'] as $type) {
-            if (!empty($questions[$type])) {
-                usort($questions[$type], function($a, $b) {
+            if (! empty($questions[$type])) {
+                usort($questions[$type], function ($a, $b) {
                     $aNum = $a->question_number ?? 0;
                     $bNum = $b->question_number ?? 0;
+
                     return $aNum <=> $bNum;
                 });
             }
@@ -958,4 +984,169 @@ class ExamPdfController extends Controller
             'includeAnswer' => true,
         ]);
     }
+
+    /**
+     * 重新生成 PDF(统一生成卷子和判卷)
+     *
+     * @param  string  $paper_id
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function regeneratePdf(Request $request, $paper_id)
+    {
+        Log::info('RegeneratePdf: 开始重新生成PDF', ['paper_id' => $paper_id]);
+
+        // 验证 paper_id 格式
+        if (empty($paper_id) || ! preg_match('/^paper_\d+$/', $paper_id)) {
+            return response()->json([
+                'success' => false,
+                'message' => '无效的试卷ID格式',
+                'paper_id' => $paper_id,
+            ], 400);
+        }
+
+        try {
+            // 调用 PDF 生成服务
+            $pdfService = app(\App\Services\ExamPdfExportService::class);
+
+            // 生成统一 PDF(卷子 + 判卷)
+            $pdfUrl = $pdfService->generateUnifiedPdf($paper_id);
+
+            if ($pdfUrl) {
+                Log::info('RegeneratePdf: PDF重新生成成功', [
+                    'paper_id' => $paper_id,
+                    'url' => $pdfUrl,
+                ]);
+
+                return response()->json([
+                    'success' => true,
+                    'message' => 'PDF重新生成成功',
+                    'paper_id' => $paper_id,
+                    'pdf_url' => $pdfUrl,
+                ]);
+            }
+
+            Log::error('RegeneratePdf: PDF生成失败', ['paper_id' => $paper_id]);
+
+            return response()->json([
+                'success' => false,
+                'message' => 'PDF生成失败',
+                'paper_id' => $paper_id,
+            ], 500);
+
+        } catch (\Exception $e) {
+            Log::error('RegeneratePdf: 异常错误', [
+                'paper_id' => $paper_id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+
+            return response()->json([
+                'success' => false,
+                'message' => 'PDF生成异常:'.$e->getMessage(),
+                'paper_id' => $paper_id,
+            ], 500);
+        }
+    }
+
+    /**
+     * 重新生成试卷 PDF(不含答案)
+     *
+     * @param  string  $paper_id
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function regenerateExamPdf(Request $request, $paper_id)
+    {
+        Log::info('RegenerateExamPdf: 开始重新生成试卷PDF', ['paper_id' => $paper_id]);
+
+        if (empty($paper_id) || ! preg_match('/^paper_\d+$/', $paper_id)) {
+            return response()->json([
+                'success' => false,
+                'message' => '无效的试卷ID格式',
+                'paper_id' => $paper_id,
+            ], 400);
+        }
+
+        try {
+            $pdfService = app(\App\Services\ExamPdfExportService::class);
+            $pdfUrl = $pdfService->generateExamPdf($paper_id);
+
+            if ($pdfUrl) {
+                return response()->json([
+                    'success' => true,
+                    'message' => '试卷PDF重新生成成功',
+                    'paper_id' => $paper_id,
+                    'pdf_url' => $pdfUrl,
+                ]);
+            }
+
+            return response()->json([
+                'success' => false,
+                'message' => '试卷PDF生成失败',
+                'paper_id' => $paper_id,
+            ], 500);
+
+        } catch (\Exception $e) {
+            Log::error('RegenerateExamPdf: 异常错误', [
+                'paper_id' => $paper_id,
+                'error' => $e->getMessage(),
+            ]);
+
+            return response()->json([
+                'success' => false,
+                'message' => 'PDF生成异常:'.$e->getMessage(),
+                'paper_id' => $paper_id,
+            ], 500);
+        }
+    }
+
+    /**
+     * 重新生成判卷 PDF(含答案)
+     *
+     * @param  string  $paper_id
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function regenerateGradingPdf(Request $request, $paper_id)
+    {
+        Log::info('RegenerateGradingPdf: 开始重新生成判卷PDF', ['paper_id' => $paper_id]);
+
+        if (empty($paper_id) || ! preg_match('/^paper_\d+$/', $paper_id)) {
+            return response()->json([
+                'success' => false,
+                'message' => '无效的试卷ID格式',
+                'paper_id' => $paper_id,
+            ], 400);
+        }
+
+        try {
+            $pdfService = app(\App\Services\ExamPdfExportService::class);
+            $pdfUrl = $pdfService->generateGradingPdf($paper_id);
+
+            if ($pdfUrl) {
+                return response()->json([
+                    'success' => true,
+                    'message' => '判卷PDF重新生成成功',
+                    'paper_id' => $paper_id,
+                    'pdf_url' => $pdfUrl,
+                ]);
+            }
+
+            return response()->json([
+                'success' => false,
+                'message' => '判卷PDF生成失败',
+                'paper_id' => $paper_id,
+            ], 500);
+
+        } catch (\Exception $e) {
+            Log::error('RegenerateGradingPdf: 异常错误', [
+                'paper_id' => $paper_id,
+                'error' => $e->getMessage(),
+            ]);
+
+            return response()->json([
+                'success' => false,
+                'message' => 'PDF生成异常:'.$e->getMessage(),
+                'paper_id' => $paper_id,
+            ], 500);
+        }
+    }
 }

Datei-Diff unterdrückt, da er zu groß ist
+ 181 - 110
app/Services/ExamPdfExportService.php


+ 7 - 6
resources/views/components/exam/paper-body.blade.php

@@ -211,10 +211,10 @@
                         // 短选项(≤15字符)且选项数≤4:4列布局
                         // 中等选项(16-30字符)或选项数>4:2列布局
                         // 长选项(>30字符):1列布局
-                        if ($maxOptionLength <= 15 && $optCount <= 4) {
+                        if ($maxOptionLength <= 13) {
                             $optionsClass = 'options-grid-4';
                             $layoutDesc = '4列布局';
-                        } elseif ($maxOptionLength <= 30) {
+                        } elseif ($maxOptionLength <= 26) {
                             $optionsClass = 'options-grid-2';
                             $layoutDesc = '2列布局';
                         } else {
@@ -393,12 +393,13 @@
                 </div>
                 <div class="question-main">
                     @unless($gradingMode)
-                        <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)</span>
+                        <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)
+                            @if($showQuestionId && !empty($q->id))
+                                <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
+                            @endif
+                        </span>
                     @endunless
                     <span class="question-stem">{!! $mathProcessed ? $q->content : \App\Services\MathFormulaProcessor::processFormulas($q->content) !!}</span>
-                    @if($showQuestionId && !empty($q->id))
-                        <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
-                    @endif
                 </div>
                 @unless($gradingMode)
                     <div class="question-lead spacer"></div>

+ 29 - 5
resources/views/pdf/exam-grading.blade.php

@@ -17,22 +17,22 @@
             margin: 2cm 2cm 2.5cm 2cm;
             @top-left {
                 content: "知了数学";
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
             @top-right {
                 content: "{{ $gradingCode }}";
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
             @bottom-left {
                 content: "{{ $gradingCode }}";
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
             @bottom-right {
                 content: counter(page) "/" counter(pages);
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
         }
@@ -235,7 +235,31 @@
         svg circle, svg line, svg polygon, svg polyline {
             shape-rendering: geometricPrecision;
         }
-        /* 题干中的图片样式 */
+        /* PDF图片容器:防止图片跨页分割 - 增强版 */
+        .pdf-figure {
+            break-inside: avoid;
+            page-break-inside: avoid;
+            -webkit-column-break-inside: avoid;
+            break-before: avoid;
+            break-after: avoid;
+            page-break-before: avoid;
+            page-break-after: avoid;
+            margin: 8px 0;
+            display: block;
+            /* 确保图片不会在页面底部被截断 */
+            min-height: 30px;
+            /* 限制单张图片最大高度,防止过大图片跨页 */
+            max-height: 180mm;
+        }
+        .pdf-figure img {
+            max-width: 100%;
+            height: auto;
+            display: block;
+            margin: 0 auto;
+            object-fit: contain;
+            box-sizing: border-box;
+        }
+        /* 题干中的图片样式(向后兼容) */
         .question-stem img,
         .question-main img,
         .question-content img {

+ 30 - 6
resources/views/pdf/exam-paper.blade.php

@@ -16,22 +16,22 @@
             margin: 2cm 2cm 2.5cm 2cm;
             @top-left {
                 content: "知了数学";
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
             @top-right {
                 content: "{{ $examCode }}";
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
             @bottom-left {
                 content: "{{ $examCode }}";
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
             @bottom-right {
                 content: counter(page) "/" counter(pages);
-                font-size: 10px;
+                font-size: 12px;
                 color: #666;
             }
         }
@@ -132,7 +132,7 @@
             display: block;
             font-size: 13px;
             color: #555;
-            margin: 0 0 4px 0;
+            margin: 0 0 2px 0;
             white-space: nowrap;
         }
         /* 题目内容:防止孤行 */
@@ -320,7 +320,31 @@
         .wavy-underline.short {
             min-width: 60px;
         }
-        /* 题干中的图片样式 */
+        /* PDF图片容器:防止图片跨页分割 - 增强版 */
+        .pdf-figure {
+            break-inside: avoid;
+            page-break-inside: avoid;
+            -webkit-column-break-inside: avoid;
+            break-before: avoid;
+            break-after: avoid;
+            page-break-before: avoid;
+            page-break-after: avoid;
+            margin: 8px 0;
+            display: block;
+            /* 确保图片不会在页面底部被截断 */
+            min-height: 30px;
+            /* 限制单张图片最大高度,防止过大图片跨页 */
+            max-height: 180mm;
+        }
+        .pdf-figure img {
+            max-width: 100%;
+            height: auto;
+            display: block;
+            margin: 0 auto;
+            object-fit: contain;
+            box-sizing: border-box;
+        }
+        /* 题干中的图片样式(向后兼容) */
         .question-stem img,
         .question-main img,
         .question-content img {

+ 116 - 78
routes/api.php

@@ -1,25 +1,23 @@
 <?php
 
+use App\Http\Controllers\Api\AbilityEvaluateController;
+use App\Http\Controllers\Api\ExamAnalysisApiController;
 use App\Http\Controllers\Api\IntelligentExamController;
-use App\Http\Controllers\Api\PreQuestionApiController;
-use App\Http\Controllers\Api\TextbookApiController;
+use App\Http\Controllers\Api\KnowledgeRecommendController;
 use App\Http\Controllers\Api\MistakeBookController;
-use App\Http\Controllers\Api\QuestionSearchController;
-use App\Http\Controllers\Api\QuestionRandomController;
 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\KnowledgeRecommendController;
-use App\Http\Controllers\Api\AbilityEvaluateController;
+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;
-use App\Events\QuestionGenerationCompleted;
-use App\Events\QuestionGenerationFailed;
-use Illuminate\Auth\Middleware\Authenticate;
-use App\Http\Controllers\Api\ExamAnalysisApiController;
-use App\Http\Controllers\Api\StudentAnswerAnalysisController;
-use App\Http\Controllers\Api\StudentKnowledgeController;
 
 /*
 |--------------------------------------------------------------------------
@@ -46,7 +44,7 @@ Route::post('/questions/callback', function () {
         Log::info('Received question generation callback', $data);
 
         // 验证回调数据
-        if (!isset($data['task_id']) || !isset($data['status'])) {
+        if (! isset($data['task_id']) || ! isset($data['status'])) {
             return response()->json(['error' => 'Invalid callback data'], 400);
         }
 
@@ -60,14 +58,14 @@ Route::post('/questions/callback', function () {
             session()->flash('notification', [
                 'type' => 'success',
                 'title' => '✅ 题目生成完成',
-                'body' => "任务 ID: {$data['task_id']}\n生成题目: {$total} 道" . ($kpCode ? "\n知识点: {$kpCode}" : ''),
-                'color' => 'success'
+                'body' => "任务 ID: {$data['task_id']}\n生成题目: {$total} 道".($kpCode ? "\n知识点: {$kpCode}" : ''),
+                'color' => 'success',
             ]);
 
-            Log::info("题目生成成功通知已存储", [
+            Log::info('题目生成成功通知已存储', [
                 'task_id' => $data['task_id'],
                 'total' => $total,
-                'kp_code' => $kpCode
+                'kp_code' => $kpCode,
             ]);
 
         } elseif ($data['status'] === 'failed') {
@@ -78,22 +76,23 @@ Route::post('/questions/callback', function () {
                 'type' => 'error',
                 'title' => '❌ 题目生成失败',
                 'body' => "任务 ID: {$data['task_id']}\n错误: {$error}",
-                'color' => 'danger'
+                'color' => 'danger',
             ]);
 
-            Log::error("题目生成失败通知已存储", [
+            Log::error('题目生成失败通知已存储', [
                 'task_id' => $data['task_id'],
-                'error' => $error
+                'error' => $error,
             ]);
         }
 
         return response()->json([
             'success' => true,
             'message' => 'Callback received and notification stored',
-            'status' => $data['status']
+            'status' => $data['status'],
         ]);
     } catch (\Exception $e) {
-        Log::error('Callback processing failed: ' . $e->getMessage());
+        Log::error('Callback processing failed: '.$e->getMessage());
+
         return response()->json(['error' => $e->getMessage()], 500);
     }
 })->name('api.questions.callback');
@@ -105,11 +104,12 @@ Route::post('/ocr-question-callback', function () {
         Log::info('Received OCR question generation callback', $data);
 
         // 验证必要的回调数据
-        if (!isset($data['task_id']) || !isset($data['status']) || !isset($data['ocr_record_id'])) {
+        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'
+                'error' => 'Missing required fields: task_id, status, ocr_record_id',
             ], 400);
         }
 
@@ -126,7 +126,7 @@ Route::post('/ocr-question-callback', function () {
             'task_id' => $taskId,
             'status' => $status,
             'total_generated' => $data['result']['total_generated'] ?? 0,
-            'total_saved' => $data['result']['total_saved'] ?? 0
+            'total_saved' => $data['result']['total_saved'] ?? 0,
         ]);
 
         // 处理题目关联逻辑
@@ -135,10 +135,10 @@ Route::post('/ocr-question-callback', function () {
             // 从result中提取question_mappings(QuestionBank API将它放在result字段中)
             $mappings = $data['result']['question_mappings'] ?? $data['question_mappings'] ?? [];
 
-            Log::info("Processing OCR question associations", [
+            Log::info('Processing OCR question associations', [
                 'ocr_record_id' => $ocrRecordId,
                 'task_id' => $taskId,
-                'mappings_count' => count($mappings)
+                'mappings_count' => count($mappings),
             ]);
 
             // 更新ocr_question_results表中的关联关系
@@ -162,32 +162,32 @@ Route::post('/ocr-question-callback', function () {
 
                         if ($updated) {
                             $updatedCount++;
-                            Log::info("Updated OCR question association", [
+                            Log::info('Updated OCR question association', [
                                 'ocr_record_id' => $ocrRecordId,
                                 'question_number' => $ocrQuestionNumber,
                                 'question_bank_id' => $questionBankId,
-                                'question_code' => $questionCode
+                                'question_code' => $questionCode,
                             ]);
                         } else {
-                            Log::warning("No OCR question result found for association", [
+                            Log::warning('No OCR question result found for association', [
                                 'ocr_record_id' => $ocrRecordId,
-                                'question_number' => $ocrQuestionNumber
+                                'question_number' => $ocrQuestionNumber,
                             ]);
                         }
                     }
                 } catch (\Exception $e) {
-                    Log::error("Failed to update OCR question association", [
+                    Log::error('Failed to update OCR question association', [
                         'mapping' => $mapping,
-                        'error' => $e->getMessage()
+                        'error' => $e->getMessage(),
                     ]);
                 }
             }
 
-            Log::info("OCR question association completed", [
+            Log::info('OCR question association completed', [
                 'ocr_record_id' => $ocrRecordId,
                 'task_id' => $taskId,
                 'total_mappings' => count($mappings),
-                'updated_count' => $updatedCount
+                'updated_count' => $updatedCount,
             ]);
 
             // 更新OCR记录的整体状态为已完成
@@ -197,17 +197,17 @@ Route::post('/ocr-question-callback', function () {
                     ->update([
                         'status' => 'completed',
                         'processed_at' => now(),
-                        'updated_at' => now()
+                        'updated_at' => now(),
                     ]);
 
-                Log::info("Updated OCR record status to completed", [
+                Log::info('Updated OCR record status to completed', [
                     'ocr_record_id' => $ocrRecordId,
-                    'task_id' => $taskId
+                    'task_id' => $taskId,
                 ]);
             } catch (\Exception $e) {
-                Log::error("Failed to update OCR record status", [
+                Log::error('Failed to update OCR record status', [
                     'ocr_record_id' => $ocrRecordId,
-                    'error' => $e->getMessage()
+                    'error' => $e->getMessage(),
                 ]);
             }
 
@@ -223,11 +223,11 @@ Route::post('/ocr-question-callback', function () {
                         'generation_error' => $data['error'] ?? 'Unknown error',
                     ]);
 
-                Log::info("Updated OCR questions to failed status", [
+                Log::info('Updated OCR questions to failed status', [
                     'ocr_record_id' => $ocrRecordId,
                     'task_id' => $taskId,
                     'updated_count' => $updated,
-                    'error' => $data['error'] ?? 'Unknown error'
+                    'error' => $data['error'] ?? 'Unknown error',
                 ]);
 
                 // 更新OCR记录的状态为失败
@@ -236,18 +236,18 @@ Route::post('/ocr-question-callback', function () {
                     ->update([
                         'status' => 'failed',
                         'error_message' => $data['error'] ?? 'Question generation failed',
-                        'updated_at' => now()
+                        'updated_at' => now(),
                     ]);
 
-                Log::info("Updated OCR record status to failed", [
+                Log::info('Updated OCR record status to failed', [
                     'ocr_record_id' => $ocrRecordId,
                     'task_id' => $taskId,
-                    'error' => $data['error'] ?? 'Unknown error'
+                    'error' => $data['error'] ?? 'Unknown error',
                 ]);
             } catch (\Exception $e) {
-                Log::error("Failed to update OCR questions to failed status", [
+                Log::error('Failed to update OCR questions to failed status', [
                     'ocr_record_id' => $ocrRecordId,
-                    'error' => $e->getMessage()
+                    'error' => $e->getMessage(),
                 ]);
             }
         }
@@ -260,16 +260,17 @@ Route::post('/ocr-question-callback', function () {
                 'ocr_record_id' => $ocrRecordId,
                 'status' => $status,
                 'cache_key' => $cacheKey,
-                'associations_processed' => $status === 'completed' ? count($data['question_mappings'] ?? []) : 0
-            ]
+                '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());
+        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()
+            'error' => 'Callback processing failed: '.$e->getMessage(),
         ], 500);
     }
 })->name('api.ocr.callback');
@@ -282,15 +283,17 @@ Route::get('/questions/callback/{taskId}', function (string $taskId) {
     if ($callbackData) {
         // 清除已读取的回调数据
         cache()->forget($taskId);
-        session()->forget('question_gen_callback_' . $taskId);
+        session()->forget('question_gen_callback_'.$taskId);
+
         return response()->json($callbackData);
     }
 
     // 备选:从session读取
-    $sessionData = session('question_gen_callback_' . $taskId);
+    $sessionData = session('question_gen_callback_'.$taskId);
     if ($sessionData) {
         // 清除已读取的回调数据
-        session()->forget('question_gen_callback_' . $taskId);
+        session()->forget('question_gen_callback_'.$taskId);
+
         return response()->json($sessionData);
     }
 
@@ -306,16 +309,17 @@ Route::get('/ocr-question-callback/{ocrRecordId}/{taskId}', function (int $ocrRe
     if ($callbackData) {
         // 清除已读取的回调数据
         cache()->forget($cacheKey);
+
         return response()->json([
             'success' => true,
-            'data' => $callbackData
+            'data' => $callbackData,
         ]);
     }
 
     return response()->json([
         'success' => false,
         'status' => 'pending',
-        'message' => 'OCR callback not received yet'
+        'message' => 'OCR callback not received yet',
     ], 202);
 })->name('api.ocr.callback.get');
 
@@ -335,7 +339,8 @@ Route::get('/questions', function (QuestionServiceApi $service) {
 
         return response()->json($response);
     } catch (\Exception $e) {
-        \Log::error('Failed to fetch questions: ' . $e->getMessage());
+        \Log::error('Failed to fetch questions: '.$e->getMessage());
+
         return response()->json([
             'data' => [],
             'meta' => [
@@ -353,9 +358,11 @@ Route::get('/questions', function (QuestionServiceApi $service) {
 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());
+        \Log::error('Failed to get question statistics: '.$e->getMessage());
+
         return response()->json(['error' => $e->getMessage()], 500);
     }
 });
@@ -365,9 +372,11 @@ 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());
+        \Log::error('Question search failed: '.$e->getMessage());
+
         return response()->json(['error' => $e->getMessage()], 500);
     }
 });
@@ -376,12 +385,14 @@ Route::post('/questions/search', function (QuestionServiceApi $service) {
 Route::get('/questions/{id}', function (int $id, QuestionServiceApi $service) {
     try {
         $question = $service->getQuestionById($id);
-        if (!$question) {
+        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());
+        \Log::error("Failed to get question {$id}: ".$e->getMessage());
+
         return response()->json(['error' => $e->getMessage()], 500);
     }
 });
@@ -391,9 +402,11 @@ 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());
+        \Log::error('Question generation failed: '.$e->getMessage());
+
         return response()->json([
             'success' => false,
             'message' => $e->getMessage(),
@@ -405,12 +418,14 @@ Route::post('/questions/generate', function (QuestionServiceApi $service) {
 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());
+        \Log::error("Failed to delete question {$id}: ".$e->getMessage());
+
         return response()->json([
             'success' => false,
             'message' => $e->getMessage(),
@@ -880,31 +895,30 @@ Route::get('/mathrecsys/test', function () {
     return response()->json([
         'success' => true,
         'message' => 'MathRecSys API integration is working',
-        'timestamp' => now()->toISOString()
+        'timestamp' => now()->toISOString(),
     ]);
 })->name('api.mathrecsys.test');
 
-
 // 测试OCR题目生成API调用
 Route::post('/test-ocr-generation', function () {
     try {
-        $service = new \App\Services\QuestionBankService();
+        $service = new \App\Services\QuestionBankService;
 
         // 模拟前端传递的OCR题目数据
         $questions = [
             [
                 'id' => 1,
-                'content' => '计算:2+3-4'
+                'content' => '计算:2+3-4',
             ],
             [
                 'id' => 2,
-                'content' => '解方程:x+5=10'
-            ]
+                'content' => '解方程:x+5=10',
+            ],
         ];
 
         Log::info('开始测试OCR题目生成', [
             'questions_count' => count($questions),
-            'ocr_record_id' => 12
+            'ocr_record_id' => 12,
         ]);
 
         // 使用异步API,系统自动生成回调URL
@@ -920,24 +934,24 @@ Route::post('/test-ocr-generation', function () {
         Log::info('OCR题目生成响应', [
             'response' => $response,
             'status' => $response['status'] ?? 'unknown',
-            'task_id' => $response['task_id'] ?? 'N/A'
+            'task_id' => $response['task_id'] ?? 'N/A',
         ]);
 
         return response()->json([
             'success' => true,
             'message' => 'OCR题目生成测试完成',
-            'data' => $response
+            'data' => $response,
         ]);
 
     } catch (\Exception $e) {
         Log::error('测试OCR题目生成失败', [
             'error' => $e->getMessage(),
-            'trace' => $e->getTraceAsString()
+            'trace' => $e->getTraceAsString(),
         ]);
 
         return response()->json([
             'success' => false,
-            'error' => $e->getMessage()
+            'error' => $e->getMessage(),
         ], 500);
     }
 })->name('api.test.ocr.generation');
@@ -985,8 +999,8 @@ Route::get('/student-answers/history/{studentId}', [StudentAnswerAnalysisControl
 */
 
 use App\Http\Controllers\Api\ExamAnswerAnalysisController;
-use App\Http\Controllers\Api\PaperSubmitAnalysisController;
 use App\Http\Controllers\Api\HealthCheckController;
+use App\Http\Controllers\Api\PaperSubmitAnalysisController;
 
 // 分析考试答题数据
 Route::post('/exam-answer-analysis', [ExamAnswerAnalysisController::class, 'analyze'])
@@ -1067,7 +1081,7 @@ Route::post('/exam-answer-analysis/batch', [ExamAnswerAnalysisController::class,
 
 Route::get('/tasks/status/{taskId}', function (string $taskId) {
     $task = app(\App\Services\TaskManager::class)->getTaskStatus($taskId);
-    if (!$task) {
+    if (! $task) {
         return response()->json([
             'success' => false,
             'message' => '任务不存在',
@@ -1128,8 +1142,8 @@ Route::get('/health', [HealthCheckController::class, 'index'])
 |--------------------------------------------------------------------------
 */
 
-use App\Http\Controllers\Api\StudentProgressController;
 use App\Http\Controllers\Api\QuestionPdfController;
+use App\Http\Controllers\Api\StudentProgressController;
 
 // 获取单个学生学习进度
 Route::get('/students/{studentId}/learning-progress', [StudentProgressController::class, 'show'])
@@ -1152,6 +1166,30 @@ 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,保留注释供参考)

+ 11 - 6
routes/web.php

@@ -1,19 +1,24 @@
 <?php
 
-use Illuminate\Support\Facades\Route;
-use App\Http\Controllers\NotificationController;
-use App\Http\Controllers\MenuVisibilityController;
 use App\Http\Controllers\ImportStreamController;
+use App\Http\Controllers\MenuVisibilityController;
+use App\Http\Controllers\NotificationController;
+use Illuminate\Support\Facades\Route;
 
 Route::get('/', function () {
     return redirect()->route('filament.admin.pages.dashboard');
 });
 
-Route::get('/test-math', function() { return view('test-math'); });
-Route::get('/test-case', function() { return view('test-case'); });
+Route::get('/test-math', function () {
+    return view('test-math');
+});
+Route::get('/test-case', function () {
+    return view('test-case');
+});
 Route::view('/knowledge-mindmap-public', 'public.knowledge-mindmap');
 Route::get('/admin/intelligent-exam/pdf/{paper_id}', [\App\Http\Controllers\ExamPdfController::class, 'show'])->name('filament.admin.auth.intelligent-exam.pdf');
 Route::get('/admin/intelligent-exam/grading/{paper_id}', [\App\Http\Controllers\ExamPdfController::class, 'showGrading'])->name('filament.admin.auth.intelligent-exam.grading');
+
 Route::get('/admin/exam-analysis/pdf', [\App\Http\Controllers\ExamAnalysisPdfController::class, 'show'])->name('filament.admin.auth.exam-analysis.pdf');
 
 // 检查通知的路由
@@ -24,7 +29,7 @@ Route::post('/admin/toggle-menu-visibility', [MenuVisibilityController::class, '
     ->name('filament.admin.auth.toggle-menu-visibility');
 
 // Livewire测试路由
-Route::get('/test-livewire', function() {
+Route::get('/test-livewire', function () {
     return view('test-livewire');
 })->name('test.livewire');
 

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.