201 lines
7.0 KiB
PHP
201 lines
7.0 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('/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('/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);
|
|
}
|
|
}
|