|
|
@@ -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,263 @@ class ExamPdfController extends Controller
|
|
|
'includeAnswer' => true,
|
|
|
]);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 知识点讲解视图
|
|
|
+ */
|
|
|
+ public function showKnowledgeExplanation(Request $request, $paper_id)
|
|
|
+ {
|
|
|
+ // 获取试卷数据
|
|
|
+ $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
|
|
|
+
|
|
|
+ if (! $paper) {
|
|
|
+ abort(404, '试卷未找到');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取15位paper_id数字部分作为学案编号
|
|
|
+ $rawPaperId = $paper->paper_id ?? $paper_id;
|
|
|
+ preg_match('/paper_(\d{15})/', (string) $rawPaperId, $matches);
|
|
|
+ $examCode = $matches[1] ?? preg_replace('/[^0-9]/', '', (string) $rawPaperId);
|
|
|
+
|
|
|
+ // 生成日期
|
|
|
+ $generateDate = now()->locale('zh_CN')->isoFormat('M月D日');
|
|
|
+
|
|
|
+ // 提取并去重知识点代码(优先 paper_questions.knowledge_point,缺失时回退到题库 kp_code)
|
|
|
+ $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id)->get();
|
|
|
+ $kpCodes = [];
|
|
|
+ $seen = [];
|
|
|
+
|
|
|
+ $questionBankIds = $paperQuestions
|
|
|
+ ->pluck('question_bank_id')
|
|
|
+ ->filter()
|
|
|
+ ->unique()
|
|
|
+ ->values();
|
|
|
+ $questionKpMap = [];
|
|
|
+ if ($questionBankIds->isNotEmpty()) {
|
|
|
+ $questionKpMap = \App\Models\Question::whereIn('id', $questionBankIds)
|
|
|
+ ->pluck('kp_code', 'id')
|
|
|
+ ->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($paperQuestions as $pq) {
|
|
|
+ $kpCode = trim((string) ($pq->knowledge_point ?? ''));
|
|
|
+ if ($kpCode === '' && ! empty($pq->question_bank_id)) {
|
|
|
+ $kpCode = trim((string) ($questionKpMap[$pq->question_bank_id] ?? ''));
|
|
|
+ }
|
|
|
+ if ($kpCode === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (isset($seen[$kpCode])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $seen[$kpCode] = true;
|
|
|
+ $kpCodes[] = $kpCode;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用 ExamPdfExportService 构建知识点数据
|
|
|
+ $pdfService = app(\App\Services\ExamPdfExportService::class);
|
|
|
+ // 获取知识点名称映射
|
|
|
+ $kpNameMap = [];
|
|
|
+ try {
|
|
|
+ $kpNameMap = app(\App\Services\QuestionServiceApi::class)->getKnowledgePointOptions();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ // 静默失败,使用 code 作为名称
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量获取知识点讲解
|
|
|
+ $explanations = $pdfService->buildExplanations($kpCodes);
|
|
|
+
|
|
|
+ $knowledgePoints = [];
|
|
|
+ foreach ($kpCodes as $kpCode) {
|
|
|
+ $kpData = [
|
|
|
+ 'kp_code' => $kpCode,
|
|
|
+ 'kp_name' => $kpNameMap[$kpCode] ?? $kpCode,
|
|
|
+ 'explanation' => $explanations[$kpCode] ?? '',
|
|
|
+ ];
|
|
|
+
|
|
|
+ $knowledgePoints[] = $kpData;
|
|
|
+ }
|
|
|
+
|
|
|
+ return view('pdf.exam-knowledge-explanation', [
|
|
|
+ 'paperId' => $paper_id,
|
|
|
+ 'examCode' => $examCode ?: $paper_id,
|
|
|
+ 'generateDate' => $generateDate,
|
|
|
+ 'knowledgePoints' => $knowledgePoints,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重新生成 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);
|
|
|
+
|
|
|
+ // 是否包含知识点讲解(可选):未传参则使用 config/pdf.php 默认值
|
|
|
+ $includeKpExplain = null;
|
|
|
+ if ($request->has('include_kp_explain')) {
|
|
|
+ $includeKpExplain = filter_var(
|
|
|
+ $request->input('include_kp_explain'),
|
|
|
+ FILTER_VALIDATE_BOOLEAN,
|
|
|
+ FILTER_NULL_ON_FAILURE
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成统一 PDF(卷子 + 判卷)
|
|
|
+ $pdfUrl = $pdfService->generateUnifiedPdf($paper_id, $includeKpExplain);
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|