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