compare_question_pdf_two_paths.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <?php
  2. /**
  3. * 同一 questions.id:输出两份本地 PDF 供对比
  4. * A) 题目质检(pdf.question-check → generateQuestionCheckPdf)
  5. * B) 常规学生卷页面(pdf.exam-paper,含装订线与卷头,与组卷导出同源样式)
  6. *
  7. * 用法:
  8. * php scripts/compare_question_pdf_two_paths.php --id=34728
  9. * [--out-dir storage/app/audit_placeholder/pdf_compare]
  10. */
  11. declare(strict_types=1);
  12. require __DIR__.'/../vendor/autoload.php';
  13. $app = require __DIR__.'/../bootstrap/app.php';
  14. $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
  15. $kernel->bootstrap();
  16. use App\Services\ExamPdfExportService;
  17. use App\Services\KatexRenderer;
  18. use Illuminate\Support\Facades\DB;
  19. $options = getopt('', ['id::', 'out-dir::', 'connection::', 'table::']);
  20. $questionId = isset($options['id']) ? max(1, (int) $options['id']) : 34728;
  21. $connection = isset($options['connection']) ? trim((string) $options['connection']) : config('database.default');
  22. $table = isset($options['table']) ? trim((string) $options['table']) : 'questions';
  23. $outRoot = isset($options['out-dir']) ? rtrim((string) $options['out-dir'], '/') : dirname(__DIR__).'/storage/app/audit_placeholder/pdf_compare';
  24. if ($outRoot === '' || ($outRoot[0] !== '/' && ! preg_match('#^[A-Za-z]:[\\\\/]#', $outRoot))) {
  25. $outRoot = dirname(__DIR__).'/'.ltrim($outRoot, '/');
  26. }
  27. $stamp = date('Ymd_His');
  28. $runDir = $outRoot.'/'.$stamp.'_q'.$questionId;
  29. if (! @mkdir($runDir, 0775, true) && ! is_dir($runDir)) {
  30. fwrite(STDERR, "Cannot mkdir {$runDir}\n");
  31. exit(1);
  32. }
  33. $row = DB::connection($connection)->table($table)->where('id', $questionId)->first();
  34. if ($row === null) {
  35. fwrite(STDERR, "Question id {$questionId} not found.\n");
  36. exit(1);
  37. }
  38. $questionMap = [(int) $row->id => $row];
  39. $grouped = groupQuestionsByType($questionMap, [$questionId]);
  40. $paper = buildVirtualPaper('PDF对比_'.$questionId, 'pdf_compare_'.$stamp, $grouped);
  41. /** @var ExamPdfExportService $pdfService */
  42. $pdfService = $app->make(ExamPdfExportService::class);
  43. // --- A 题目质检 ---
  44. $pdfMetaCheck = [
  45. 'student_name' => '对比',
  46. 'exam_code' => 'QC_'.$questionId,
  47. 'assemble_type_label' => '题目质检',
  48. 'header_title' => '对比|QC_'.$questionId.'|题目质检',
  49. 'exam_pdf_title' => '对比_QC_'.$questionId,
  50. 'grading_pdf_title' => '对比_QC_'.$questionId,
  51. 'knowledge_pdf_title' => '对比_QC_'.$questionId,
  52. ];
  53. $htmlCheck = view('pdf.question-check', [
  54. 'paper' => $paper,
  55. 'questions' => $grouped,
  56. 'student' => ['name' => '对比', 'grade' => '________'],
  57. 'teacher' => ['name' => '________'],
  58. 'pdfMeta' => $pdfMetaCheck,
  59. ])->render();
  60. $resultA = $pdfService->generateQuestionCheckPdf(
  61. $paper,
  62. $grouped,
  63. ['name' => '对比', 'grade' => '________'],
  64. ['name' => '________'],
  65. $runDir.'/A_question_check.pdf'
  66. );
  67. if (($resultA['local_path'] ?? '') === '') {
  68. fwrite(STDERR, "Path A failed: ".json_encode($resultA, JSON_UNESCAPED_UNICODE)."\n");
  69. exit(1);
  70. }
  71. // --- B 常规试卷(学生卷,无答案)---
  72. $pdfMetaExam = [
  73. 'exam_pdf_title' => '对比_常规卷_'.$questionId,
  74. 'exam_code' => 'EX_'.$questionId,
  75. 'student_name' => '对比',
  76. 'header_title' => '对比|EX_'.$questionId.'|常规卷',
  77. 'assemble_type_label' => '样式对比',
  78. ];
  79. $htmlExam = view('pdf.exam-paper', [
  80. 'paper' => $paper,
  81. 'questions' => $grouped,
  82. 'student' => ['name' => '对比', 'grade' => '________'],
  83. 'teacher' => ['name' => '________'],
  84. 'includeAnswer' => false,
  85. 'pdfMeta' => $pdfMetaExam,
  86. ])->render();
  87. $katex = new KatexRenderer();
  88. $htmlExam = $katex->renderHtml($htmlExam);
  89. $ref = new ReflectionClass($pdfService);
  90. $mEnsure = $ref->getMethod('ensureUtf8Html');
  91. $mEnsure->setAccessible(true);
  92. $mBuild = $ref->getMethod('buildPdf');
  93. $mBuild->setAccessible(true);
  94. $htmlExamUtf8 = $mEnsure->invoke($pdfService, $htmlExam);
  95. $binaryExam = $mBuild->invoke($pdfService, $htmlExamUtf8, true);
  96. if ($binaryExam === null || $binaryExam === '') {
  97. fwrite(STDERR, "Path B buildPdf returned empty.\n");
  98. exit(1);
  99. }
  100. $pathB = $runDir.'/B_exam_paper_student.pdf';
  101. file_put_contents($pathB, $binaryExam);
  102. // 可选:保存 HTML 便于 diff
  103. file_put_contents($runDir.'/A_question_check.html', $htmlCheck);
  104. file_put_contents($runDir.'/B_exam_paper.html', $htmlExam);
  105. echo json_encode([
  106. 'question_id' => $questionId,
  107. 'output_directory' => $runDir,
  108. 'A_question_check_pdf' => $resultA['local_path'],
  109. 'B_exam_paper_pdf' => $pathB,
  110. 'notes' => 'A=题目质检模板;B=常规组卷学生卷模板(exam-paper)。题干区均用 components.exam.paper-body。',
  111. ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)."\n";
  112. /**
  113. * @param array<int, object> $questionMap
  114. */
  115. function groupQuestionsByType(array $questionMap, array $originalOrder): array
  116. {
  117. $grouped = ['choice' => [], 'fill' => [], 'answer' => []];
  118. $n = 1;
  119. foreach ($originalOrder as $id) {
  120. if (! isset($questionMap[$id])) {
  121. continue;
  122. }
  123. $q = $questionMap[$id];
  124. $type = normalizeQuestionType($q->question_type ?? null);
  125. $grouped[$type][] = (object) [
  126. 'id' => $q->id,
  127. 'question_number' => $n++,
  128. 'content' => $q->stem,
  129. 'options' => is_string($q->options) ? json_decode($q->options, true) : ($q->options ?? []),
  130. 'answer' => $q->answer,
  131. 'solution' => $q->solution,
  132. 'score' => match ($type) {
  133. 'choice', 'fill' => 5,
  134. default => 10,
  135. },
  136. 'difficulty' => $q->difficulty,
  137. 'kp_code' => $q->kp_code,
  138. ];
  139. }
  140. return $grouped;
  141. }
  142. function normalizeQuestionType(?string $type): string
  143. {
  144. if (! $type) {
  145. return 'answer';
  146. }
  147. $type = strtolower(trim($type));
  148. $map = [
  149. 'choice' => 'choice', '选择题' => 'choice', 'single_choice' => 'choice', 'multiple_choice' => 'choice',
  150. 'fill' => 'fill', '填空题' => 'fill', 'blank' => 'fill',
  151. 'answer' => 'answer', '解答题' => 'answer', 'subjective' => 'answer',
  152. ];
  153. return $map[$type] ?? 'answer';
  154. }
  155. function buildVirtualPaper(string $paperName, string $studentId, array $groupedQuestions): object
  156. {
  157. $totalScore = 0;
  158. $totalQuestions = 0;
  159. foreach ($groupedQuestions as $questions) {
  160. foreach ($questions as $q) {
  161. $totalScore += $q->score;
  162. $totalQuestions++;
  163. }
  164. }
  165. // PaperNaming::extractExamCode 要求可解析为合法 15 位考试编码
  166. $paperId = 'paper_100000000000001';
  167. return (object) [
  168. 'paper_id' => $paperId,
  169. 'paper_name' => $paperName,
  170. 'total_score' => $totalScore,
  171. 'total_questions' => $totalQuestions,
  172. 'created_at' => now()->toDateTimeString(),
  173. ];
  174. }