Files
anxinyan/server-api/app/controller/app/TicketsController.php
2026-06-04 12:08:16 +08:00

365 lines
13 KiB
PHP

<?php
namespace app\controller\app;
use app\support\ContentService;
use app\support\TicketAttachmentService;
use support\Request;
use support\think\Db;
class TicketsController
{
public function overview(Request $request)
{
$userId = app_user_id($request);
$ticketIds = Db::name('tickets')->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();
}
}