| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- <?php
- /**
- * 将审计 priority 明细里的题目分批导出为本地 PDF(题目质检模板),不上传 CDN。
- *
- * 用法:
- * php scripts/dump_priority_issue_pdfs_local.php [--per-pdf 150]
- * [--file storage/app/audit_placeholder/rendered_placeholder_audit_priority_issues_*.ndjson]
- * [--out-dir storage/app/audit_placeholder/local_priority_pdfs]
- * [--connection mysql] [--table questions]
- *
- * 不指定 --file 时,取 storage/app/audit_placeholder 下最新的 priority_issues ndjson。
- * 每份 PDF 内题量由 --per-pdf 控制(默认 150,可改 50/200 等)。
- */
- 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\Support\PaperNaming;
- use Illuminate\Support\Facades\DB;
- $options = getopt('', [
- 'per-pdf::',
- 'file::',
- 'out-dir::',
- 'connection::',
- 'table::',
- ]);
- $perPdf = isset($options['per-pdf']) ? max(1, (int) $options['per-pdf']) : 150;
- $connection = isset($options['connection']) ? trim((string) $options['connection']) : config('database.default');
- $table = isset($options['table']) ? trim((string) $options['table']) : 'questions';
- $defaultOut = dirname(__DIR__).'/storage/app/audit_placeholder/local_priority_pdfs';
- $outDir = isset($options['out-dir']) ? rtrim((string) $options['out-dir'], '/') : $defaultOut;
- if ($outDir[0] !== '/') {
- $outDir = dirname(__DIR__).'/'.$outDir;
- }
- $ndjsonPath = isset($options['file']) ? trim((string) $options['file']) : '';
- if ($ndjsonPath === '') {
- $auditDir = dirname(__DIR__).'/storage/app/audit_placeholder';
- $glob = glob($auditDir.'/rendered_placeholder_audit_priority_issues_*.ndjson') ?: [];
- if ($glob === []) {
- fwrite(STDERR, "No priority ndjson under {$auditDir}. Run audit script first.\n");
- exit(1);
- }
- usort($glob, static fn(string $a, string $b): int => strcmp($b, $a));
- $ndjsonPath = $glob[0];
- }
- if (! is_readable($ndjsonPath)) {
- fwrite(STDERR, "Cannot read: {$ndjsonPath}\n");
- exit(1);
- }
- $orderedUniqueIds = [];
- $seen = [];
- $fh = fopen($ndjsonPath, 'rb');
- if ($fh === false) {
- fwrite(STDERR, "Failed to open {$ndjsonPath}\n");
- exit(1);
- }
- while (($line = fgets($fh)) !== false) {
- $line = trim($line);
- if ($line === '') {
- continue;
- }
- $row = json_decode($line, true);
- if (! is_array($row) || ! isset($row['id'])) {
- continue;
- }
- $id = (int) $row['id'];
- if ($id <= 0 || isset($seen[$id])) {
- continue;
- }
- $seen[$id] = true;
- $orderedUniqueIds[] = $id;
- }
- fclose($fh);
- if ($orderedUniqueIds === []) {
- fwrite(STDERR, "No question ids in {$ndjsonPath}\n");
- exit(1);
- }
- $batches = array_chunk($orderedUniqueIds, $perPdf);
- $stamp = date('Ymd_His');
- $runDir = $outDir.'/'.$stamp;
- if (! @mkdir($runDir, 0775, true) && ! is_dir($runDir)) {
- fwrite(STDERR, "Cannot mkdir {$runDir}\n");
- exit(1);
- }
- /** @var ExamPdfExportService $pdfService */
- $pdfService = $app->make(ExamPdfExportService::class);
- $written = [];
- $batchIndex = 0;
- foreach ($batches as $chunk) {
- $batchIndex++;
- $questions = DB::connection($connection)
- ->table($table)
- ->whereIn('id', $chunk)
- ->get();
- $questionMap = [];
- foreach ($questions as $q) {
- $questionMap[(int) $q->id] = $q;
- }
- $groupedQuestions = groupQuestionsByType($questionMap, $chunk);
- $firstId = $chunk[0];
- $lastId = $chunk[count($chunk) - 1];
- $safeTitle = PaperNaming::toSafeFilename(
- 'priority_issues_batch_'.$batchIndex.'_'.count($chunk).'qs_'.$firstId.'-'.$lastId
- );
- $filename = $safeTitle.'.pdf';
- $localPath = $runDir.'/'.$filename;
- $paper = buildVirtualPaper(
- '重点排查 PDF 第 '.$batchIndex.'/'.count($batches).' 批('.count($chunk).' 题)',
- 'priority_local_'.$stamp.'_b'.$batchIndex,
- $groupedQuestions
- );
- $result = $pdfService->generateQuestionCheckPdf(
- $paper,
- $groupedQuestions,
- ['name' => '本地导出', 'grade' => '________'],
- ['name' => '________'],
- $localPath
- );
- if (($result['local_path'] ?? '') === '') {
- fwrite(STDERR, "FAILED batch {$batchIndex}: ".json_encode($result, JSON_UNESCAPED_UNICODE)."\n");
- exit(1);
- }
- $written[] = [
- 'batch' => $batchIndex,
- 'count' => count($chunk),
- 'ids_range' => [$firstId, $lastId],
- 'local_path' => $localPath,
- 'bytes' => @filesize($localPath) ?: 0,
- ];
- fwrite(STDERR, "wrote batch {$batchIndex}/".count($batches)." → {$filename}\n");
- }
- $manifestPath = $runDir.'/manifest.json';
- file_put_contents($manifestPath, json_encode([
- 'generated_at' => date('c'),
- 'source_ndjson' => realpath($ndjsonPath) ?: $ndjsonPath,
- 'connection' => $connection,
- 'table' => $table,
- 'per_pdf' => $perPdf,
- 'total_questions' => count($orderedUniqueIds),
- 'batch_count' => count($batches),
- 'output_directory' => $runDir,
- 'batches' => $written,
- ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
- echo json_encode([
- 'output_directory' => $runDir,
- 'manifest_path' => $manifestPath,
- 'batch_count' => count($written),
- 'total_questions' => count($orderedUniqueIds),
- 'batches' => $written,
- ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)."\n";
- /**
- * @param array<int, object> $questionMap
- */
- function groupQuestionsByType(array $questionMap, array $originalOrder): array
- {
- $grouped = [
- 'choice' => [],
- 'fill' => [],
- 'answer' => [],
- ];
- $questionNumber = 1;
- foreach ($originalOrder as $id) {
- if (! isset($questionMap[$id])) {
- continue;
- }
- $q = $questionMap[$id];
- $type = normalizeQuestionType($q->question_type ?? null);
- $questionObj = (object) [
- 'id' => $q->id,
- 'question_number' => $questionNumber++,
- 'content' => $q->stem,
- 'options' => is_string($q->options) ? json_decode($q->options, true) : ($q->options ?? []),
- 'answer' => $q->answer,
- 'solution' => $q->solution,
- 'score' => getDefaultScore($type),
- 'difficulty' => $q->difficulty,
- 'kp_code' => $q->kp_code,
- ];
- $grouped[$type][] = $questionObj;
- }
- return $grouped;
- }
- function normalizeQuestionType(?string $type): string
- {
- if (! $type) {
- return 'answer';
- }
- $type = strtolower(trim($type));
- $typeMap = [
- 'choice' => 'choice',
- '选择题' => 'choice',
- 'single_choice' => 'choice',
- 'multiple_choice' => 'choice',
- 'fill' => 'fill',
- '填空题' => 'fill',
- 'blank' => 'fill',
- 'answer' => 'answer',
- '解答题' => 'answer',
- 'subjective' => 'answer',
- 'calculation' => 'answer',
- 'proof' => 'answer',
- ];
- return $typeMap[$type] ?? 'answer';
- }
- function getDefaultScore(string $type): int
- {
- return match ($type) {
- 'choice' => 5,
- 'fill' => 5,
- 'answer' => 10,
- default => 5,
- };
- }
- /**
- * @param array<string, array<int, object>> $groupedQuestions
- */
- 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++;
- }
- }
- $paperId = $studentId.'_'.uniqid();
- return (object) [
- 'paper_id' => $paperId,
- 'paper_name' => $paperName,
- 'total_score' => $totalScore,
- 'total_questions' => $totalQuestions,
- 'created_at' => now()->toDateTimeString(),
- ];
- }
|