345 lines
12 KiB
PHP
345 lines
12 KiB
PHP
<?php
|
||
|
||
namespace app\controller\admin;
|
||
|
||
use app\support\ContentService;
|
||
use app\support\MessageDispatcher;
|
||
use app\support\TicketAttachmentService;
|
||
use support\Request;
|
||
use support\think\Db;
|
||
|
||
class TicketsController
|
||
{
|
||
public function overview(Request $request)
|
||
{
|
||
return api_success([
|
||
'cards' => [
|
||
[
|
||
'title' => '工单总量',
|
||
'value' => (int)Db::name('tickets')->count(),
|
||
'desc' => '当前数据库内工单总数',
|
||
],
|
||
[
|
||
'title' => '待处理工单',
|
||
'value' => (int)Db::name('tickets')->whereIn('status', ['pending', 'processing'])->count(),
|
||
'desc' => '待处理与处理中工单数量',
|
||
],
|
||
[
|
||
'title' => '已解决工单',
|
||
'value' => (int)Db::name('tickets')->where('status', 'resolved')->count(),
|
||
'desc' => '当前已解决的工单数量',
|
||
],
|
||
[
|
||
'title' => '工单留言',
|
||
'value' => (int)Db::name('ticket_messages')->count(),
|
||
'desc' => '当前工单消息记录总数',
|
||
],
|
||
],
|
||
]);
|
||
}
|
||
|
||
public function index(Request $request)
|
||
{
|
||
$keyword = trim((string)$request->input('keyword', ''));
|
||
$status = trim((string)$request->input('status', ''));
|
||
$type = trim((string)$request->input('ticket_type', ''));
|
||
|
||
$query = Db::name('tickets')
|
||
->field([
|
||
'id',
|
||
'ticket_no',
|
||
'ticket_type',
|
||
'biz_type',
|
||
'biz_id',
|
||
'order_id',
|
||
'user_id',
|
||
'status',
|
||
'priority',
|
||
'assignee_id',
|
||
'title',
|
||
'created_at',
|
||
'updated_at',
|
||
])
|
||
->order('id', 'desc');
|
||
|
||
if ($keyword !== '') {
|
||
$query->where(function ($builder) use ($keyword) {
|
||
$builder->whereLike('ticket_no', "%{$keyword}%")
|
||
->whereOrLike('title', "%{$keyword}%");
|
||
});
|
||
}
|
||
if ($status !== '') {
|
||
$query->where('status', $status);
|
||
}
|
||
if ($type !== '') {
|
||
$query->where('ticket_type', $type);
|
||
}
|
||
|
||
$rows = $query->select()->toArray();
|
||
|
||
$list = array_map(function (array $item) {
|
||
return [
|
||
'id' => (int)$item['id'],
|
||
'ticket_no' => $item['ticket_no'],
|
||
'ticket_type' => $item['ticket_type'],
|
||
'ticket_type_text' => $this->ticketTypeText($item['ticket_type']),
|
||
'biz_type' => $item['biz_type'],
|
||
'biz_id' => (int)($item['biz_id'] ?? 0),
|
||
'order_id' => (int)($item['order_id'] ?? 0),
|
||
'user_id' => (int)($item['user_id'] ?? 0),
|
||
'status' => $item['status'],
|
||
'status_text' => $this->statusText($item['status']),
|
||
'priority' => $item['priority'],
|
||
'priority_text' => $this->priorityText($item['priority']),
|
||
'title' => $item['title'] ?: '未命名工单',
|
||
'created_at' => $item['created_at'],
|
||
'updated_at' => $item['updated_at'],
|
||
];
|
||
}, $rows);
|
||
|
||
return api_success(['list' => $list]);
|
||
}
|
||
|
||
public function detail(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
if (!$id) {
|
||
return api_error('工单 ID 不能为空', 422);
|
||
}
|
||
|
||
$ticket = Db::name('tickets')->where('id', $id)->find();
|
||
if (!$ticket) {
|
||
return api_error('工单不存在', 404);
|
||
}
|
||
|
||
$messages = Db::name('ticket_messages')
|
||
->where('ticket_id', $id)
|
||
->order('id', 'asc')
|
||
->select()
|
||
->toArray();
|
||
|
||
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']),
|
||
'biz_type' => $ticket['biz_type'],
|
||
'biz_id' => (int)($ticket['biz_id'] ?? 0),
|
||
'order_id' => (int)($ticket['order_id'] ?? 0),
|
||
'user_id' => (int)($ticket['user_id'] ?? 0),
|
||
'status' => $ticket['status'],
|
||
'status_text' => $this->statusText($ticket['status']),
|
||
'priority' => $ticket['priority'],
|
||
'priority_text' => $this->priorityText($ticket['priority']),
|
||
'title' => $ticket['title'],
|
||
'content' => $ticket['content'],
|
||
'created_at' => $ticket['created_at'],
|
||
'updated_at' => $ticket['updated_at'],
|
||
],
|
||
'messages' => array_map(function (array $item) {
|
||
return [
|
||
'sender_type' => $item['sender_type'],
|
||
'sender_type_text' => $item['sender_type'] === 'customer_service' ? '客服' : ($item['sender_type'] === 'system' ? '系统' : '用户'),
|
||
'content' => $item['content'] ?: '',
|
||
'attachments' => $this->attachmentService()->normalize($item['attachments_json'] ?? null, $request),
|
||
'created_at' => $item['created_at'],
|
||
];
|
||
}, $messages),
|
||
]);
|
||
}
|
||
|
||
public function save(Request $request)
|
||
{
|
||
$id = (int)$request->input('id', 0);
|
||
if (!$id) {
|
||
return api_error('工单 ID 不能为空', 422);
|
||
}
|
||
|
||
$ticket = Db::name('tickets')->where('id', $id)->find();
|
||
if (!$ticket) {
|
||
return api_error('工单不存在', 404);
|
||
}
|
||
|
||
$status = trim((string)$request->input('status', $ticket['status']));
|
||
$priority = trim((string)$request->input('priority', $ticket['priority']));
|
||
$now = date('Y-m-d H:i:s');
|
||
|
||
Db::startTrans();
|
||
try {
|
||
Db::name('tickets')->where('id', $id)->update([
|
||
'status' => $status,
|
||
'priority' => $priority,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
if ($status !== $ticket['status']) {
|
||
$this->notifyStatusChanged($ticket, $status, $now);
|
||
}
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('工单更新失败', 500, [
|
||
'detail' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
return api_success(['id' => $id], '工单已更新');
|
||
}
|
||
|
||
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) {
|
||
return api_error('工单 ID 不能为空', 422);
|
||
}
|
||
if ($content === '' && !$attachments) {
|
||
return api_error('回复内容和附件至少填写一项', 422);
|
||
}
|
||
|
||
$ticket = Db::name('tickets')->where('id', $ticketId)->find();
|
||
if (!$ticket) {
|
||
return api_error('工单不存在', 404);
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
Db::startTrans();
|
||
try {
|
||
$messageId = (int)Db::name('ticket_messages')->insertGetId([
|
||
'ticket_id' => $ticketId,
|
||
'sender_type' => 'customer_service',
|
||
'sender_id' => 1,
|
||
'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,
|
||
]);
|
||
|
||
(new MessageDispatcher())->sendInboxEvent('ticket_reply', [
|
||
'user_id' => (int)($ticket['user_id'] ?? 0),
|
||
'biz_type' => 'ticket_message',
|
||
'biz_id' => $messageId,
|
||
'ticket_id' => $ticketId,
|
||
'ticket_no' => $ticket['ticket_no'],
|
||
'ticket_title' => $ticket['title'] ?: '客服工单',
|
||
'reply_content' => $content,
|
||
'fallback_title' => '工单有新回复',
|
||
'fallback_content' => sprintf('客服已回复您的工单「%s」,点击查看详情。', $ticket['title'] ?: '客服工单'),
|
||
]);
|
||
|
||
Db::commit();
|
||
return api_success(['ticket_id' => $ticketId], '回复成功');
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return api_error('回复失败', 500, [
|
||
'detail' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
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 notifyStatusChanged(array $ticket, string $status, string $now): void
|
||
{
|
||
$eventConfig = match ($status) {
|
||
'waiting_user' => [
|
||
'event_code' => 'ticket_waiting_user',
|
||
'title' => '工单等待您补充反馈',
|
||
'content' => sprintf('客服正在跟进工单「%s」,当前需要您补充反馈信息。', $ticket['title'] ?: '客服工单'),
|
||
'system_message' => '工单状态已更新为待用户反馈,请等待用户补充信息。',
|
||
],
|
||
'resolved' => [
|
||
'event_code' => 'ticket_resolved',
|
||
'title' => '工单已解决',
|
||
'content' => sprintf('您的工单「%s」已处理完成,如仍有疑问可继续留言。', $ticket['title'] ?: '客服工单'),
|
||
'system_message' => '工单状态已更新为已解决。',
|
||
],
|
||
'closed' => [
|
||
'event_code' => 'ticket_closed',
|
||
'title' => '工单已关闭',
|
||
'content' => sprintf('您的工单「%s」已关闭,如需继续处理可重新发起工单。', $ticket['title'] ?: '客服工单'),
|
||
'system_message' => '工单状态已更新为已关闭。',
|
||
],
|
||
default => null,
|
||
};
|
||
|
||
if (!$eventConfig) {
|
||
return;
|
||
}
|
||
|
||
$messageId = (int)Db::name('ticket_messages')->insertGetId([
|
||
'ticket_id' => (int)$ticket['id'],
|
||
'sender_type' => 'system',
|
||
'sender_id' => null,
|
||
'content' => $eventConfig['system_message'],
|
||
'attachments_json' => null,
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
(new MessageDispatcher())->sendInboxEvent($eventConfig['event_code'], [
|
||
'user_id' => (int)($ticket['user_id'] ?? 0),
|
||
'biz_type' => 'ticket_message',
|
||
'biz_id' => $messageId,
|
||
'ticket_id' => (int)$ticket['id'],
|
||
'ticket_no' => $ticket['ticket_no'] ?? '',
|
||
'ticket_title' => $ticket['title'] ?: '客服工单',
|
||
'fallback_title' => $eventConfig['title'],
|
||
'fallback_content' => $eventConfig['content'],
|
||
]);
|
||
}
|
||
|
||
private function attachmentService(): TicketAttachmentService
|
||
{
|
||
return new TicketAttachmentService();
|
||
}
|
||
}
|