Răsfoiți Sursa

处理卷子生成选择题选项和判卷公式解析的问题

yemeishu 1 zi în urmă
părinte
comite
5c88ad85b9

+ 150 - 7
app/Http/Controllers/ExamPdfController.php

@@ -10,6 +10,50 @@ use Illuminate\Support\Facades\Log;
 
 class ExamPdfController extends Controller
 {
+    /**
+     * 标准化选项格式为数组值列表
+     * 支持格式:
+     * 1. {"A": "0", "B": "5", "C": "-3", "D": "12"} -> ["0", "5", "-3", "12"]
+     * 2. [["label": "A", "text": "选项A"], ...] -> ["选项A", "选项B", ...]
+     * 3. ["A", "B", "C", "D"] -> ["A", "B", "C", "D"]
+     */
+    private function normalizeOptions($options): array
+    {
+        if (empty($options)) {
+            return [];
+        }
+
+        // 如果是对象格式 {"A": "值1", "B": "值2", ...}
+        if (is_array($options) && !isset($options[0])) {
+            return array_values($options);
+        }
+
+        // 如果是AI生成格式 [{"label": "A", "text": "选项A"}, ...]
+        if (is_array($options) && isset($options[0]) && is_array($options[0])) {
+            // 提取text字段,如果不存在则使用整个数组元素
+            $normalized = [];
+            foreach ($options as $opt) {
+                if (isset($opt['text'])) {
+                    $normalized[] = $opt['text'];
+                } elseif (isset($opt['value'])) {
+                    $normalized[] = $opt['value'];
+                } else {
+                    // 如果既没有text也没有value,取数组的第一个值
+                    $normalized[] = is_array($opt) ? (string) reset($opt) : (string) $opt;
+                }
+            }
+            return $normalized;
+        }
+
+        // 如果已经是简单数组格式 ["A", "B", "C", "D"]
+        if (is_array($options)) {
+            return array_values($options);
+        }
+
+        // 其他情况返回空数组
+        return [];
+    }
+
     /**
      * 根据题目内容或类型字段判断题型
      */
@@ -436,10 +480,30 @@ class ExamPdfController extends Controller
                                 list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent);
 
                                 $q['stem'] = $stem;
+                                $q['content'] = $stem; // 同时设置content字段
                                 $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
                                 $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
                                 $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
-                                $q['options'] = $apiData['options'] ?? $extractedOptions; // 优先使用API选项,备选提取的选项
+
+                                // 优先使用API选项,支持多种数据格式
+                                $apiOptions = $apiData['options'] ?? null;
+
+                                if (!empty($apiOptions)) {
+                                    // 标准化options格式为数组值列表
+                                    $q['options'] = $this->normalizeOptions($apiOptions);
+                                    Log::debug('使用标准化API options', [
+                                        'question_id' => $q['id'],
+                                        'raw_options' => $apiOptions,
+                                        'normalized_options' => $q['options']
+                                    ]);
+                                } else {
+                                    // 备选:从题干中提取的选项
+                                    $q['options'] = $extractedOptions;
+                                    Log::debug('使用提取的options', [
+                                        'question_id' => $q['id'],
+                                        'extracted_options' => $extractedOptions
+                                    ]);
+                                }
                             }
                             return $q;
                         }, $questionsData);
@@ -525,10 +589,30 @@ class ExamPdfController extends Controller
 
                             // 合并数据,优先使用题库API的 stem、answer、solution、options
                             $q['stem'] = $stem;
+                            $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
                             $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
                             $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
-                            $q['options'] = $apiData['options'] ?? $extractedOptions; // 优先使用API选项,备选提取的选项
+
+                            // 优先使用API选项,支持多种数据格式
+                            $apiOptions = $apiData['options'] ?? null;
+
+                            if (!empty($apiOptions)) {
+                                // 标准化options格式为数组值列表
+                                $q['options'] = $this->normalizeOptions($apiOptions);
+                                Log::debug('使用标准化API options', [
+                                    'question_id' => $q['id'],
+                                    'raw_options' => $apiOptions,
+                                    'normalized_options' => $q['options']
+                                ]);
+                            } else {
+                                // 备选:从题干中提取的选项
+                                $q['options'] = $extractedOptions;
+                                Log::debug('使用提取的options', [
+                                    'question_id' => $q['id'],
+                                    'extracted_options' => $extractedOptions
+                                ]);
+                            }
                         }
 
                         // 从数据库 paper_questions 表中获取 question_type(已在前面设置,这里确保有值)
@@ -585,17 +669,26 @@ class ExamPdfController extends Controller
                 $type = 'answer';
             }
 
-            $qData = (object)[
+            // 统一处理数学公式和选项数据
+            $questionData = [
                 'id' => $q['id'] ?? $q['question_bank_id'] ?? null,
                 'content' => $content,
+                'stem' => $content, // 同时提供stem字段
                 '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), // 优先使用生成时分配的分数
+                'score' => $q['score'] ?? $this->getQuestionScore($type),
+                'question_type' => $type,
             ];
+
+            // 统一处理数学公式 - 标记已处理,避免模板中重复处理
+            $questionData = \App\Services\MathFormulaProcessor::processQuestionData($questionData);
+            $questionData['math_processed'] = true; // 添加标记
+
+            $qData = (object) $questionData;
             $questions[$type][] = $qData;
         }
 
@@ -663,10 +756,30 @@ class ExamPdfController extends Controller
                             $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? '';
                             list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent);
                             $q['stem'] = $stem;
+                            $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
                             $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
                             $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
-                            $q['options'] = $apiData['options'] ?? $extractedOptions;
+
+                            // 优先使用API选项,支持多种数据格式
+                            $apiOptions = $apiData['options'] ?? null;
+
+                            if (!empty($apiOptions)) {
+                                // 标准化options格式为数组值列表
+                                $q['options'] = $this->normalizeOptions($apiOptions);
+                                Log::debug('使用标准化API options', [
+                                    'question_id' => $q['id'],
+                                    'raw_options' => $apiOptions,
+                                    'normalized_options' => $q['options']
+                                ]);
+                            } else {
+                                // 备选:从题干中提取的选项
+                                $q['options'] = $extractedOptions;
+                                Log::debug('使用提取的options', [
+                                    'question_id' => $q['id'],
+                                    'extracted_options' => $extractedOptions
+                                ]);
+                            }
                         }
                         return $q;
                     }, $questionsData);
@@ -710,10 +823,30 @@ class ExamPdfController extends Controller
                             $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失';
                             list($stem, $extractedOptions) = $this->separateStemAndOptions($rawContent);
                             $q['stem'] = $stem;
+                            $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
                             $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
                             $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
-                            $q['options'] = $apiData['options'] ?? $extractedOptions;
+
+                            // 优先使用API选项,支持多种数据格式
+                            $apiOptions = $apiData['options'] ?? null;
+
+                            if (!empty($apiOptions)) {
+                                // 标准化options格式为数组值列表
+                                $q['options'] = $this->normalizeOptions($apiOptions);
+                                Log::debug('使用标准化API options', [
+                                    'question_id' => $q['id'],
+                                    'raw_options' => $apiOptions,
+                                    'normalized_options' => $q['options']
+                                ]);
+                            } else {
+                                // 备选:从题干中提取的选项
+                                $q['options'] = $extractedOptions;
+                                Log::debug('使用提取的options', [
+                                    'question_id' => $q['id'],
+                                    'extracted_options' => $extractedOptions
+                                ]);
+                            }
                         }
                         if (!isset($q['question_type']) || empty($q['question_type'])) {
                             $dbQuestion = $paperQuestions->firstWhere('question_bank_id', $q['id']);
@@ -740,9 +873,12 @@ class ExamPdfController extends Controller
             if (!isset($questions[$type])) {
                 $type = 'answer';
             }
-            $qData = (object)[
+
+            // 统一处理数学公式和选项数据
+            $questionData = [
                 'id' => $q['id'] ?? $q['question_bank_id'] ?? null,
                 'content' => $content,
+                'stem' => $content, // 同时提供stem字段
                 'answer' => $answer,
                 'solution' => $solution,
                 'difficulty' => $q['difficulty'] ?? 0.5,
@@ -750,7 +886,14 @@ class ExamPdfController extends Controller
                 'tags' => $q['tags'] ?? '',
                 'options' => $options,
                 'score' => $q['score'] ?? $this->getQuestionScore($type),
+                'question_type' => $type,
             ];
+
+            // 统一处理数学公式 - 标记已处理,避免模板中重复处理
+            $questionData = \App\Services\MathFormulaProcessor::processQuestionData($questionData);
+            $questionData['math_processed'] = true; // 添加标记
+
+            $qData = (object) $questionData;
             $questions[$type][] = $qData;
         }
 

+ 1 - 1
app/Services/MathFormulaProcessor.php

@@ -292,7 +292,7 @@ class MathFormulaProcessor
         $fieldsToProcess = [
             'stem', 'content', 'question_text', 'answer',
             'correct_answer', 'student_answer', 'explanation',
-            'solution', 'question_content'
+            'solution', 'question_content', 'options'
         ];
         return self::processArray($question, $fieldsToProcess);
     }

+ 52 - 12
resources/views/components/exam/paper-body.blade.php

@@ -4,6 +4,18 @@
     $answerQuestions = $questions['answer'] ?? [];
     $gradingMode = $grading ?? false;
 
+    // 检查是否有数学公式处理标记,避免重复处理
+    $mathProcessed = false;
+    // 检查所有题型中是否有任何题目包含 math_processed 标记
+    foreach ([$choiceQuestions, $fillQuestions, $answerQuestions] as $questionType) {
+        foreach ($questionType as $q) {
+            if (isset($q->math_processed) && $q->math_processed) {
+                $mathProcessed = true;
+                break 2; // 找到标记就退出两层循环
+            }
+        }
+    }
+
     // 计算填空空格数量
     $countBlanks = function($text) {
         $count = 0;
@@ -67,7 +79,7 @@
             if ($renderedStem === $stemLine) {
                 $renderedStem .= ' ' . $blankSpan;
             }
-            $renderedStem = \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
+            $renderedStem = $mathProcessed ? $renderedStem : \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
         @endphp
         <div class="question">
             <div class="question-grid">
@@ -82,15 +94,43 @@
                 </div>
                 @if(!empty($options))
                     @php
+                        // 计算选项长度并动态选择布局
                         $optCount = count($options);
-                        $optionsClass = $optCount <= 4 ? 'options-grid-2' : 'options';
+                        $maxOptionLength = 0;
+                        foreach ($options as $opt) {
+                            $optText = strip_tags(\App\Services\MathFormulaProcessor::processFormulas($opt));
+                            $maxOptionLength = max($maxOptionLength, mb_strlen($optText, 'UTF-8'));
+                        }
+
+                        // 根据最长选项长度和选项数量动态选择布局
+                        // 短选项(≤15字符)且选项数≤4:4列布局
+                        // 中等选项(16-30字符)或选项数>4:2列布局
+                        // 长选项(>30字符):1列布局
+                        if ($maxOptionLength <= 15 && $optCount <= 4) {
+                            $optionsClass = 'options-grid-4';
+                            $layoutDesc = '4列布局';
+                        } elseif ($maxOptionLength <= 30) {
+                            $optionsClass = 'options-grid-2';
+                            $layoutDesc = '2列布局';
+                        } else {
+                            $optionsClass = 'options-grid-1';
+                            $layoutDesc = '1列布局';
+                        }
+
+                        \Illuminate\Support\Facades\Log::debug('选择题布局决策', [
+                            'question_number' => $questionNumber,
+                            'opt_count' => $optCount,
+                            'max_length' => $maxOptionLength,
+                            'selected_class' => $optionsClass,
+                            'layout' => $layoutDesc
+                        ]);
                     @endphp
                     <div class="question-lead spacer"></div>
                     <div class="{{ $optionsClass }}">
                         @foreach($options as $optIndex => $opt)
                             @php $label = chr(65 + (int)$optIndex); @endphp
                             <div class="option option-compact">
-                                <strong>{{ $label }}.</strong>&nbsp;{!! \App\Services\MathFormulaProcessor::processFormulas($opt) !!}
+                                <strong>{{ $label }}.</strong>&nbsp;{!! $mathProcessed ? $opt : \App\Services\MathFormulaProcessor::processFormulas($opt) !!}
                             </div>
                         @endforeach
                     </div>
@@ -98,15 +138,15 @@
                 @if($gradingMode)
                     @php
                         $solutionText = trim($q->solution ?? '');
-                        // 去掉前置的“解题思路”标签,避免出现“解题思路:【解题思路】”重复
+                        // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
                         $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
                         $solutionHtml = $solutionText === ''
                             ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
-                            : \App\Services\MathFormulaProcessor::processFormulas($solutionText);
+                            : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
                     @endphp
                     <div class="question-lead spacer"></div>
                     <div class="answer-meta">
-                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
+                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
                         <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
                     </div>
                 @endif
@@ -141,7 +181,7 @@
             if ($renderedContent === $q->content) {
                 $renderedContent .= ' ' . $blankSpan;
             }
-            $renderedContent = \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
+            $renderedContent = $mathProcessed ? $renderedContent : \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
         @endphp
         <div class="question">
             <div class="question-grid">
@@ -161,11 +201,11 @@
                         $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
                         $solutionHtml = $solutionText === ''
                             ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
-                            : \App\Services\MathFormulaProcessor::processFormulas($solutionText);
+                            : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
                     @endphp
                     <div class="question-lead spacer"></div>
                     <div class="answer-meta">
-                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
+                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
                         <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
                     </div>
                 @endif
@@ -202,7 +242,7 @@
                     @unless($gradingMode)
                         <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)</span>
                     @endunless
-                    <span class="question-stem">{!! \App\Services\MathFormulaProcessor::processFormulas($q->content) !!}</span>
+                    <span class="question-stem">{!! $mathProcessed ? $q->content : \App\Services\MathFormulaProcessor::processFormulas($q->content) !!}</span>
                 </div>
                 @unless($gradingMode)
                     <div class="question-lead spacer"></div>
@@ -213,7 +253,7 @@
                 @if($gradingMode)
                     @php
                         $solutionRaw = $q->solution ?? '';
-                        $solutionProcessed = \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
+                        $solutionProcessed = $mathProcessed ? $solutionRaw : \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
                         // 去掉分步得分等分值标记
                         $solutionProcessed = preg_replace('/(\s*\d+\s*分\s*)/u', '', $solutionProcessed);
 
@@ -249,7 +289,7 @@
                     @endphp
                     <div class="question-lead spacer"></div>
                     <div class="answer-meta">
-                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
+                        <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
                         <div class="answer-line solution-parsed">{!! $solutionProcessed !!}</div>
                     </div>
                 @endif

+ 12 - 2
resources/views/pdf/exam-grading.blade.php

@@ -86,11 +86,21 @@
         .question-score { margin-right: 6px; font-weight: 600; }
         .question-stem { display: inline-block; font-size: 14px; font-family: inherit; }
         .options { display: grid; row-gap: 8px; margin-top: 8px; }
+        .options-grid-4 {
+            display: grid;
+            grid-template-columns: repeat(4, 1fr);
+            gap: 8px 12px;
+        }
         .options-grid-2 {
             display: grid;
             grid-template-columns: 1fr 1fr;
             gap: 8px 20px;
         }
+        .options-grid-1 {
+            display: grid;
+            grid-template-columns: 1fr;
+            gap: 8px;
+        }
         .option { display: flex; align-items: flex-start; font-size: 14px; line-height: 1.6; }
         .option strong { margin-right: 4px; }
         .option-compact { font-size: 14px; line-height: 1.6; }
@@ -198,8 +208,8 @@
                     delimiters: [
                         {left: '$$', right: '$$', display: true},
                         {left: '$', right: '$', display: false},
-                        {left: '\\\\(', right: '\\\\)', display: false},
-                        {left: '\\\\[', right: '\\\\]', display: true}
+                        {left: '\\(', right: '\\)', display: false},
+                        {left: '\\[', right: '\\]', display: true}
                     ],
                     throwOnError: false,
                     strict: false,

+ 5 - 0
resources/views/pdf/exam-paper.blade.php

@@ -138,6 +138,11 @@
             grid-template-columns: 1fr 1fr;
             gap: 8px 20px;
         }
+        .options-grid-1 {
+            display: grid;
+            grid-template-columns: 1fr;
+            gap: 8px;
+        }
         .option {
             width: 100%;
             font-size: 14px;