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

252 lines
9.5 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\Question;
use App\Models\QuestionBank;
use App\Services\QuestionImportService;
use App\Support\ApiResponse;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
#[Apidoc\Group('后台')]
#[Apidoc\Title('题目管理')]
#[Apidoc\RouteMiddleware(['jwt.auth'])]
final class QuestionController extends Controller
{
use AuthorizesOwnedResources;
#[Apidoc\Title('题目列表')]
#[Apidoc\Url('/admin/questions')]
#[Apidoc\Method('GET')]
#[Apidoc\RouteMiddleware(['permission:questions'])]
public function index(Request $request): JsonResponse
{
$query = Question::query()->with(['bank', 'options', 'tags'])->latest();
if ($request->user()->role !== 'admin') {
$query->whereHas('bank', fn ($bankQuery) => $bankQuery->where('owner_id', $request->user()->id));
}
if ($bankId = $request->query('question_bank_id')) {
$query->where('question_bank_id', $bankId);
}
if ($type = $request->query('type')) {
$query->where('type', $type);
}
if ($keyword = $request->query('keyword')) {
$query->where('content', 'like', '%'.$keyword.'%');
}
if ($request->filled('is_active')) {
$query->where('is_active', $request->boolean('is_active'));
}
return ApiResponse::page($query->paginate((int) $request->query('per_page', 20)));
}
#[Apidoc\Title('创建题目')]
#[Apidoc\Url('/admin/questions')]
#[Apidoc\Method('POST')]
#[Apidoc\RouteMiddleware(['permission:questions.import'])]
public function store(Request $request, QuestionImportService $service): JsonResponse
{
$data = $request->validate([
'question_bank_id' => ['required', 'exists:question_banks,id'],
'content' => ['required', 'string'],
'type' => ['required', 'in:single,multiple,judge,blank'],
'explanation' => ['nullable', 'string'],
'options' => ['array'],
'answers' => ['array'],
]);
$bank = QuestionBank::findOrFail($data['question_bank_id']);
$this->authorizeBankOwner($request, $bank);
$job = $service->importRows($bank, $request->user(), [[
'content' => $data['content'],
'type' => $data['type'],
'explanation' => $data['explanation'] ?? null,
'options' => $data['options'] ?? [],
'answer' => implode('|', $data['answers'] ?? []),
]], 'manual');
return ApiResponse::success($job->load('bank'), '题目已创建');
}
#[Apidoc\Title('批量导入题目')]
#[Apidoc\Url('/admin/banks/{bank}/imports')]
#[Apidoc\Method('POST')]
#[Apidoc\RouteMiddleware(['permission:questions.import'])]
public function import(Request $request, mixed $bank, QuestionImportService $service): JsonResponse
{
$bank = $this->resolveBank($bank);
$this->authorizeBankOwner($request, $bank);
$request->validate([
'file' => ['required', 'file', 'mimes:json,xlsx,xls,csv,txt'],
]);
$job = $service->importUploadedFile($bank, $request->user(), $request->file('file'));
OperationLog::create([
'user_id' => $request->user()->id,
'action' => 'questions.imported',
'target_type' => QuestionBank::class,
'target_id' => $bank->id,
'ip' => $request->ip(),
'payload' => ['job_id' => $job->id, 'success_count' => $job->success_count, 'skipped_count' => $job->skipped_count],
]);
return ApiResponse::success($job, '导入完成');
}
#[Apidoc\Title('校验导入题目')]
#[Apidoc\Url('/admin/banks/{bank}/imports/validate')]
#[Apidoc\Method('POST')]
#[Apidoc\RouteMiddleware(['permission:questions.import'])]
public function validateImport(Request $request, mixed $bank, QuestionImportService $service): JsonResponse
{
$bank = $this->resolveBank($bank);
$this->authorizeBankOwner($request, $bank);
$request->validate([
'file' => ['required', 'file', 'mimes:json,xlsx,xls,csv,txt'],
]);
$prepared = $service->prepareUploadedFile($request->file('file'));
return ApiResponse::success([
...$service->validateRows($prepared['rows'], $bank),
'type' => $prepared['type'],
'file_path' => $prepared['path'],
], '校验完成');
}
#[Apidoc\Title('提交已校验题目')]
#[Apidoc\Url('/admin/banks/{bank}/imports/rows')]
#[Apidoc\Method('POST')]
#[Apidoc\RouteMiddleware(['permission:questions.import'])]
public function importRows(Request $request, mixed $bank, QuestionImportService $service): JsonResponse
{
$bank = $this->resolveBank($bank);
$this->authorizeBankOwner($request, $bank);
$data = $request->validate([
'rows' => ['required', 'array'],
'type' => ['nullable', 'string'],
'file_path' => ['nullable', 'string'],
]);
$validation = $service->validateRows($data['rows'], $bank);
if (! $validation['valid']) {
return ApiResponse::success($validation, '校验未通过');
}
$job = $service->importRows($bank, $request->user(), $data['rows'], $data['type'] ?? 'manual', $data['file_path'] ?? null);
return ApiResponse::success($job, '导入完成');
}
#[Apidoc\Title('校验已编辑题目')]
#[Apidoc\Url('/admin/banks/{bank}/imports/rows/validate')]
#[Apidoc\Method('POST')]
#[Apidoc\RouteMiddleware(['permission:questions.import'])]
public function validateRows(Request $request, mixed $bank, QuestionImportService $service): JsonResponse
{
$bank = $this->resolveBank($bank);
$this->authorizeBankOwner($request, $bank);
$data = $request->validate([
'rows' => ['required', 'array'],
]);
return ApiResponse::success($service->validateRows($data['rows'], $bank), '校验完成');
}
private function resolveBank(mixed $bank): QuestionBank
{
if ($bank instanceof QuestionBank && $bank->exists) {
return $bank;
}
return QuestionBank::query()->findOrFail((int) $bank);
}
#[Apidoc\Title('更新题目状态')]
#[Apidoc\Url('/admin/questions/{question}')]
#[Apidoc\Method('PUT')]
#[Apidoc\RouteMiddleware(['permission:banks.update'])]
public function update(Request $request, mixed $question): JsonResponse
{
$question = $this->resolveQuestion($question);
$this->authorizeQuestionOwner($request, $question);
$data = $request->validate([
'content' => ['sometimes', 'string'],
'type' => ['sometimes', 'in:single,multiple,judge,blank'],
'explanation' => ['nullable', 'string'],
'is_active' => ['sometimes', 'boolean'],
'options' => ['sometimes', 'array'],
'options.*.text' => ['nullable', 'string'],
'options.*.content' => ['nullable', 'string'],
'options.*.correct' => ['nullable', 'boolean'],
'options.*.is_correct' => ['nullable', 'boolean'],
'answers' => ['sometimes', 'array'],
]);
DB::transaction(function () use ($question, $data): void {
$question->update(collect($data)->only(['content', 'type', 'explanation', 'is_active'])->all());
if (array_key_exists('answers', $data)) {
$question->update(['answers' => array_values(array_filter($data['answers']))]);
}
if (array_key_exists('options', $data)) {
$question->options()->delete();
foreach ($data['options'] as $sort => $option) {
$content = trim((string) ($option['text'] ?? $option['content'] ?? ''));
if ($content === '') {
continue;
}
$question->options()->create([
'content' => $content,
'is_correct' => (bool) ($option['correct'] ?? $option['is_correct'] ?? false),
'sort' => $sort,
]);
}
}
});
return ApiResponse::success($question->fresh('options'), '题目已更新');
}
#[Apidoc\Title('删除题目')]
#[Apidoc\Url('/admin/questions/{question}')]
#[Apidoc\Method('DELETE')]
#[Apidoc\RouteMiddleware(['permission:banks.update'])]
public function destroy(Request $request, mixed $question): JsonResponse
{
$question = $this->resolveQuestion($question);
$this->authorizeQuestionOwner($request, $question);
$question->delete();
OperationLog::create([
'user_id' => $request->user()->id,
'action' => 'question.deleted',
'target_type' => Question::class,
'target_id' => $question->id,
'ip' => $request->ip(),
'payload' => ['question_bank_id' => $question->question_bank_id],
]);
return ApiResponse::success(null, '题目已删除');
}
private function resolveQuestion(mixed $question): Question
{
if ($question instanceof Question && $question->exists) {
return $question;
}
return Question::query()->findOrFail((int) $question);
}
}