204 lines
6.9 KiB
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('/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('/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('/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('/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('/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);
|
|
}
|
|
}
|