with(['bank', 'options', 'tags'])->latest(); if ($request->user()->role !== 'admin') { $query->whereHas('bank', fn ($bankQuery) => $bankQuery->where('owner_id', $request->user()->id)); } if ($bankId = $request->query('question_bank_id')) { $query->where('question_bank_id', $bankId); } if ($type = $request->query('type')) { $query->where('type', $type); } if ($keyword = $request->query('keyword')) { $query->where('content', 'like', '%'.$keyword.'%'); } if ($request->filled('is_active')) { $query->where('is_active', $request->boolean('is_active')); } return ApiResponse::page($query->paginate((int) $request->query('per_page', 20))); } #[Apidoc\Title('创建题目')] #[Apidoc\Url('/api/admin/questions')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['permission:questions.import'])] public function store(Request $request, QuestionImportService $service): JsonResponse { $data = $request->validate([ 'question_bank_id' => ['required', 'exists:question_banks,id'], 'content' => ['required', 'string'], 'type' => ['required', 'in:single,multiple,judge,blank'], 'explanation' => ['nullable', 'string'], 'options' => ['array'], 'answers' => ['array'], ]); $bank = QuestionBank::findOrFail($data['question_bank_id']); $this->authorizeBankOwner($request, $bank); $job = $service->importRows($bank, $request->user(), [[ 'content' => $data['content'], 'type' => $data['type'], 'explanation' => $data['explanation'] ?? null, 'options' => $data['options'] ?? [], 'answer' => implode('|', $data['answers'] ?? []), ]], 'manual'); return ApiResponse::success($job->load('bank'), '题目已创建'); } #[Apidoc\Title('批量导入题目')] #[Apidoc\Url('/api/admin/banks/{bank}/imports')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['permission:questions.import'])] public function import(Request $request, mixed $bank, QuestionImportService $service): JsonResponse { $bank = $this->resolveBank($bank); $this->authorizeBankOwner($request, $bank); $request->validate([ 'file' => ['required', 'file', 'mimes:json,xlsx,xls,csv,txt'], ]); $job = $service->importUploadedFile($bank, $request->user(), $request->file('file')); OperationLog::create([ 'user_id' => $request->user()->id, 'action' => 'questions.imported', 'target_type' => QuestionBank::class, 'target_id' => $bank->id, 'ip' => $request->ip(), 'payload' => ['job_id' => $job->id, 'success_count' => $job->success_count, 'skipped_count' => $job->skipped_count], ]); return ApiResponse::success($job, '导入完成'); } #[Apidoc\Title('校验导入题目')] #[Apidoc\Url('/api/admin/banks/{bank}/imports/validate')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['permission:questions.import'])] public function validateImport(Request $request, mixed $bank, QuestionImportService $service): JsonResponse { $bank = $this->resolveBank($bank); $this->authorizeBankOwner($request, $bank); $request->validate([ 'file' => ['required', 'file', 'mimes:json,xlsx,xls,csv,txt'], ]); $prepared = $service->prepareUploadedFile($request->file('file')); return ApiResponse::success([ ...$service->validateRows($prepared['rows']), 'type' => $prepared['type'], 'file_path' => $prepared['path'], ], '校验完成'); } #[Apidoc\Title('提交已校验题目')] #[Apidoc\Url('/api/admin/banks/{bank}/imports/rows')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['permission:questions.import'])] public function importRows(Request $request, mixed $bank, QuestionImportService $service): JsonResponse { $bank = $this->resolveBank($bank); $this->authorizeBankOwner($request, $bank); $data = $request->validate([ 'rows' => ['required', 'array'], 'type' => ['nullable', 'string'], 'file_path' => ['nullable', 'string'], ]); $validation = $service->validateRows($data['rows']); if (! $validation['valid']) { return ApiResponse::success($validation, '校验未通过'); } $job = $service->importRows($bank, $request->user(), $data['rows'], $data['type'] ?? 'manual', $data['file_path'] ?? null); return ApiResponse::success($job, '导入完成'); } #[Apidoc\Title('校验已编辑题目')] #[Apidoc\Url('/api/admin/banks/{bank}/imports/rows/validate')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['permission:questions.import'])] public function validateRows(Request $request, mixed $bank, QuestionImportService $service): JsonResponse { $bank = $this->resolveBank($bank); $this->authorizeBankOwner($request, $bank); $data = $request->validate([ 'rows' => ['required', 'array'], ]); return ApiResponse::success($service->validateRows($data['rows']), '校验完成'); } private function resolveBank(mixed $bank): QuestionBank { if ($bank instanceof QuestionBank && $bank->exists) { return $bank; } return QuestionBank::query()->findOrFail((int) $bank); } #[Apidoc\Title('更新题目状态')] #[Apidoc\Url('/api/admin/questions/{question}')] #[Apidoc\Method('PUT')] #[Apidoc\RouteMiddleware(['permission:banks.update'])] public function update(Request $request, mixed $question): JsonResponse { $question = $this->resolveQuestion($question); $this->authorizeQuestionOwner($request, $question); $data = $request->validate([ 'content' => ['sometimes', 'string'], 'type' => ['sometimes', 'in:single,multiple,judge,blank'], 'explanation' => ['nullable', 'string'], 'is_active' => ['sometimes', 'boolean'], 'options' => ['sometimes', 'array'], 'options.*.text' => ['nullable', 'string'], 'options.*.content' => ['nullable', 'string'], 'options.*.correct' => ['nullable', 'boolean'], 'options.*.is_correct' => ['nullable', 'boolean'], 'answers' => ['sometimes', 'array'], ]); DB::transaction(function () use ($question, $data): void { $question->update(collect($data)->only(['content', 'type', 'explanation', 'is_active'])->all()); if (array_key_exists('answers', $data)) { $question->update(['answers' => array_values(array_filter($data['answers']))]); } if (array_key_exists('options', $data)) { $question->options()->delete(); foreach ($data['options'] as $sort => $option) { $content = trim((string) ($option['text'] ?? $option['content'] ?? '')); if ($content === '') { continue; } $question->options()->create([ 'content' => $content, 'is_correct' => (bool) ($option['correct'] ?? $option['is_correct'] ?? false), 'sort' => $sort, ]); } } }); return ApiResponse::success($question->fresh('options'), '题目已更新'); } #[Apidoc\Title('删除题目')] #[Apidoc\Url('/api/admin/questions/{question}')] #[Apidoc\Method('DELETE')] #[Apidoc\RouteMiddleware(['permission:banks.update'])] public function destroy(Request $request, mixed $question): JsonResponse { $question = $this->resolveQuestion($question); $this->authorizeQuestionOwner($request, $question); $question->delete(); OperationLog::create([ 'user_id' => $request->user()->id, 'action' => 'question.deleted', 'target_type' => Question::class, 'target_id' => $question->id, 'ip' => $request->ip(), 'payload' => ['question_bank_id' => $question->question_bank_id], ]); return ApiResponse::success(null, '题目已删除'); } private function resolveQuestion(mixed $question): Question { if ($question instanceof Question && $question->exists) { return $question; } return Question::query()->findOrFail((int) $question); } }