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', '%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 */ 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(); } }