190 lines
7.3 KiB
PHP
190 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketCategory;
|
|
use App\Models\TicketMessage;
|
|
use App\Models\User;
|
|
use hg\apidoc\annotation as Apidoc;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
#[Apidoc\Title('工单管理')]
|
|
class TicketController extends Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
$this->middleware('auth:api');
|
|
$this->middleware('permission:tickets.use|platform.tickets.view|platform.tickets.manage,api')->only(['index', 'show']);
|
|
$this->middleware('permission:tickets.use,api')->only(['store']);
|
|
$this->middleware('permission:tickets.use|platform.tickets.manage,api')->only(['reply']);
|
|
$this->middleware('permission:platform.tickets.manage,api')->only(['update']);
|
|
}
|
|
|
|
#[Apidoc\Title('工单列表'), Apidoc\Method('GET'), Apidoc\Url('/tickets')]
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
|
'status' => ['nullable', 'string', Rule::in(Ticket::STATUSES)],
|
|
'category_id' => ['nullable', 'integer', 'exists:ticket_categories,id'],
|
|
'mine' => ['nullable', 'boolean'],
|
|
]);
|
|
|
|
/** @var User $user */
|
|
$user = auth('api')->user();
|
|
$canViewAll = $this->canViewAllTickets($user);
|
|
$query = Ticket::query()
|
|
->with(['user:id,nickname,email,phone', 'category', 'assignedUser:id,nickname,email'])
|
|
->withCount('messages')
|
|
->latest('last_replied_at')
|
|
->latest();
|
|
|
|
if (! $canViewAll || (bool) ($validated['mine'] ?? false)) {
|
|
$query->where('user_id', $user->id);
|
|
}
|
|
|
|
if (! empty($validated['status'])) {
|
|
$query->where('status', $validated['status']);
|
|
}
|
|
|
|
if (! empty($validated['category_id'])) {
|
|
$query->where('ticket_category_id', (int) $validated['category_id']);
|
|
}
|
|
|
|
$tickets = $query->paginate((int) ($validated['per_page'] ?? 20));
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $tickets]);
|
|
}
|
|
|
|
#[Apidoc\Title('提交工单'), Apidoc\Method('POST'), Apidoc\Url('/tickets')]
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'ticket_category_id' => ['required', 'integer', 'exists:ticket_categories,id'],
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'content' => ['required', 'string', 'max:10000'],
|
|
]);
|
|
|
|
$category = TicketCategory::query()
|
|
->where('is_active', true)
|
|
->findOrFail((int) $validated['ticket_category_id']);
|
|
|
|
/** @var User $user */
|
|
$user = auth('api')->user();
|
|
$ticket = Ticket::query()->create([
|
|
'user_id' => $user->id,
|
|
'ticket_category_id' => $category->id,
|
|
'title' => $validated['title'],
|
|
'content' => $validated['content'],
|
|
'status' => Ticket::StatusOpen,
|
|
'last_replied_at' => now(),
|
|
]);
|
|
$ticket->messages()->create([
|
|
'user_id' => $user->id,
|
|
'sender_type' => TicketMessage::SenderUser,
|
|
'content' => $validated['content'],
|
|
]);
|
|
|
|
$this->auditLog($request, 'ticket_create', ['metadata' => ['ticket_id' => $ticket->id]]);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $ticket->load(['user', 'category', 'messages.user'])], 201);
|
|
}
|
|
|
|
#[Apidoc\Title('工单详情'), Apidoc\Method('GET'), Apidoc\Url('/tickets/{id}')]
|
|
public function show(int $id): JsonResponse
|
|
{
|
|
$ticket = Ticket::query()
|
|
->with(['user:id,nickname,email,phone', 'category', 'assignedUser:id,nickname,email', 'messages.user:id,nickname,email'])
|
|
->findOrFail($id);
|
|
|
|
$this->authorizeTicketAccess($ticket);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $ticket]);
|
|
}
|
|
|
|
#[Apidoc\Title('更新工单状态'), Apidoc\Method('PUT'), Apidoc\Url('/tickets/{id}')]
|
|
public function update(Request $request, int $id): JsonResponse
|
|
{
|
|
$ticket = Ticket::query()->findOrFail($id);
|
|
$validated = $request->validate([
|
|
'status' => ['required', 'string', Rule::in(Ticket::STATUSES)],
|
|
'ticket_category_id' => ['nullable', 'integer', 'exists:ticket_categories,id'],
|
|
]);
|
|
|
|
/** @var User $user */
|
|
$user = auth('api')->user();
|
|
$status = (string) $validated['status'];
|
|
$ticket->fill([
|
|
'status' => $status,
|
|
'assigned_user_id' => $user->id,
|
|
'closed_at' => in_array($status, [Ticket::StatusResolved, Ticket::StatusClosed], true) ? now() : null,
|
|
]);
|
|
if (array_key_exists('ticket_category_id', $validated)) {
|
|
$ticket->ticket_category_id = $validated['ticket_category_id'];
|
|
}
|
|
$ticket->save();
|
|
|
|
$this->auditLog($request, 'ticket_update', ['metadata' => ['ticket_id' => $ticket->id, 'status' => $status]]);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $ticket->fresh(['user', 'category', 'assignedUser'])]);
|
|
}
|
|
|
|
#[Apidoc\Title('回复工单'), Apidoc\Method('POST'), Apidoc\Url('/tickets/{id}/messages')]
|
|
public function reply(Request $request, int $id): JsonResponse
|
|
{
|
|
$ticket = Ticket::query()->findOrFail($id);
|
|
$this->authorizeTicketAccess($ticket);
|
|
$validated = $request->validate([
|
|
'content' => ['required', 'string', 'max:10000'],
|
|
]);
|
|
|
|
/** @var User $user */
|
|
$user = auth('api')->user();
|
|
$isOwnTicket = (int) $ticket->user_id === (int) $user->id;
|
|
$isAdminReply = $this->canManageTickets($user);
|
|
if (! $isAdminReply && (! $isOwnTicket || ! $user->can('tickets.use'))) {
|
|
abort(403, '无权限回复该工单。');
|
|
}
|
|
|
|
$message = $ticket->messages()->create([
|
|
'user_id' => $user->id,
|
|
'sender_type' => $isAdminReply ? TicketMessage::SenderAdmin : TicketMessage::SenderUser,
|
|
'content' => $validated['content'],
|
|
]);
|
|
$ticket->update([
|
|
'assigned_user_id' => $isAdminReply ? $user->id : $ticket->assigned_user_id,
|
|
'status' => $isAdminReply && $ticket->status === Ticket::StatusOpen ? Ticket::StatusProcessing : $ticket->status,
|
|
'last_replied_at' => now(),
|
|
]);
|
|
|
|
$this->auditLog($request, 'ticket_reply', ['metadata' => ['ticket_id' => $ticket->id, 'ticket_message_id' => $message->id]]);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $message->load('user')], 201);
|
|
}
|
|
|
|
private function authorizeTicketAccess(Ticket $ticket): void
|
|
{
|
|
/** @var User $user */
|
|
$user = auth('api')->user();
|
|
if ($this->canViewAllTickets($user) || (int) $ticket->user_id === (int) $user->id) {
|
|
return;
|
|
}
|
|
|
|
abort(403, '无权限查看该工单。');
|
|
}
|
|
|
|
private function canViewAllTickets(User $user): bool
|
|
{
|
|
return $user->can('platform.tickets.view') || $user->can('platform.tickets.manage');
|
|
}
|
|
|
|
private function canManageTickets(User $user): bool
|
|
{
|
|
return $user->can('platform.tickets.manage');
|
|
}
|
|
}
|