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