first
This commit is contained in:
344
server-api/app/controller/admin/TicketsController.php
Normal file
344
server-api/app/controller/admin/TicketsController.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user