applyFilters(Question::query(), $filters); $paginator = $query->orderByDesc('id')->paginate($perPage, ['*'], 'page', $page); $data = $this->mapQuestions(collect($paginator->items())); return [ 'data' => $data, 'meta' => [ 'page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), 'total_pages' => $paginator->lastPage(), ], ]; } public function getQuestionById(int $id): ?array { $question = Question::find($id); if (!$question) { return null; } return $this->mapQuestion($question); } public function getQuestionByCode(string $questionCode): ?array { $question = Question::where('question_code', $questionCode)->first(); if (!$question) { return null; } return $this->mapQuestion($question); } public function updateQuestionByCode(string $questionCode, array $payload): bool { $question = Question::where('question_code', $questionCode)->first(); if (!$question) { return false; } $question->fill($this->normalizePayload($payload)); $question->save(); return true; } public function deleteQuestionByCode(string $questionCode): bool { $question = Question::where('question_code', $questionCode)->first(); if (!$question) { return false; } $question->delete(); return true; } public function deleteQuestionById(int $id): bool { $question = Question::find($id); if (!$question) { return false; } $question->delete(); return true; } public function searchQuestions(string $query, int $limit = 20): array { $questions = Question::query() ->search($query) ->orderByDesc('id') ->limit($limit) ->get(); return [ 'data' => $this->mapQuestions($questions), ]; } public function getQuestionsByIds(array $ids): array { if (empty($ids)) { return ['data' => []]; } $questions = Question::query() ->whereIn('id', $ids) ->orderByDesc('id') ->get(); return [ 'data' => $this->mapQuestions($questions), ]; } public function getQuestionsByKpCode(string $kpCode, int $limit = 100): array { $questions = Question::query() ->where('kp_code', $kpCode) ->orderByDesc('id') ->limit($limit) ->get(); return [ 'data' => $this->mapQuestions($questions), ]; } public function getStatistics(array $filters = []): array { $baseQuery = $this->applyFilters(Question::query(), $filters); $total = (clone $baseQuery)->count(); $byDifficulty = (clone $baseQuery) ->selectRaw('difficulty, COUNT(*) as total') ->groupBy('difficulty') ->pluck('total', 'difficulty') ->toArray(); $byTypeRaw = (clone $baseQuery) ->selectRaw('question_type, COUNT(*) as total') ->groupBy('question_type') ->pluck('total', 'question_type') ->toArray(); $byType = []; foreach ($byTypeRaw as $type => $count) { $label = $this->mapQuestionTypeLabel((string) $type); $byType[$label] = ($byType[$label] ?? 0) + $count; } $byKp = (clone $baseQuery) ->selectRaw('kp_code, COUNT(*) as total') ->groupBy('kp_code') ->pluck('total', 'kp_code') ->toArray(); $bySource = (clone $baseQuery) ->selectRaw('source, COUNT(*) as total') ->groupBy('source') ->pluck('total', 'source') ->toArray(); return [ 'total' => $total, 'by_difficulty' => $byDifficulty, 'by_type' => $byType, 'by_kp' => $byKp, 'by_source' => $bySource, ]; } public function generateQuestions(array $params): array { $kpCode = $params['kp_code'] ?? null; if (!$kpCode) { return [ 'success' => false, 'message' => '缺少知识点代码', ]; } $count = max(1, (int) ($params['count'] ?? 1)); $keyword = (string) ($params['keyword'] ?? ''); $type = $params['type'] ?? null; $difficulty = $params['difficulty'] ?? null; $skills = $params['skills'] ?? []; $solutionService = app(AiSolutionService::class); $created = []; DB::transaction(function () use ( $count, $kpCode, $keyword, $type, $difficulty, $skills, $solutionService, &$created, $params ) { for ($i = 1; $i <= $count; $i++) { $questionCode = $this->generateQuestionCode(); $stemSuffix = $keyword ? "({$keyword})" : ''; $stem = "【AI生成】{$kpCode} 题目 {$i}{$stemSuffix}"; $options = null; $answer = null; $questionType = $type ?? 'CALCULATION'; if (in_array($questionType, ['CHOICE', 'MULTIPLE_CHOICE'], true)) { $options = [ ['label' => 'A', 'text' => '选项 A'], ['label' => 'B', 'text' => '选项 B'], ['label' => 'C', 'text' => '选项 C'], ['label' => 'D', 'text' => '选项 D'], ]; $answer = 'A'; } $solution = $solutionService->generateSolution($stem, [ 'kp_code' => $kpCode, 'difficulty' => $difficulty, 'question_type' => $questionType, ]); $question = Question::create([ 'question_code' => $questionCode, 'kp_code' => $kpCode, 'stem' => $stem, 'options' => $options, 'answer' => $answer, 'solution' => $solution['solution'] ?? null, 'difficulty' => $difficulty, 'source' => 'ai::local', 'question_type' => $questionType, 'meta' => [ 'skills' => $skills, 'prompt_template' => $params['prompt_template'] ?? null, 'strategy' => $params['strategy'] ?? null, 'generated_at' => now()->toDateTimeString(), 'solution_steps' => $solution['steps'] ?? [], ], ]); $created[] = $this->mapQuestion($question); } }); return [ 'success' => true, 'message' => '生成完成', 'count' => count($created), 'data' => $created, ]; } public function importQuestions(array $questions): array { if (empty($questions)) { return [ 'success' => false, 'message' => '题目为空', 'count' => 0, ]; } $created = 0; DB::transaction(function () use ($questions, &$created) { foreach ($questions as $payload) { $questionCode = $payload['question_code'] ?? $this->generateQuestionCode(); $question = Question::firstOrNew(['question_code' => $questionCode]); $question->fill($this->normalizePayload($payload)); $question->save(); $created++; } }); return [ 'success' => true, 'message' => '导入完成', 'count' => $created, ]; } public function selectQuestionsForExam(int $totalQuestions, array $filters): array { $query = Question::query(); if (!empty($filters['kp_codes'])) { $query->whereIn('kp_code', $filters['kp_codes']); } if (!empty($filters['skills'])) { $skills = array_values(array_filter($filters['skills'])); if (!empty($skills)) { $query->where(function ($q) use ($skills) { foreach ($skills as $skill) { $q->orWhereJsonContains('meta->skills', $skill); } }); } } $questions = $query->get(); $selected = $this->applyRatioSelection($questions, $totalQuestions, $filters); return $this->mapQuestions(collect($selected)); } public function getKnowledgePointOptions(): array { return KnowledgePoint::query() ->orderBy('kp_code') ->pluck('name', 'kp_code') ->toArray(); } public function getSkillNameMapping(?string $kpCode = null): array { return []; } private function applyFilters($query, array $filters) { if (!empty($filters['kp_code'])) { $query->where('kp_code', $filters['kp_code']); } if (!empty($filters['difficulty'])) { $query->where('difficulty', $filters['difficulty']); } if (!empty($filters['type'])) { $query->where('question_type', $filters['type']); } if (!empty($filters['search'])) { $query->search($filters['search']); } return $query; } private function normalizePayload(array $payload): array { $normalized = [ 'question_code' => $payload['question_code'] ?? null, 'kp_code' => $payload['kp_code'] ?? null, 'stem' => $payload['stem'] ?? ($payload['content'] ?? ''), 'options' => $payload['options'] ?? null, 'answer' => $payload['answer'] ?? null, 'solution' => $payload['solution'] ?? null, 'difficulty' => $payload['difficulty'] ?? null, 'source' => $payload['source'] ?? null, 'tags' => $payload['tags'] ?? null, 'question_type' => $payload['question_type'] ?? ($payload['type'] ?? null), 'meta' => $payload['meta'] ?? null, ]; if (isset($payload['skills'])) { $meta = $normalized['meta'] ?? []; $meta['skills'] = is_array($payload['skills']) ? $payload['skills'] : array_filter(array_map('trim', explode(',', (string) $payload['skills']))); $normalized['meta'] = $meta; } return array_filter($normalized, static fn ($value) => $value !== null); } private function mapQuestions(Collection $questions): array { $kpCodes = $questions->pluck('kp_code')->filter()->unique()->values(); $kpMap = $this->resolveKnowledgePointNames($kpCodes->all()); return $questions->map(function (Question $question) use ($kpMap) { $data = $this->mapQuestion($question); $data['kp_name'] = $kpMap[$question->kp_code] ?? null; return $data; })->values()->all(); } private function mapQuestion(Question $question): array { $meta = $question->meta ?? []; $data = [ 'id' => $question->id, 'question_code' => $question->question_code, 'kp_code' => $question->kp_code, 'stem' => $question->stem, 'options' => $question->options, 'answer' => $question->answer, 'solution' => $question->solution, 'difficulty' => $question->difficulty, 'source' => $question->source, 'tags' => $question->tags, 'type' => $question->question_type, 'question_type' => $question->question_type, 'skills' => $meta['skills'] ?? [], 'meta' => $meta, 'created_at' => $question->created_at?->toDateTimeString(), 'updated_at' => $question->updated_at?->toDateTimeString(), ]; return MathFormulaProcessor::processQuestionData($data); } private function generateQuestionCode(): string { return 'Q' . Str::upper(Str::random(10)); } private function mapQuestionTypeLabel(string $type): string { return match (strtoupper($type)) { 'CHOICE' => '选择题', 'MULTIPLE_CHOICE' => '多选题', 'FILL_IN_THE_BLANK', 'FILL' => '填空题', 'CALCULATION', 'WORD_PROBLEM', 'ANSWER' => '解答题', 'PROOF' => '证明题', default => '其他', }; } private function applyRatioSelection(Collection $questions, int $totalQuestions, array $filters): array { $questionsByType = $questions->groupBy(fn (Question $q) => $q->question_type ?? 'CALCULATION'); $questionsByDifficulty = $questions->groupBy(function (Question $q) { $difficulty = (float) ($q->difficulty ?? 0); if ($difficulty <= 0.4) { return 'easy'; } if ($difficulty <= 0.7) { return 'medium'; } return 'hard'; }); $typeRatio = $filters['question_type_ratio'] ?? []; $difficultyRatio = $filters['difficulty_ratio'] ?? []; $selected = collect(); if (!empty($typeRatio)) { foreach ($typeRatio as $type => $ratio) { $bucket = $questionsByType->get($type, collect()); $count = (int) round($totalQuestions * (float) $ratio); $selected = $selected->merge($bucket->shuffle()->take($count)); } } if (!empty($difficultyRatio)) { foreach ($difficultyRatio as $key => $ratio) { $bucketKey = $this->normalizeDifficultyKey($key); $bucket = $questionsByDifficulty->get($bucketKey, collect()); $count = (int) round($totalQuestions * (float) $ratio); $selected = $selected->merge($bucket->shuffle()->take($count)); } } if ($selected->isEmpty()) { return $questions->shuffle()->take($totalQuestions)->values()->all(); } if ($selected->count() < $totalQuestions) { $missing = $totalQuestions - $selected->count(); $fill = $questions->diff($selected)->shuffle()->take($missing); $selected = $selected->merge($fill); } return $selected->values()->all(); } private function normalizeDifficultyKey(string $key): string { if (in_array($key, ['easy', 'medium', 'hard'], true)) { return $key; } $value = (float) $key; if ($value <= 0.4) { return 'easy'; } if ($value <= 0.7) { return 'medium'; } return 'hard'; } private function resolveKnowledgePointNames(array $kpCodes): array { $kpCodes = array_values(array_filter(array_unique($kpCodes))); if (empty($kpCodes)) { return []; } $cacheKey = 'kp-name-map-' . md5(implode('|', $kpCodes)); return Cache::remember($cacheKey, now()->addMinutes(30), function () use ($kpCodes) { $kpMap = KnowledgePoint::query() ->whereIn('kp_code', $kpCodes) ->pluck('name', 'kp_code') ->toArray(); $missing = array_values(array_filter($kpCodes, fn ($code) => empty($kpMap[$code]))); if (empty($missing)) { return $kpMap; } try { $api = app(KnowledgeServiceApi::class); $all = $api->listKnowledgePoints(); foreach ($all as $kp) { $code = $kp['kp_code'] ?? null; $name = $kp['cn_name'] ?? $kp['name'] ?? null; if ($code && $name && in_array($code, $missing, true)) { $kpMap[$code] = $name; } } } catch (\Throwable $e) { // Fallback: keep existing mapping } return $kpMap; }); } }