|
|
@@ -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'] ?? '';
|