Files
anxinyan/server-api/app/controller/admin/TicketsController.php
wushumin 9aac78b8da first
2026-05-11 15:28:27 +08:00

345 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
}
}