252 lines
9.5 KiB
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);
|
|
}
|
|
}
|