where('user_id', $userId)->column('id'); $ticketTypes = (new ContentService())->getTicketTypes(); return api_success([ 'cards' => [ [ 'title' => '全部工单', 'value' => (int)Db::name('tickets')->where('user_id', $userId)->count(), 'desc' => '您当前已提交的全部客服工单', ], [ 'title' => '待处理', 'value' => (int)Db::name('tickets')->where('user_id', $userId)->whereIn('status', ['pending', 'processing', 'waiting_user'])->count(), 'desc' => '客服待处理或正在跟进中的工单', ], [ 'title' => '已解决', 'value' => (int)Db::name('tickets')->where('user_id', $userId)->where('status', 'resolved')->count(), 'desc' => '已处理完成的工单数量', ], [ 'title' => '工单留言', 'value' => $ticketIds ? (int)Db::name('ticket_messages')->whereIn('ticket_id', $ticketIds)->count() : 0, 'desc' => '您与客服之间的全部沟通记录', ], ], 'ticket_types' => $ticketTypes, ]); } public function meta(Request $request) { $content = new ContentService(); return api_success([ 'ticket_types' => $content->getTicketTypes(), 'ticket_statuses' => $content->getTicketStatuses(), ]); } public function index(Request $request) { $userId = app_user_id($request); $status = trim((string)$request->input('status', '')); $type = trim((string)$request->input('ticket_type', '')); $query = Db::name('tickets') ->where('user_id', $userId) ->order('id', 'desc'); if ($status !== '') { $query->where('status', $status); } if ($type !== '') { $query->where('ticket_type', $type); } $rows = $query->select()->toArray(); $list = array_map(function (array $item) { $lastMessage = Db::name('ticket_messages') ->where('ticket_id', $item['id']) ->order('id', 'desc') ->find(); $lastAttachments = $this->attachmentService()->normalize($lastMessage['attachments_json'] ?? null, $request); $latestMessage = $lastMessage['content'] ?? ($item['content'] ?? ''); if ($latestMessage === '' && $lastAttachments) { $latestMessage = sprintf('[附件 %d 张]', count($lastAttachments)); } return [ 'id' => (int)$item['id'], 'ticket_no' => $item['ticket_no'], 'ticket_type' => $item['ticket_type'], 'ticket_type_text' => $this->ticketTypeText($item['ticket_type']), 'status' => $item['status'], 'status_text' => $this->statusText($item['status']), 'priority' => $item['priority'], 'priority_text' => $this->priorityText($item['priority']), 'title' => $item['title'] ?: '未命名工单', 'order_id' => (int)($item['order_id'] ?? 0), 'latest_message' => $latestMessage, 'updated_at' => $item['updated_at'], 'created_at' => $item['created_at'], ]; }, $rows); return api_success(['list' => $list]); } public function detail(Request $request) { $id = (int)$request->input('id', 0); if ($id <= 0) { return api_error('工单 ID 不能为空', 422); } $ticket = Db::name('tickets')->where('id', $id)->where('user_id', app_user_id($request))->find(); if (!$ticket) { return api_error('工单不存在', 404); } $messages = Db::name('ticket_messages') ->where('ticket_id', $id) ->order('id', 'asc') ->select() ->toArray(); $order = null; if (!empty($ticket['order_id'])) { $order = Db::name('orders') ->field(['id', 'order_no', 'display_status']) ->where('id', $ticket['order_id']) ->where('user_id', app_user_id($request)) ->find(); } return api_success([ 'ticket_info' => [ 'id' => (int)$ticket['id'], 'ticket_no' => $ticket['ticket_no'], 'ticket_type' => $ticket['ticket_type'], 'ticket_type_text' => $this->ticketTypeText($ticket['ticket_type']), 'status' => $ticket['status'], 'status_text' => $this->statusText($ticket['status']), 'priority' => $ticket['priority'], 'priority_text' => $this->priorityText($ticket['priority']), 'title' => $ticket['title'], 'content' => $ticket['content'] ?: '', 'order_id' => (int)($ticket['order_id'] ?? 0), 'created_at' => $ticket['created_at'], 'updated_at' => $ticket['updated_at'], ], 'order_info' => $order ? [ 'order_id' => (int)$order['id'], 'order_no' => $order['order_no'], 'display_status' => $order['display_status'], ] : null, 'messages' => array_map(function (array $item) { return [ 'sender_type' => $item['sender_type'], 'sender_type_text' => match ($item['sender_type']) { 'customer_service' => '客服', 'system' => '系统', default => '您', }, 'content' => $item['content'] ?: '', 'attachments' => $this->attachmentService()->normalize($item['attachments_json'] ?? null, $request), 'created_at' => $item['created_at'], ]; }, $messages), ]); } public function create(Request $request) { $userId = app_user_id($request); $ticketType = trim((string)$request->input('ticket_type', 'order_issue')); $title = trim((string)$request->input('title', '')); $content = trim((string)$request->input('content', '')); $attachments = $this->attachmentService()->normalize($request->input('attachments', []), $request, true); $orderId = (int)$request->input('order_id', 0); $reportId = (int)$request->input('report_id', 0); if ($title === '') { return api_error('工单标题不能为空', 422); } if ($content === '' && !$attachments) { return api_error('问题描述和附件至少填写一项', 422); } $bizType = 'support'; $bizId = null; if ($orderId > 0) { $order = Db::name('orders')->where('id', $orderId)->where('user_id', $userId)->find(); if (!$order) { return api_error('关联订单不存在', 404); } $bizType = 'order'; $bizId = $orderId; } elseif ($reportId > 0) { $report = Db::name('reports') ->alias('r') ->join('orders o', 'o.id = r.order_id') ->where('r.id', $reportId) ->where('r.report_status', 'published') ->where('o.user_id', $userId) ->field('r.id') ->find(); if (!$report) { return api_error('关联报告不存在', 404); } $bizType = 'report'; $bizId = $reportId; } $now = date('Y-m-d H:i:s'); $ticketNo = 'TK' . date('YmdHis') . mt_rand(100, 999); Db::startTrans(); try { $ticketId = (int)Db::name('tickets')->insertGetId([ 'ticket_no' => $ticketNo, 'ticket_type' => $ticketType, 'biz_type' => $bizType, 'biz_id' => $bizId, 'order_id' => $orderId > 0 ? $orderId : null, 'user_id' => $userId, 'status' => 'pending', 'priority' => 'normal', 'assignee_id' => null, 'title' => $title, 'content' => $content, 'closed_at' => null, 'created_at' => $now, 'updated_at' => $now, ]); Db::name('ticket_messages')->insertAll([ [ 'ticket_id' => $ticketId, 'sender_type' => 'user', 'sender_id' => $userId, 'content' => $content, 'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null, 'created_at' => $now, ], [ 'ticket_id' => $ticketId, 'sender_type' => 'system', 'sender_id' => null, 'content' => '工单已创建,客服会尽快与您联系。', 'attachments_json' => null, 'created_at' => $now, ], ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); return api_error('工单创建失败', 500, [ 'detail' => $e->getMessage(), ]); } return api_success([ 'ticket_id' => $ticketId, 'ticket_no' => $ticketNo, ], '工单已提交'); } public function reply(Request $request) { $ticketId = (int)$request->input('ticket_id', 0); $content = trim((string)$request->input('content', '')); $attachments = $this->attachmentService()->normalize($request->input('attachments', []), $request, true); if ($ticketId <= 0) { return api_error('工单 ID 不能为空', 422); } if ($content === '' && !$attachments) { return api_error('回复内容和附件至少填写一项', 422); } $ticket = Db::name('tickets')->where('id', $ticketId)->where('user_id', app_user_id($request))->find(); if (!$ticket) { return api_error('工单不存在', 404); } $now = date('Y-m-d H:i:s'); Db::startTrans(); try { Db::name('ticket_messages')->insert([ 'ticket_id' => $ticketId, 'sender_type' => 'user', 'sender_id' => app_user_id($request), 'content' => $content, 'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null, 'created_at' => $now, ]); Db::name('tickets')->where('id', $ticketId)->update([ 'status' => 'processing', 'updated_at' => $now, ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); return api_error('发送失败', 500, [ 'detail' => $e->getMessage(), ]); } return api_success([ 'ticket_id' => $ticketId, ], '已发送'); } public function uploadFile(Request $request) { try { $asset = $this->attachmentService()->upload($request); return api_success($asset); } catch (\Throwable $e) { return api_error($e->getMessage(), 422); } } public function deleteFile(Request $request) { $fileUrl = trim((string)$request->input('file_url', '')); if ($fileUrl === '') { return api_error('文件地址不能为空', 422); } $this->attachmentService()->delete($fileUrl); return api_success([ 'file_url' => $fileUrl, ], '删除成功'); } private function statusText(string $status): string { return (new ContentService())->ticketStatusText($status); } private function priorityText(string $priority): string { return match ($priority) { 'high' => '高优先级', 'normal' => '普通', 'low' => '低优先级', default => $priority, }; } private function ticketTypeText(string $type): string { return (new ContentService())->ticketTypeText($type); } private function attachmentService(): TicketAttachmentService { return new TicketAttachmentService(); } }