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 $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(), ]; }