365 lines
13 KiB
PHP
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();
|
|
}
|
|
}
|