Selaa lähdekoodia

fix: prevent non-choice stem option mis-splitting

Restrict stem/option splitting to choice questions and require four consecutive option markers while removing colon delimiters. This avoids formulas like \odot C: being misparsed as options during exam PDF rendering.

Made-with: Cursor
yemeishu 2 viikkoa sitten
vanhempi
commit
b065fe86b7
1 muutettua tiedostoa jossa 61 lisäystä ja 12 poistoa
  1. 61 12
      app/Http/Controllers/ExamPdfController.php

+ 61 - 12
app/Http/Controllers/ExamPdfController.php

@@ -216,9 +216,14 @@ class ExamPdfController extends Controller
         // 【修复】先移除SVG内容,避免误匹配SVG注释中的 BD:DC、A:B 等内容
         $contentWithoutSvg = preg_replace('/<svg[^>]*>.*?<\/svg>/is', '[SVG_PLACEHOLDER]', $content);
 
+        // 方案B:必须先检测到至少4个连续选项字母(如 A/B/C/D)才允许拆分选项。
+        if (! $this->hasConsecutiveOptionMarkers($contentWithoutSvg, 4)) {
+            return [];
+        }
+
         // 1. 尝试匹配多种格式的选项:A. / A、/ A: / A.(中文句点)/ A.(无空格)
         // 【修复】选项标记必须在行首或空白后,避免误匹配 SVG 注释中的 BD:DC 等内容
-        $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
+        $pattern = '/(?:^|\s)([A-D])[\.、.]\s*(.+?)(?=(?:^|\s)[A-D][\.、.]|$)/su';
 
         if (preg_match_all($pattern, $contentWithoutSvg, $matches, PREG_SET_ORDER)) {
             foreach ($matches as $match) {
@@ -240,7 +245,7 @@ class ExamPdfController extends Controller
             foreach ($lines as $line) {
                 $line = trim($line);
                 // 【修复】行首匹配选项标记
-                if (preg_match('/^([A-D])[\.、:]\s*(.+)$/u', $line, $match)) {
+                if (preg_match('/^([A-D])[\.、.]\s*(.+)$/u', $line, $match)) {
                     $optionText = trim($match[2]);
                     if (! empty($optionText)) {
                         $options[] = $optionText;
@@ -261,13 +266,18 @@ class ExamPdfController extends Controller
     /**
      * 分离题干内容和选项
      */
-    private function separateStemAndOptions(string $content): array
+    private function separateStemAndOptions(string $content, ?string $questionType = null): array
     {
+        // 仅选择题需要做“题干/选项拆分”,其他题型直接返回原始题干。
+        if ($questionType === null || $this->normalizeQuestionTypeValue($questionType) !== 'choice') {
+            return [$content, []];
+        }
+
         // 【修复】先移除SVG内容,避免误匹配SVG注释中的 BD:DC、A:B 等内容
         $contentWithoutSvg = preg_replace('/<svg[^>]*>.*?<\/svg>/is', '[SVG_PLACEHOLDER]', $content);
 
-        // 【修复】检测是否有选项时,要求选项标记在行首或空白后
-        $hasOptions = preg_match('/(?:^|\s)[A-D][\.、:.:]/u', $contentWithoutSvg);
+        // 方案B:必须至少命中4个连续选项字母(A→D)才判定为选择题选项区。
+        $hasOptions = $this->hasConsecutiveOptionMarkers($contentWithoutSvg, 4);
 
         if (! $hasOptions) {
             return [$content, []];
@@ -279,7 +289,7 @@ class ExamPdfController extends Controller
         // 如果提取到选项,分离题干
         if (! empty($options)) {
             // 【修复】找到第一个选项的位置,要求选项标记在行首或空白后
-            if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:])/su', $contentWithoutSvg, $match)) {
+            if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、.])/su', $contentWithoutSvg, $match)) {
                 $stem = trim($match[1]);
                 // 如果题干中有SVG占位符,从原始内容中提取对应部分
                 if (strpos($stem, '[SVG_PLACEHOLDER]') !== false) {
@@ -322,6 +332,45 @@ class ExamPdfController extends Controller
         return [$content, []];
     }
 
+    /**
+     * 检测题干中是否存在至少 N 个连续选项字母(如 A/B/C/D)。
+     */
+    private function hasConsecutiveOptionMarkers(string $content, int $minRun = 4): bool
+    {
+        if ($minRun <= 1) {
+            return preg_match('/(?:^|\s)[A-H][\.、.]/u', $content) === 1;
+        }
+
+        if (! preg_match_all('/(?:^|\s)([A-H])[\.、.]/u', $content, $matches)) {
+            return false;
+        }
+
+        $letters = array_map(static fn ($ch) => strtoupper((string) $ch), $matches[1] ?? []);
+        if (empty($letters)) {
+            return false;
+        }
+
+        $longestRun = 1;
+        $currentRun = 1;
+        for ($i = 1, $count = count($letters); $i < $count; $i++) {
+            $prev = ord($letters[$i - 1]);
+            $curr = ord($letters[$i]);
+            if ($curr === $prev + 1) {
+                $currentRun++;
+            } else {
+                $currentRun = 1;
+            }
+            if ($currentRun > $longestRun) {
+                $longestRun = $currentRun;
+            }
+            if ($longestRun >= $minRun) {
+                return true;
+            }
+        }
+
+        return $longestRun >= $minRun;
+    }
+
     /**
      * 根据题型获取默认分数
      */
@@ -555,7 +604,7 @@ class ExamPdfController extends Controller
                                 $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? '';
 
                                 // 分离题干和选项
-                                [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
+                                [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent, $q['question_type'] ?? null);
 
                                 $q['stem'] = $stem;
                                 $q['content'] = $stem; // 同时设置content字段
@@ -664,7 +713,7 @@ class ExamPdfController extends Controller
                             $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失';
 
                             // 分离题干和选项
-                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
+                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent, $q['question_type'] ?? null);
 
                             // 合并数据,优先使用题库API的 stem、answer、solution、options
                             $q['stem'] = $stem;
@@ -715,7 +764,7 @@ class ExamPdfController extends Controller
             $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
 
             // 分离题干和选项
-            [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent);
+            [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent, $q['question_type'] ?? null);
 
             // 如果从题库API获取了选项,优先使用
             $options = $q['options'] ?? $extractedOptions;
@@ -853,7 +902,7 @@ class ExamPdfController extends Controller
                         if (isset($responseDataMap[$q['id']])) {
                             $apiData = $responseDataMap[$q['id']];
                             $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? '';
-                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
+                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent, $q['question_type'] ?? null);
                             $q['stem'] = $stem;
                             $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
@@ -922,7 +971,7 @@ class ExamPdfController extends Controller
                         if (isset($responseDataMap[$q['id']])) {
                             $apiData = $responseDataMap[$q['id']];
                             $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失';
-                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
+                            [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent, $q['question_type'] ?? null);
                             $q['stem'] = $stem;
                             $q['content'] = $stem; // 同时设置content字段
                             $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
@@ -965,7 +1014,7 @@ class ExamPdfController extends Controller
         $questions = ['choice' => [], 'fill' => [], 'answer' => []];
         foreach ($questionsData as $q) {
             $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
-            [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent);
+            [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent, $q['question_type'] ?? null);
             $options = $q['options'] ?? $extractedOptions;
             $answer = $q['answer'] ?? '';
             $solution = $q['solution'] ?? '';