make(Illuminate\Contracts\Console\Kernel::class); $kernel->bootstrap(); use App\Services\ExamPdfExportService; use Illuminate\Support\Facades\DB; $options = getopt('', [ 'count::', 'detail::', 'connection::', 'table::', ]); $count = isset($options['count']) ? max(1, min(100, (int) $options['count'])) : 30; $connection = isset($options['connection']) ? trim((string) $options['connection']) : config('database.default'); $table = isset($options['table']) ? trim((string) $options['table']) : 'questions'; $detailPath = isset($options['detail']) ? trim((string) $options['detail']) : ''; if ($detailPath === '') { $auditDir = dirname(__DIR__).'/storage/app/audit_placeholder'; $glob = glob($auditDir.'/rendered_placeholder_audit_details_*.ndjson') ?: []; if ($glob === []) { fwrite(STDERR, "No detail ndjson found under {$auditDir}. Run scripts/audit_rendered_placeholder_integrity.php first.\n"); exit(1); } usort($glob, static fn(string $a, string $b): int => strcmp($b, $a)); $detailPath = $glob[0]; } if (! is_readable($detailPath)) { fwrite(STDERR, "Cannot read detail file: {$detailPath}\n"); exit(1); } $priorityTypes = [ 'internal_placeholder_token_leak', 'broken_left_right_split', 'span_then_dollar_before_han', 'blank_between_math_segments', 'unbalanced_dollar_after_render', 'math_ends_with_operator_before_blank', ]; $buckets = array_fill_keys($priorityTypes, []); $fh = fopen($detailPath, 'rb'); if ($fh === false) { fwrite(STDERR, "Failed to open {$detailPath}\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['issue'], $row['id'])) { continue; } $issue = (string) $row['issue']; $id = (int) $row['id']; if ($id <= 0 || ! isset($buckets[$issue])) { continue; } $buckets[$issue][] = $id; } fclose($fh); foreach ($buckets as $issue => &$ids) { $seen = []; $unique = []; foreach ($ids as $id) { if (! isset($seen[$id])) { $seen[$id] = true; $unique[] = $id; } } $ids = $unique; } unset($ids); $selectedOrder = []; $selectedSet = []; while (count($selectedOrder) < $count) { $progress = false; foreach ($priorityTypes as $type) { if (count($selectedOrder) >= $count) { break 2; } while ($buckets[$type] !== []) { $id = array_shift($buckets[$type]); if (! isset($selectedSet[$id])) { $selectedSet[$id] = true; $selectedOrder[] = $id; $progress = true; break; } } } if (! $progress) { break; } } if ($selectedOrder === []) { fwrite(STDERR, "No issue rows parsed from {$detailPath}; nothing to sample.\n"); exit(1); } if (count($selectedOrder) < $count) { fwrite(STDERR, 'warning: only '.count($selectedOrder)." unique ids available (requested {$count}).\n"); } $questions = DB::connection($connection) ->table($table) ->whereIn('id', $selectedOrder) ->get(); $questionMap = []; foreach ($questions as $q) { $questionMap[(int) $q->id] = $q; } $missing = array_values(array_filter($selectedOrder, static fn(int $id): bool => ! isset($questionMap[$id]))); if ($missing !== []) { fwrite(STDERR, 'warning: ids not found in '.$table.' ('.$connection.'): '.implode(',', $missing)."\n"); } $groupedQuestions = groupQuestionsByType($questionMap, $selectedOrder); $paper = buildVirtualPaper('占位符抽样质检_'.count($selectedOrder).'题', 'placeholder_audit_sample', $groupedQuestions); /** @var ExamPdfExportService $pdf */ $pdf = $app->make(ExamPdfExportService::class); $result = $pdf->generateQuestionCheckPdf( $paper, $groupedQuestions, ['name' => '质检抽样', 'grade' => '________'], ['name' => '________'] ); $manifestPath = dirname(__DIR__).'/storage/app/audit_placeholder/sample_pdf_manifest_'.date('Ymd_His').'.json'; $manifest = [ 'generated_at' => date('c'), 'detail_source' => $detailPath, 'connection' => $connection, 'table' => $table, 'requested_count' => $count, 'sampled_question_ids' => $selectedOrder, 'pdf_result' => $result, ]; @mkdir(dirname($manifestPath), 0777, true); file_put_contents($manifestPath, json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); echo json_encode([ 'detail_source' => $detailPath, 'manifest_path' => $manifestPath, 'sampled_ids' => $selectedOrder, 'pdf' => $result, ], 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.'_'.time().'_'.uniqid(); return (object) [ 'paper_id' => $paperId, 'paper_name' => $paperName, 'total_score' => $totalScore, 'total_questions' => $totalQuestions, 'created_at' => now()->toDateTimeString(), ]; }