withCount('questions')->latest(); if ($request->user()->role !== 'admin') { $query->where('owner_id', $request->user()->id); } return ApiResponse::page($query->paginate((int) $request->query('per_page', 20))); } #[Apidoc\Title('试卷详情')] #[Apidoc\Url('/api/admin/papers/{paper}')] #[Apidoc\Method('GET')] #[Apidoc\RouteMiddleware(['permission:papers'])] public function show(Request $request, mixed $paper): JsonResponse { $paper = $this->resolvePaper($paper); $this->authorizePaperOwner($request, $paper); return ApiResponse::success($paper->load('questions.options')->loadCount('questions')); } #[Apidoc\Title('创建固定试卷')] #[Apidoc\Url('/api/admin/papers')] #[Apidoc\Method('POST')] #[Apidoc\RouteMiddleware(['permission:papers'])] public function store(Request $request): JsonResponse { $data = $this->validatePaper($request); if (! empty($data['question_bank_id'])) { $this->authorizeBankOwner($request, QuestionBank::findOrFail($data['question_bank_id'])); } $this->authorizeQuestions($request, collect($data['questions'] ?? [])->pluck('id')->all()); $paper = Paper::create([ 'owner_id' => $request->user()->id, 'question_bank_id' => $data['question_bank_id'] ?? null, 'title' => $data['title'], 'description' => $data['description'] ?? null, 'duration_minutes' => $data['duration_minutes'] ?? null, 'attempt_limit' => $data['attempt_limit'] ?? null, 'is_active' => true, ]); $this->syncQuestions($paper, $data['questions'] ?? []); OperationLog::create([ 'user_id' => $request->user()->id, 'action' => 'paper.created', 'target_type' => Paper::class, 'target_id' => $paper->id, 'ip' => $request->ip(), 'payload' => ['title' => $paper->title], ]); return ApiResponse::success($paper->load('questions'), '试卷已创建'); } #[Apidoc\Title('更新固定试卷')] #[Apidoc\Url('/api/admin/papers/{paper}')] #[Apidoc\Method('PUT')] #[Apidoc\RouteMiddleware(['permission:papers'])] public function update(Request $request, mixed $paper): JsonResponse { $paper = $this->resolvePaper($paper); $this->authorizePaperOwner($request, $paper); $data = $this->validatePaper($request, true); if (array_key_exists('question_bank_id', $data) && $data['question_bank_id'] !== null) { $this->authorizeBankOwner($request, QuestionBank::findOrFail($data['question_bank_id'])); } if (array_key_exists('questions', $data)) { $this->authorizeQuestions($request, collect($data['questions'] ?? [])->pluck('id')->all()); } $paper->update([ ...collect($data) ->only(['title', 'description', 'question_bank_id', 'duration_minutes', 'attempt_limit', 'is_active']) ->all(), ]); if (array_key_exists('questions', $data)) { $this->syncQuestions($paper, $data['questions'] ?? []); } return ApiResponse::success($paper->fresh('questions')->loadCount('questions'), '试卷已更新'); } #[Apidoc\Title('删除固定试卷')] #[Apidoc\Url('/api/admin/papers/{paper}')] #[Apidoc\Method('DELETE')] #[Apidoc\RouteMiddleware(['permission:papers'])] public function destroy(Request $request, mixed $paper): JsonResponse { $paper = $this->resolvePaper($paper); $this->authorizePaperOwner($request, $paper); $paper->delete(); OperationLog::create([ 'user_id' => $request->user()->id, 'action' => 'paper.deleted', 'target_type' => Paper::class, 'target_id' => $paper->id, 'ip' => $request->ip(), ]); return ApiResponse::success(null, '试卷已删除'); } /** * @return array */ private function validatePaper(Request $request, bool $updating = false): array { return $request->validate([ 'title' => [$updating ? 'sometimes' : 'required', 'string', 'max:120'], 'description' => ['nullable', 'string'], 'question_bank_id' => ['nullable', 'exists:question_banks,id'], 'duration_minutes' => ['nullable', 'integer', 'min:1'], 'attempt_limit' => ['nullable', 'integer', 'min:1'], 'is_active' => ['sometimes', 'boolean'], 'questions' => ['array'], 'questions.*.id' => ['required_with:questions', 'exists:questions,id'], 'questions.*.score' => ['nullable', 'numeric', 'min:0'], ]); } /** * @param array $questionIds */ private function authorizeQuestions(Request $request, array $questionIds): void { if ($questionIds === []) { return; } $visibleQuestionCount = Question::query() ->whereIn('id', $questionIds) ->whereHas('bank', function ($query) use ($request): void { if ($request->user()->role !== 'admin') { $query->where('owner_id', $request->user()->id); } }) ->count(); abort_if($visibleQuestionCount !== count(array_unique($questionIds)), 403, '题目权限不足'); } /** * @param array $questions */ private function syncQuestions(Paper $paper, array $questions): void { $syncPayload = []; foreach ($questions as $sort => $question) { $syncPayload[$question['id']] = [ 'score' => $question['score'] ?? null, 'sort' => $sort, ]; } $paper->questions()->sync($syncPayload); } private function resolvePaper(mixed $paper): Paper { if ($paper instanceof Paper && $paper->exists) { return $paper; } return Paper::query()->findOrFail((int) $paper); } }