| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- <?php
- namespace App\Console\Commands;
- use App\Services\ExamPdfExportService;
- use App\Support\OptionLayoutDecider;
- use Illuminate\Console\Command;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\File;
- class GenerateOptionLayoutRegressionCommand extends Command
- {
- protected $signature = 'exam:generate-option-layout-regression
- {--connection=remote_mysql : 数据库连接}
- {--source=main : 题库来源 main|default|ai}
- {--question-type=choice : 题型过滤 choice|fill|answer|all}
- {--keep-ids= : 固定保留题目ID,逗号分隔,例如 38848,38772,38869}
- {--limit=40 : 抽题数量}
- {--grading=1 : 是否同时生成判卷PDF 1|0}
- {--student-id=REGRESSION : 学生ID}
- {--student-name=排版回归 : 学生姓名}
- {--student-grade=初三 : 学生年级}
- {--teacher-name=回归老师 : 教师姓名}
- {--paper-name=选项排版回归 : 试卷名称}
- {--report= : 报告输出路径(JSON)}';
- protected $description = '遍历题库复杂公式题,生成真实PDF并输出选项布局回归报告';
- public function handle(
- ExamPdfExportService $pdfExportService,
- OptionLayoutDecider $layoutDecider
- ): int {
- $connection = (string) $this->option('connection');
- $source = (string) $this->option('source');
- $limit = max(1, (int) $this->option('limit'));
- $questionType = (string) $this->option('question-type');
- $keepIds = $this->parseKeepIds((string) $this->option('keep-ids'));
- $includeGrading = (string) $this->option('grading') !== '0';
- $table = match ($source) {
- 'default' => 'questions_tem',
- 'ai' => 'questions_ai',
- default => 'questions',
- };
- $this->info("开始回归抽题: {$connection}.{$table}, type={$questionType}, limit={$limit}, keep=".count($keepIds));
- $questions = $this->fetchComplexQuestions($connection, $table, $limit, $questionType, $keepIds);
- if ($questions->isEmpty()) {
- $this->warn('未找到复杂公式题,请调整筛选条件或检查题库数据');
- return self::FAILURE;
- }
- $groupedQuestions = $this->groupQuestionsByType($questions);
- $paperId = 'layout_regression_'.date('YmdHis').'_'.substr(uniqid('', true), -6);
- $paper = $this->buildVirtualPaper(
- $paperId,
- (string) $this->option('paper-name'),
- $groupedQuestions
- );
- $student = [
- 'id' => (string) $this->option('student-id'),
- 'name' => (string) $this->option('student-name'),
- 'grade' => (string) $this->option('student-grade'),
- ];
- $teacher = ['name' => (string) $this->option('teacher-name')];
- $result = $pdfExportService->generateByQuestions(
- $paper,
- $groupedQuestions,
- $student,
- $teacher,
- $includeGrading
- );
- $reportPath = (string) ($this->option('report') ?: storage_path('app/regression/option-layout-'.$paperId.'.json'));
- File::ensureDirectoryExists(dirname($reportPath));
- File::put($reportPath, json_encode([
- 'generated_at' => now()->toDateTimeString(),
- 'paper_id' => $paperId,
- 'source' => [
- 'connection' => $connection,
- 'table' => $table,
- 'keep_ids' => $keepIds,
- ],
- 'summary' => [
- 'question_count' => $questions->count(),
- 'choice_count' => count($groupedQuestions['choice']),
- 'fill_count' => count($groupedQuestions['fill']),
- 'answer_count' => count($groupedQuestions['answer']),
- ],
- 'pdfs' => $result,
- 'layout_checks' => $this->buildLayoutChecks($groupedQuestions, $layoutDecider),
- ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).PHP_EOL);
- $this->info('回归PDF生成完成');
- $this->line('Exam PDF: '.($result['pdf_url'] ?? 'N/A'));
- $this->line('Grading PDF: '.($result['grading_pdf_url'] ?? 'N/A'));
- $this->line('Report JSON: '.$reportPath);
- return self::SUCCESS;
- }
- private function fetchComplexQuestions(string $connection, string $table, int $limit, string $questionType, array $keepIds): Collection
- {
- $kept = collect();
- if (! empty($keepIds)) {
- $keptRaw = DB::connection($connection)
- ->table($table)
- ->select(['id', 'question_type', 'kp_code', 'stem', 'options', 'answer', 'solution', 'difficulty'])
- ->whereIn('id', $keepIds)
- ->get()
- ->keyBy('id');
- foreach ($keepIds as $id) {
- if ($keptRaw->has($id)) {
- $kept->push($keptRaw->get($id));
- }
- }
- }
- $remainingLimit = max(0, $limit - $kept->count());
- $query = DB::connection($connection)
- ->table($table)
- ->select(['id', 'question_type', 'kp_code', 'stem', 'options', 'answer', 'solution', 'difficulty'])
- ->when($questionType !== 'all', function ($q) use ($questionType) {
- $q->where('question_type', $questionType);
- })
- ->when($questionType === 'choice', function ($q) {
- $q->whereNotNull('options')
- ->where('options', '!=', '')
- ->where('options', '!=', '[]')
- ->where('options', '!=', '{}');
- })
- ->where(function ($query) {
- $query->where('stem', 'like', '%\\frac%')
- ->orWhere('stem', 'like', '%\\sqrt%')
- ->orWhere('stem', 'like', '%\\log%')
- ->orWhere('stem', 'like', '%\\sin%')
- ->orWhere('stem', 'like', '%\\cos%')
- ->orWhere('stem', 'like', '%\\tan%')
- ->orWhere('stem', 'like', '%<img%')
- ->orWhere('options', 'like', '%\\frac%')
- ->orWhere('options', 'like', '%\\sqrt%')
- ->orWhere('options', 'like', '%\\log%')
- ->orWhere('options', 'like', '%\\sin%')
- ->orWhere('options', 'like', '%\\cos%')
- ->orWhere('options', 'like', '%\\tan%')
- ->orWhere('options', 'like', '%^%')
- ->orWhere('options', 'like', '%/%');
- })
- ->when(! empty($keepIds), function ($q) use ($keepIds) {
- $q->whereNotIn('id', $keepIds);
- })
- ->orderByDesc('id');
- $sampled = $remainingLimit > 0 ? $query->limit($remainingLimit)->get() : collect();
- return $kept->concat($sampled)->values();
- }
- private function groupQuestionsByType(Collection $questions): array
- {
- $grouped = [
- 'choice' => [],
- 'fill' => [],
- 'answer' => [],
- ];
- $questionNumber = 1;
- foreach ($questions as $q) {
- $type = $this->normalizeQuestionType($q->question_type);
- $grouped[$type][] = (object) [
- 'id' => $q->id,
- 'question_number' => $questionNumber++,
- 'content' => $q->stem,
- 'options' => $this->normalizeOptions($q->options),
- 'answer' => $q->answer,
- 'solution' => $q->solution,
- 'score' => $this->defaultScore($type),
- 'difficulty' => $q->difficulty,
- 'kp_code' => $q->kp_code,
- ];
- }
- return $grouped;
- }
- private function normalizeQuestionType(?string $type): string
- {
- $type = strtolower(trim((string) $type));
- return match ($type) {
- 'choice', '选择题', 'single_choice', 'multiple_choice' => 'choice',
- 'fill', '填空题', 'blank' => 'fill',
- default => 'answer',
- };
- }
- private function normalizeOptions(mixed $options): array
- {
- if (is_array($options)) {
- return array_values($options);
- }
- if (is_string($options) && trim($options) !== '') {
- $decoded = json_decode($options, true);
- if (is_array($decoded)) {
- return array_values($decoded);
- }
- }
- return [];
- }
- private function defaultScore(string $type): int
- {
- return match ($type) {
- 'answer' => 10,
- default => 5,
- };
- }
- private function buildVirtualPaper(string $paperId, string $paperName, array $groupedQuestions): object
- {
- $totalScore = 0;
- $totalQuestions = 0;
- foreach ($groupedQuestions as $items) {
- foreach ($items as $item) {
- $totalScore += (int) ($item->score ?? 0);
- $totalQuestions++;
- }
- }
- return (object) [
- 'paper_id' => $paperId,
- 'paper_name' => $paperName,
- 'total_score' => $totalScore,
- 'total_questions' => $totalQuestions,
- 'created_at' => now()->toDateTimeString(),
- ];
- }
- private function buildLayoutChecks(array $groupedQuestions, OptionLayoutDecider $layoutDecider): array
- {
- $rows = [];
- foreach (['choice', 'fill', 'answer'] as $type) {
- foreach ($groupedQuestions[$type] as $question) {
- $options = is_array($question->options ?? null) ? $question->options : [];
- if ($type !== 'choice' || empty($options)) {
- continue;
- }
- $examDecision = $layoutDecider->decide($options, 'exam');
- $gradingDecision = $layoutDecider->decide($options, 'grading');
- $rows[] = [
- 'question_id' => $question->id,
- 'question_number' => $question->question_number,
- 'kp_code' => $question->kp_code,
- 'options_count' => count($options),
- 'exam_layout' => $examDecision['class'],
- 'grading_layout' => $gradingDecision['class'],
- 'max_length_exam' => $examDecision['max_length'],
- 'has_complex_formula' => $examDecision['has_complex_formula'],
- ];
- }
- }
- return $rows;
- }
- /**
- * @return array<int>
- */
- private function parseKeepIds(string $keepIds): array
- {
- if (trim($keepIds) === '') {
- return [];
- }
- return collect(explode(',', $keepIds))
- ->map(fn ($id) => (int) trim($id))
- ->filter(fn ($id) => $id > 0)
- ->unique()
- ->values()
- ->all();
- }
- }
|