|
@@ -0,0 +1,203 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 同一 questions.id:输出两份本地 PDF 供对比
|
|
|
|
|
+ * A) 题目质检(pdf.question-check → generateQuestionCheckPdf)
|
|
|
|
|
+ * B) 常规学生卷页面(pdf.exam-paper,含装订线与卷头,与组卷导出同源样式)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用法:
|
|
|
|
|
+ * php scripts/compare_question_pdf_two_paths.php --id=34728
|
|
|
|
|
+ * [--out-dir storage/app/audit_placeholder/pdf_compare]
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+declare(strict_types=1);
|
|
|
|
|
+
|
|
|
|
|
+require __DIR__.'/../vendor/autoload.php';
|
|
|
|
|
+$app = require __DIR__.'/../bootstrap/app.php';
|
|
|
|
|
+$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
|
|
|
|
+$kernel->bootstrap();
|
|
|
|
|
+
|
|
|
|
|
+use App\Services\ExamPdfExportService;
|
|
|
|
|
+use App\Services\KatexRenderer;
|
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
|
|
+
|
|
|
|
|
+$options = getopt('', ['id::', 'out-dir::', 'connection::', 'table::']);
|
|
|
|
|
+
|
|
|
|
|
+$questionId = isset($options['id']) ? max(1, (int) $options['id']) : 34728;
|
|
|
|
|
+$connection = isset($options['connection']) ? trim((string) $options['connection']) : config('database.default');
|
|
|
|
|
+$table = isset($options['table']) ? trim((string) $options['table']) : 'questions';
|
|
|
|
|
+
|
|
|
|
|
+$outRoot = isset($options['out-dir']) ? rtrim((string) $options['out-dir'], '/') : dirname(__DIR__).'/storage/app/audit_placeholder/pdf_compare';
|
|
|
|
|
+if ($outRoot === '' || ($outRoot[0] !== '/' && ! preg_match('#^[A-Za-z]:[\\\\/]#', $outRoot))) {
|
|
|
|
|
+ $outRoot = dirname(__DIR__).'/'.ltrim($outRoot, '/');
|
|
|
|
|
+}
|
|
|
|
|
+$stamp = date('Ymd_His');
|
|
|
|
|
+$runDir = $outRoot.'/'.$stamp.'_q'.$questionId;
|
|
|
|
|
+if (! @mkdir($runDir, 0775, true) && ! is_dir($runDir)) {
|
|
|
|
|
+ fwrite(STDERR, "Cannot mkdir {$runDir}\n");
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$row = DB::connection($connection)->table($table)->where('id', $questionId)->first();
|
|
|
|
|
+if ($row === null) {
|
|
|
|
|
+ fwrite(STDERR, "Question id {$questionId} not found.\n");
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$questionMap = [(int) $row->id => $row];
|
|
|
|
|
+$grouped = groupQuestionsByType($questionMap, [$questionId]);
|
|
|
|
|
+$paper = buildVirtualPaper('PDF对比_'.$questionId, 'pdf_compare_'.$stamp, $grouped);
|
|
|
|
|
+
|
|
|
|
|
+/** @var ExamPdfExportService $pdfService */
|
|
|
|
|
+$pdfService = $app->make(ExamPdfExportService::class);
|
|
|
|
|
+
|
|
|
|
|
+// --- A 题目质检 ---
|
|
|
|
|
+$pdfMetaCheck = [
|
|
|
|
|
+ 'student_name' => '对比',
|
|
|
|
|
+ 'exam_code' => 'QC_'.$questionId,
|
|
|
|
|
+ 'assemble_type_label' => '题目质检',
|
|
|
|
|
+ 'header_title' => '对比|QC_'.$questionId.'|题目质检',
|
|
|
|
|
+ 'exam_pdf_title' => '对比_QC_'.$questionId,
|
|
|
|
|
+ 'grading_pdf_title' => '对比_QC_'.$questionId,
|
|
|
|
|
+ 'knowledge_pdf_title' => '对比_QC_'.$questionId,
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+$htmlCheck = view('pdf.question-check', [
|
|
|
|
|
+ 'paper' => $paper,
|
|
|
|
|
+ 'questions' => $grouped,
|
|
|
|
|
+ 'student' => ['name' => '对比', 'grade' => '________'],
|
|
|
|
|
+ 'teacher' => ['name' => '________'],
|
|
|
|
|
+ 'pdfMeta' => $pdfMetaCheck,
|
|
|
|
|
+])->render();
|
|
|
|
|
+
|
|
|
|
|
+$resultA = $pdfService->generateQuestionCheckPdf(
|
|
|
|
|
+ $paper,
|
|
|
|
|
+ $grouped,
|
|
|
|
|
+ ['name' => '对比', 'grade' => '________'],
|
|
|
|
|
+ ['name' => '________'],
|
|
|
|
|
+ $runDir.'/A_question_check.pdf'
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+if (($resultA['local_path'] ?? '') === '') {
|
|
|
|
|
+ fwrite(STDERR, "Path A failed: ".json_encode($resultA, JSON_UNESCAPED_UNICODE)."\n");
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// --- B 常规试卷(学生卷,无答案)---
|
|
|
|
|
+$pdfMetaExam = [
|
|
|
|
|
+ 'exam_pdf_title' => '对比_常规卷_'.$questionId,
|
|
|
|
|
+ 'exam_code' => 'EX_'.$questionId,
|
|
|
|
|
+ 'student_name' => '对比',
|
|
|
|
|
+ 'header_title' => '对比|EX_'.$questionId.'|常规卷',
|
|
|
|
|
+ 'assemble_type_label' => '样式对比',
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+$htmlExam = view('pdf.exam-paper', [
|
|
|
|
|
+ 'paper' => $paper,
|
|
|
|
|
+ 'questions' => $grouped,
|
|
|
|
|
+ 'student' => ['name' => '对比', 'grade' => '________'],
|
|
|
|
|
+ 'teacher' => ['name' => '________'],
|
|
|
|
|
+ 'includeAnswer' => false,
|
|
|
|
|
+ 'pdfMeta' => $pdfMetaExam,
|
|
|
|
|
+])->render();
|
|
|
|
|
+
|
|
|
|
|
+$katex = new KatexRenderer();
|
|
|
|
|
+$htmlExam = $katex->renderHtml($htmlExam);
|
|
|
|
|
+
|
|
|
|
|
+$ref = new ReflectionClass($pdfService);
|
|
|
|
|
+$mEnsure = $ref->getMethod('ensureUtf8Html');
|
|
|
|
|
+$mEnsure->setAccessible(true);
|
|
|
|
|
+$mBuild = $ref->getMethod('buildPdf');
|
|
|
|
|
+$mBuild->setAccessible(true);
|
|
|
|
|
+
|
|
|
|
|
+$htmlExamUtf8 = $mEnsure->invoke($pdfService, $htmlExam);
|
|
|
|
|
+$binaryExam = $mBuild->invoke($pdfService, $htmlExamUtf8, true);
|
|
|
|
|
+if ($binaryExam === null || $binaryExam === '') {
|
|
|
|
|
+ fwrite(STDERR, "Path B buildPdf returned empty.\n");
|
|
|
|
|
+ exit(1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$pathB = $runDir.'/B_exam_paper_student.pdf';
|
|
|
|
|
+file_put_contents($pathB, $binaryExam);
|
|
|
|
|
+
|
|
|
|
|
+// 可选:保存 HTML 便于 diff
|
|
|
|
|
+file_put_contents($runDir.'/A_question_check.html', $htmlCheck);
|
|
|
|
|
+file_put_contents($runDir.'/B_exam_paper.html', $htmlExam);
|
|
|
|
|
+
|
|
|
|
|
+echo json_encode([
|
|
|
|
|
+ 'question_id' => $questionId,
|
|
|
|
|
+ 'output_directory' => $runDir,
|
|
|
|
|
+ 'A_question_check_pdf' => $resultA['local_path'],
|
|
|
|
|
+ 'B_exam_paper_pdf' => $pathB,
|
|
|
|
|
+ 'notes' => 'A=题目质检模板;B=常规组卷学生卷模板(exam-paper)。题干区均用 components.exam.paper-body。',
|
|
|
|
|
+], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)."\n";
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @param array<int, object> $questionMap
|
|
|
|
|
+ */
|
|
|
|
|
+function groupQuestionsByType(array $questionMap, array $originalOrder): array
|
|
|
|
|
+{
|
|
|
|
|
+ $grouped = ['choice' => [], 'fill' => [], 'answer' => []];
|
|
|
|
|
+ $n = 1;
|
|
|
|
|
+ foreach ($originalOrder as $id) {
|
|
|
|
|
+ if (! isset($questionMap[$id])) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $q = $questionMap[$id];
|
|
|
|
|
+ $type = normalizeQuestionType($q->question_type ?? null);
|
|
|
|
|
+ $grouped[$type][] = (object) [
|
|
|
|
|
+ 'id' => $q->id,
|
|
|
|
|
+ 'question_number' => $n++,
|
|
|
|
|
+ 'content' => $q->stem,
|
|
|
|
|
+ 'options' => is_string($q->options) ? json_decode($q->options, true) : ($q->options ?? []),
|
|
|
|
|
+ 'answer' => $q->answer,
|
|
|
|
|
+ 'solution' => $q->solution,
|
|
|
|
|
+ 'score' => match ($type) {
|
|
|
|
|
+ 'choice', 'fill' => 5,
|
|
|
|
|
+ default => 10,
|
|
|
|
|
+ },
|
|
|
|
|
+ 'difficulty' => $q->difficulty,
|
|
|
|
|
+ 'kp_code' => $q->kp_code,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $grouped;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function normalizeQuestionType(?string $type): string
|
|
|
|
|
+{
|
|
|
|
|
+ if (! $type) {
|
|
|
|
|
+ return 'answer';
|
|
|
|
|
+ }
|
|
|
|
|
+ $type = strtolower(trim($type));
|
|
|
|
|
+ $map = [
|
|
|
|
|
+ 'choice' => 'choice', '选择题' => 'choice', 'single_choice' => 'choice', 'multiple_choice' => 'choice',
|
|
|
|
|
+ 'fill' => 'fill', '填空题' => 'fill', 'blank' => 'fill',
|
|
|
|
|
+ 'answer' => 'answer', '解答题' => 'answer', 'subjective' => 'answer',
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ return $map[$type] ?? 'answer';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildVirtualPaper(string $paperName, string $studentId, array $groupedQuestions): object
|
|
|
|
|
+{
|
|
|
|
|
+ $totalScore = 0;
|
|
|
|
|
+ $totalQuestions = 0;
|
|
|
|
|
+ foreach ($groupedQuestions as $questions) {
|
|
|
|
|
+ foreach ($questions as $q) {
|
|
|
|
|
+ $totalScore += $q->score;
|
|
|
|
|
+ $totalQuestions++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // PaperNaming::extractExamCode 要求可解析为合法 15 位考试编码
|
|
|
|
|
+ $paperId = 'paper_100000000000001';
|
|
|
|
|
+
|
|
|
|
|
+ return (object) [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'paper_name' => $paperName,
|
|
|
|
|
+ 'total_score' => $totalScore,
|
|
|
|
|
+ 'total_questions' => $totalQuestions,
|
|
|
|
|
+ 'created_at' => now()->toDateTimeString(),
|
|
|
|
|
+ ];
|
|
|
|
|
+}
|