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'); } }