QuickQuiz/app/Http/Controllers/Api/Admin/PaperController.php

204 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Api\Admin\Concerns\AuthorizesOwnedResources;
use App\Http\Controllers\Controller;
use App\Models\OperationLog;
use App\Models\Paper;
use App\Models\Question;
use App\Models\QuestionBank;
use App\Support\ApiResponse;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
#[Apidoc\Group('后台')]
#[Apidoc\Title('试卷管理')]
#[Apidoc\RouteMiddleware(['jwt.auth'])]
final class PaperController extends Controller
{
use AuthorizesOwnedResources;
#[Apidoc\Title('试卷列表')]
#[Apidoc\Url('/api/admin/papers')]
#[Apidoc\Method('GET')]
#[Apidoc\RouteMiddleware(['permission:papers'])]
public function index(Request $request): JsonResponse
{
$query = Paper::query()->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<string, mixed>
*/
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<int, int> $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<int, array{id: int, score?: float|int|null}> $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);
}
}