resolveBank($bank); abort_if(! $access->canAccessBank($this->currentUser($request), $bank), 403); $data = $request->validate([ 'mode' => ['required', 'in:memorize,wrong_memorize,practice,wrong_practice,sequence,random,wrong_random'], 'category_id' => ['nullable', 'exists:question_categories,id'], 'type' => ['nullable'], 'type.*' => ['in:single,multiple,judge,blank'], 'tag_ids' => ['nullable', 'array'], 'tag_ids.*' => ['integer', 'exists:question_tags,id'], 'limit' => ['nullable', 'integer', 'min:1', 'max:100'], ]); return ApiResponse::success($service->startPractice($this->currentUser($request), $bank, $data['mode'], $data), '已开始'); } #[Apidoc\Title('题库标签')] #[Apidoc\Url('/app/banks/{bank}/tags')] #[Apidoc\Method('GET')] public function tags(Request $request, mixed $bank, LearningAccessService $access): JsonResponse { $bank = $this->resolveBank($bank); abort_if(! $access->canAccessBank($this->currentUser($request), $bank), 403); return ApiResponse::success($bank->tags()->orderBy('name')->get()); } #[Apidoc\Title('开始整卷测试')] #[Apidoc\Url('/app/papers/{paper}/attempts')] #[Apidoc\Method('POST')] public function startPaper(Request $request, mixed $paper, QuizService $service, LearningAccessService $access): JsonResponse { $paper = $this->resolvePaper($paper); abort_if(! $access->canAccessPaper($this->currentUser($request), $paper), 403); return ApiResponse::success($service->startPaper($this->currentUser($request), $paper), '已开始'); } #[Apidoc\Title('继续作答')] #[Apidoc\Url('/app/attempts/{attempt}')] #[Apidoc\Method('GET')] public function show(Request $request, mixed $attempt): JsonResponse { $attempt = $this->resolveAttempt($attempt); abort_if($attempt->user_id !== $this->currentUser($request)->id, 403); return ApiResponse::success($attempt->load('items.question.options')); } #[Apidoc\Title('提交单题答案')] #[Apidoc\Url('/app/attempts/{attempt}/answer')] #[Apidoc\Method('POST')] public function answer(Request $request, mixed $attempt, QuizService $service): JsonResponse { $attempt = $this->resolveAttempt($attempt); $data = $request->validate([ 'question_id' => ['required', 'exists:questions,id'], 'answer' => ['array'], 'duration_seconds' => ['nullable', 'integer', 'min:0'], ]); return ApiResponse::success( $service->answer($this->currentUser($request), $attempt, (int) $data['question_id'], $data['answer'] ?? [], (int) ($data['duration_seconds'] ?? 0)), '已作答', ); } #[Apidoc\Title('保存作答位置')] #[Apidoc\Url('/app/attempts/{attempt}/position')] #[Apidoc\Method('PUT')] public function updatePosition(Request $request, mixed $attempt): JsonResponse { $attempt = $this->resolveAttempt($attempt); $user = $this->currentUser($request); abort_if($attempt->user_id !== $user->id, 403); $data = $request->validate([ 'current_index' => ['required', 'integer', 'min:0'], ]); $maxIndex = max(0, $attempt->total_questions - 1); $attempt->update([ 'current_index' => min((int) $data['current_index'], $maxIndex), ]); return ApiResponse::success($attempt->fresh(), '位置已保存'); } #[Apidoc\Title('交卷')] #[Apidoc\Url('/app/attempts/{attempt}/submit')] #[Apidoc\Method('POST')] public function submit(Request $request, mixed $attempt, QuizService $service): JsonResponse { $attempt = $this->resolveAttempt($attempt); return ApiResponse::success($service->submit($this->currentUser($request), $attempt), '已交卷'); } #[Apidoc\Title('错题列表')] #[Apidoc\Url('/app/wrong-questions')] #[Apidoc\Method('GET')] public function wrongQuestions(Request $request): JsonResponse { return ApiResponse::page( WrongQuestion::query() ->where('user_id', $this->currentUser($request)->id) ->whereNull('mastered_at') ->when($request->query('question_bank_id'), fn ($query, $bankId) => $query->whereHas( 'question', fn ($questionQuery) => $questionQuery->where('question_bank_id', $bankId), )) ->with('question.options') ->latest() ->paginate((int) $request->query('per_page', 20)), ); } #[Apidoc\Title('收藏和笔记')] #[Apidoc\Url('/app/favorites')] #[Apidoc\Method('POST')] public function favorite(Request $request): JsonResponse { $data = $request->validate([ 'question_id' => ['required', 'exists:questions,id'], 'note' => ['nullable', 'string'], ]); $favorite = FavoriteQuestion::updateOrCreate([ 'user_id' => $this->currentUser($request)->id, 'question_id' => $data['question_id'], ], [ 'note' => $data['note'] ?? null, ]); return ApiResponse::success($favorite, '已保存'); } private function currentUser(Request $request): User { return JWTAuth::parseToken()->authenticate() ?? auth('api')->user() ?? $request->user(); } private function resolveAttempt(mixed $attempt): QuizAttempt { if ($attempt instanceof QuizAttempt && $attempt->exists) { return $attempt; } return QuizAttempt::query()->findOrFail((int) $attempt); } private function resolveBank(mixed $bank): QuestionBank { if ($bank instanceof QuestionBank && $bank->exists) { return $bank; } return QuestionBank::query()->findOrFail((int) $bank); } private function resolvePaper(mixed $paper): Paper { if ($paper instanceof Paper && $paper->exists) { return $paper; } return Paper::query()->findOrFail((int) $paper); } }