QuickQuiz/app/Http/Controllers/Api/App/QuizController.php

190 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\App;
use App\Http\Controllers\Controller;
use App\Models\FavoriteQuestion;
use App\Models\Paper;
use App\Models\QuestionBank;
use App\Models\QuizAttempt;
use App\Models\User;
use App\Models\WrongQuestion;
use App\Services\LearningAccessService;
use App\Services\QuizService;
use App\Support\ApiResponse;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Facades\JWTAuth;
#[Apidoc\Group('用户端')]
#[Apidoc\Title('做题')]
#[Apidoc\RouteMiddleware(['jwt.auth'])]
final class QuizController extends Controller
{
#[Apidoc\Title('开始背题/刷题/抽题')]
#[Apidoc\Url('/api/app/banks/{bank}/attempts')]
#[Apidoc\Method('POST')]
public function startBank(Request $request, mixed $bank, QuizService $service, LearningAccessService $access): JsonResponse
{
$bank = $this->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('/api/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('/api/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('/api/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('/api/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('/api/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('/api/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('/api/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);
}
}