feat(admin): 添加客服手动接入会话功能并修复时长计算
- 新增 acceptSession 接口支持客服手动接入待分配会话 - 待接入列表显示"接入"按钮,点击后自动分配给当前客服 - 接入前检查客服会话数上限,防止超载 - 修复会话时长计算错误:Unix 时间戳需乘以 1000 转换为毫秒 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a49f5e8de6
commit
626b428db1
463
application/admin/controller/Chat.php
Normal file
463
application/admin/controller/Chat.php
Normal file
@ -0,0 +1,463 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use think\Db;
|
||||
use think\Request;
|
||||
use think\Session;
|
||||
|
||||
/**
|
||||
* 客服管理控制器
|
||||
* Class Chat
|
||||
* @package app\admin\controller
|
||||
*/
|
||||
class Chat extends Common
|
||||
{
|
||||
/**
|
||||
* 客服工作台
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user_info = Session::get('user_info');
|
||||
$admin_id = $user_info['id'];
|
||||
|
||||
// 从数据库获取最新的login_token(避免session中没有的情况)
|
||||
$admin = Db::name('admin')->where('id', $admin_id)->field('login_token')->find();
|
||||
$login_token = $admin['login_token'] ?? '';
|
||||
|
||||
// 如果没有login_token,生成一个新的
|
||||
if (empty($login_token)) {
|
||||
$login_token = md5($admin_id . time() . uniqid());
|
||||
Db::name('admin')->where('id', $admin_id)->update(['login_token' => $login_token]);
|
||||
}
|
||||
|
||||
// 获取客服配置
|
||||
$admin_status = Db::name('chat_admin_status')->where('admin_id', $admin_id)->find();
|
||||
if (!$admin_status) {
|
||||
// 创建默认配置
|
||||
Db::name('chat_admin_status')->insert([
|
||||
'admin_id' => $admin_id,
|
||||
'max_sessions' => 10,
|
||||
'is_enabled' => 1,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
$admin_status = ['max_sessions' => 10, 'is_enabled' => 1];
|
||||
}
|
||||
|
||||
$this->assign('admin_status', $admin_status);
|
||||
$this->assign('admin_id', $admin_id);
|
||||
$this->assign('login_token', $login_token);
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表 (AJAX)
|
||||
*/
|
||||
public function sessions()
|
||||
{
|
||||
$user_info = Session::get('user_info');
|
||||
$admin_id = $user_info['id'];
|
||||
$status = Request::instance()->get('status', 1); // 默认进行中
|
||||
|
||||
$where = ['admin_id' => $admin_id];
|
||||
if ($status !== 'all') {
|
||||
$where['status'] = (int)$status;
|
||||
}
|
||||
|
||||
$sessions = Db::name('chat_session')
|
||||
->where($where)
|
||||
->order('last_msg_time desc, create_time desc')
|
||||
->select();
|
||||
|
||||
foreach ($sessions as &$session) {
|
||||
// 获取用户信息
|
||||
$user = Db::name('user')->where('id', $session['user_id'])->find();
|
||||
$session['user_info'] = [
|
||||
'username' => $user['username'] ?? '',
|
||||
'nickname' => $user['nickname'] ?? $user['username'] ?? '',
|
||||
'money' => $user['money'] ?? 0,
|
||||
];
|
||||
|
||||
// 未读消息数
|
||||
$session['unread_count'] = Db::name('chat_message')
|
||||
->where('session_id', $session['id'])
|
||||
->where('sender_type', 1) // 用户发送
|
||||
->where('status', '<', 3) // 未读
|
||||
->count();
|
||||
|
||||
// 格式化时间
|
||||
$session['create_time_fmt'] = date('Y-m-d H:i:s', $session['create_time']);
|
||||
$session['last_msg_time_fmt'] = $session['last_msg_time'] ? date('H:i', $session['last_msg_time']) : '';
|
||||
|
||||
// 来源
|
||||
$sourceMap = [1 => 'PC', 2 => 'Game', 3 => 'Portal'];
|
||||
$session['source_name'] = $sourceMap[$session['source']] ?? 'Unknown';
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'data' => $sessions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待分配会话列表 (AJAX)
|
||||
*/
|
||||
public function pending()
|
||||
{
|
||||
$sessions = Db::name('chat_session')
|
||||
->where('status', 0) // 待分配
|
||||
->order('create_time asc')
|
||||
->select();
|
||||
|
||||
foreach ($sessions as &$session) {
|
||||
$user = Db::name('user')->where('id', $session['user_id'])->find();
|
||||
$session['user_info'] = [
|
||||
'username' => $user['username'] ?? '',
|
||||
'nickname' => $user['nickname'] ?? $user['username'] ?? '',
|
||||
'money' => $user['money'] ?? 0,
|
||||
];
|
||||
$session['create_time_fmt'] = date('Y-m-d H:i:s', $session['create_time']);
|
||||
$sourceMap = [1 => 'PC', 2 => 'Game', 3 => 'Portal'];
|
||||
$session['source_name'] = $sourceMap[$session['source']] ?? 'Unknown';
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'data' => $sessions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息历史 (AJAX)
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
$session_id = Request::instance()->get('session_id');
|
||||
$last_id = Request::instance()->get('last_id', 0);
|
||||
$limit = Request::instance()->get('limit', 50);
|
||||
|
||||
if (!$session_id) {
|
||||
return json(['code' => 1, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$query = Db::name('chat_message')->where('session_id', $session_id);
|
||||
if ($last_id > 0) {
|
||||
$query->where('id', '<', $last_id);
|
||||
}
|
||||
|
||||
$messages = $query->order('id desc')->limit($limit)->select();
|
||||
$messages = array_reverse($messages); // 按时间正序
|
||||
|
||||
foreach ($messages as &$msg) {
|
||||
$msg['create_time_fmt'] = date('H:i:s', $msg['create_time']);
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'data' => $messages]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会话 (AJAX)
|
||||
*/
|
||||
public function endSession()
|
||||
{
|
||||
$session_id = Request::instance()->post('session_id');
|
||||
$user_info = Session::get('user_info');
|
||||
|
||||
if (!$session_id) {
|
||||
return json(['code' => 1, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$session = Db::name('chat_session')->where('id', $session_id)->find();
|
||||
if (!$session || $session['admin_id'] != $user_info['id']) {
|
||||
return json(['code' => 1, 'msg' => '无权操作']);
|
||||
}
|
||||
|
||||
Db::name('chat_session')->where('id', $session_id)->update([
|
||||
'status' => 2,
|
||||
'end_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
insertAdminLog('结束客服会话', '会话ID: ' . $session_id);
|
||||
|
||||
return json(['code' => 0, 'msg' => '操作成功']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动接入会话 (AJAX)
|
||||
*/
|
||||
public function acceptSession()
|
||||
{
|
||||
$session_id = Request::instance()->post('session_id');
|
||||
$user_info = Session::get('user_info');
|
||||
$admin_id = $user_info['id'];
|
||||
|
||||
if (!$session_id) {
|
||||
return json(['code' => 1, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
// 检查会话是否存在且状态为待分配
|
||||
$session = Db::name('chat_session')->where('id', $session_id)->find();
|
||||
if (!$session) {
|
||||
return json(['code' => 1, 'msg' => '会话不存在']);
|
||||
}
|
||||
if ($session['status'] != 0) {
|
||||
return json(['code' => 1, 'msg' => '该会话已被接入']);
|
||||
}
|
||||
|
||||
// 检查客服当前会话数是否已满
|
||||
$admin_status = Db::name('chat_admin_status')->where('admin_id', $admin_id)->find();
|
||||
$max_sessions = $admin_status['max_sessions'] ?? 10;
|
||||
$current_sessions = Db::name('chat_session')
|
||||
->where('admin_id', $admin_id)
|
||||
->where('status', 1)
|
||||
->count();
|
||||
|
||||
if ($current_sessions >= $max_sessions) {
|
||||
return json(['code' => 1, 'msg' => '已达最大会话数限制(' . $max_sessions . ')']);
|
||||
}
|
||||
|
||||
// 接入会话
|
||||
$result = Db::name('chat_session')->where('id', $session_id)->where('status', 0)->update([
|
||||
'admin_id' => $admin_id,
|
||||
'status' => 1,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
insertAdminLog('接入客服会话', '会话ID: ' . $session_id);
|
||||
return json(['code' => 0, 'msg' => '接入成功']);
|
||||
} else {
|
||||
return json(['code' => 1, 'msg' => '接入失败,可能已被其他客服接入']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转接会话 (AJAX)
|
||||
*/
|
||||
public function transfer()
|
||||
{
|
||||
$session_id = Request::instance()->post('session_id');
|
||||
$target_admin_id = Request::instance()->post('target_admin_id');
|
||||
$user_info = Session::get('user_info');
|
||||
|
||||
if (!$session_id || !$target_admin_id) {
|
||||
return json(['code' => 1, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$session = Db::name('chat_session')->where('id', $session_id)->find();
|
||||
if (!$session || $session['admin_id'] != $user_info['id']) {
|
||||
return json(['code' => 1, 'msg' => '无权操作']);
|
||||
}
|
||||
|
||||
Db::name('chat_session')->where('id', $session_id)->update([
|
||||
'admin_id' => $target_admin_id,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
insertAdminLog('转接客服会话', '会话ID: ' . $session_id . ', 转接给: ' . $target_admin_id);
|
||||
|
||||
return json(['code' => 0, 'msg' => '转接成功']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线客服列表 (AJAX)
|
||||
*/
|
||||
public function onlineAgents()
|
||||
{
|
||||
$user_info = Session::get('user_info');
|
||||
|
||||
// 获取启用客服功能的管理员
|
||||
$agents = Db::name('chat_admin_status')
|
||||
->alias('s')
|
||||
->join('admin a', 'a.id = s.admin_id')
|
||||
->where('s.is_enabled', 1)
|
||||
->where('a.id', '<>', $user_info['id'])
|
||||
->field('a.id, a.admin as username, a.admin as nickname, s.max_sessions')
|
||||
->select();
|
||||
|
||||
return json(['code' => 0, 'data' => $agents]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天记录查询
|
||||
*/
|
||||
public function record()
|
||||
{
|
||||
$get = Request::instance()->get();
|
||||
$this->assign('get', $get);
|
||||
|
||||
$username = Request::instance()->get('username');
|
||||
$admin_id = Request::instance()->get('admin_id');
|
||||
$startDate = Request::instance()->get('startDate');
|
||||
$endDate = Request::instance()->get('endDate');
|
||||
$export = Request::instance()->get('export');
|
||||
|
||||
$where = [];
|
||||
$startTime = 0;
|
||||
$endTime = time();
|
||||
|
||||
if ($startDate) $startTime = strtotime($startDate);
|
||||
if ($endDate) $endTime = strtotime($endDate) + 86400;
|
||||
$where['s.create_time'] = ['between', [$startTime, $endTime]];
|
||||
|
||||
if ($admin_id) {
|
||||
$where['s.admin_id'] = $admin_id;
|
||||
}
|
||||
|
||||
$query = Db::name('chat_session')
|
||||
->alias('s')
|
||||
->join('user u', 'u.id = s.user_id', 'left')
|
||||
->join('admin a', 'a.id = s.admin_id', 'left')
|
||||
->where($where);
|
||||
|
||||
if ($username) {
|
||||
$query->where('u.username', 'like', "%{$username}%");
|
||||
}
|
||||
|
||||
if ($export == 1) {
|
||||
$list = $query->field('s.*, u.username, u.nickname as user_nickname, a.admin as admin_username')
|
||||
->order('s.create_time desc')
|
||||
->select();
|
||||
|
||||
$excelData = [];
|
||||
foreach ($list as $k => $v) {
|
||||
$excelData[$k][0] = $v['id'];
|
||||
$excelData[$k][1] = $v['username'];
|
||||
$excelData[$k][2] = $v['admin_username'] ?? '未分配';
|
||||
$excelData[$k][3] = ['PC', 'Game', 'Portal'][$v['source'] - 1] ?? '';
|
||||
$excelData[$k][4] = ['待分配', '进行中', '已结束'][$v['status']] ?? '';
|
||||
$excelData[$k][5] = $v['rating'] ?? '-';
|
||||
$excelData[$k][6] = date('Y-m-d H:i:s', $v['create_time']);
|
||||
$excelData[$k][7] = $v['end_time'] ? date('Y-m-d H:i:s', $v['end_time']) : '-';
|
||||
}
|
||||
$title = ['会话ID', '用户名', '客服', '来源', '状态', '评分', '开始时间', '结束时间'];
|
||||
$this->exportExcelCore($excelData, '聊天记录-' . date('Ymd'), $title);
|
||||
exit;
|
||||
}
|
||||
|
||||
$list = $query->field('s.*, u.username, u.nickname as user_nickname, a.admin as admin_username')
|
||||
->order('s.create_time desc')
|
||||
->paginate(15, false, ['query' => $get]);
|
||||
|
||||
// 获取客服列表
|
||||
$admins = Db::name('admin')->where('status', 1)->field('id, admin as username, admin as nickname')->select();
|
||||
|
||||
$this->assign('list', $list);
|
||||
$this->assign('admins', $admins);
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看会话详情
|
||||
*/
|
||||
public function detail()
|
||||
{
|
||||
$session_id = Request::instance()->get('session_id');
|
||||
|
||||
$session = Db::name('chat_session')
|
||||
->alias('s')
|
||||
->join('user u', 'u.id = s.user_id', 'left')
|
||||
->join('admin a', 'a.id = s.admin_id', 'left')
|
||||
->where('s.id', $session_id)
|
||||
->field('s.*, u.username, u.nickname as user_nickname, u.money, a.admin as admin_username')
|
||||
->find();
|
||||
|
||||
if (!$session) {
|
||||
$this->error('会话不存在');
|
||||
}
|
||||
|
||||
$messages = Db::name('chat_message')
|
||||
->where('session_id', $session_id)
|
||||
->order('id asc')
|
||||
->select();
|
||||
|
||||
$this->assign('session', $session);
|
||||
$this->assign('messages', $messages);
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服统计
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
$user_info = Session::get('user_info');
|
||||
$admin_id = Request::instance()->get('admin_id', $user_info['id']);
|
||||
$startDate = Request::instance()->get('startDate', date('Y-m-d', strtotime('-7 days')));
|
||||
$endDate = Request::instance()->get('endDate', date('Y-m-d'));
|
||||
|
||||
$startTime = strtotime($startDate);
|
||||
$endTime = strtotime($endDate) + 86400;
|
||||
|
||||
$where = [
|
||||
'admin_id' => $admin_id,
|
||||
'create_time' => ['between', [$startTime, $endTime]],
|
||||
];
|
||||
|
||||
// 统计数据
|
||||
$stats = [
|
||||
'total_sessions' => Db::name('chat_session')->where($where)->count(),
|
||||
'ended_sessions' => Db::name('chat_session')->where($where)->where('status', 2)->count(),
|
||||
'avg_rating' => Db::name('chat_session')->where($where)->whereNotNull('rating')->avg('rating') ?: 0,
|
||||
'total_messages' => Db::name('chat_message')
|
||||
->alias('m')
|
||||
->join('chat_session s', 's.id = m.session_id')
|
||||
->where('s.admin_id', $admin_id)
|
||||
->where('m.create_time', 'between', [$startTime, $endTime])
|
||||
->count(),
|
||||
];
|
||||
|
||||
$this->assign('stats', $stats);
|
||||
$this->assign('startDate', $startDate);
|
||||
$this->assign('endDate', $endDate);
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取快捷回复列表 (AJAX)
|
||||
*/
|
||||
public function quickReplies()
|
||||
{
|
||||
$category = Request::instance()->get('category');
|
||||
|
||||
$query = Db::name('chat_quick_reply')->where('status', 1);
|
||||
if ($category) {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
|
||||
$list = $query->order('sort asc, id asc')->select();
|
||||
$categories = Db::name('chat_quick_reply')
|
||||
->where('status', 1)
|
||||
->whereNotNull('category')
|
||||
->where('category', '<>', '')
|
||||
->group('category')
|
||||
->column('category');
|
||||
|
||||
return json(['code' => 0, 'data' => $list, 'categories' => $categories]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
$file = Request::instance()->file('image');
|
||||
if (!$file) {
|
||||
return json(['code' => 1, 'msg' => '请选择文件']);
|
||||
}
|
||||
|
||||
// 验证文件
|
||||
$info = $file->validate([
|
||||
'size' => 2097152, // 2MB
|
||||
'ext' => 'jpg,png,gif,jpeg'
|
||||
])->move(ROOT_PATH . 'public/uploads/chat/' . date('Ymd'));
|
||||
|
||||
if ($info) {
|
||||
$url = '/uploads/chat/' . date('Ymd') . '/' . $info->getSaveName();
|
||||
return json(['code' => 0, 'url' => $url]);
|
||||
} else {
|
||||
return json(['code' => 1, 'msg' => $file->getError()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,6 +162,21 @@
|
||||
color: #ddd;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.accept-btn {
|
||||
padding: 4px 10px;
|
||||
background: #009688;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.accept-btn:hover {
|
||||
background: #00796b;
|
||||
}
|
||||
|
||||
/* 中间聊天区域 */
|
||||
.chat-main {
|
||||
@ -766,7 +781,7 @@ layui.use(['layer', 'upload'], function(){
|
||||
$.get('/chat/sessions', {status: 1}, function(res) {
|
||||
if (res.code === 0) {
|
||||
$('#active-count').text(res.data.length);
|
||||
renderSessionList(res.data, '#session-list');
|
||||
renderSessionList(res.data, '#session-list', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -776,13 +791,13 @@ layui.use(['layer', 'upload'], function(){
|
||||
$.get('/chat/pending', function(res) {
|
||||
if (res.code === 0) {
|
||||
$('#pending-count').text(res.data.length);
|
||||
renderSessionList(res.data, '#pending-list');
|
||||
renderSessionList(res.data, '#pending-list', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染会话列表
|
||||
function renderSessionList(sessions, container) {
|
||||
function renderSessionList(sessions, container, isPending) {
|
||||
var html = '';
|
||||
if (sessions.length === 0) {
|
||||
html = '<div class="sessions-empty">';
|
||||
@ -803,20 +818,57 @@ layui.use(['layer', 'upload'], function(){
|
||||
html += ' <div class="session-info">';
|
||||
html += ' <div class="name-row">';
|
||||
html += ' <span class="username">' + username + '</span>';
|
||||
html += ' <span class="time">' + (session.last_msg_time_fmt || '') + '</span>';
|
||||
html += ' <span class="time">' + (session.last_msg_time_fmt || session.create_time_fmt || '') + '</span>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="last-msg">' + lastMsg + '</div>';
|
||||
html += ' </div>';
|
||||
html += unreadBadge;
|
||||
if (isPending) {
|
||||
html += ' <button class="accept-btn" data-id="' + session.id + '">接入</button>';
|
||||
} else {
|
||||
html += unreadBadge;
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
$(container).html(html);
|
||||
|
||||
// 绑定点击事件
|
||||
$(container + ' .session-item').click(function() {
|
||||
$(container + ' .session-item').click(function(e) {
|
||||
// 如果点击的是接入按钮,不触发打开会话
|
||||
if ($(e.target).hasClass('accept-btn')) return;
|
||||
var sessionId = $(this).data('id');
|
||||
openSession(sessionId);
|
||||
if (!isPending) {
|
||||
openSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// 绑定接入按钮事件
|
||||
if (isPending) {
|
||||
$(container + ' .accept-btn').click(function(e) {
|
||||
e.stopPropagation();
|
||||
var sessionId = $(this).data('id');
|
||||
acceptSession(sessionId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 接入会话
|
||||
function acceptSession(sessionId) {
|
||||
$.post('/chat/acceptSession', {session_id: sessionId}, function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('接入成功', {icon: 1, time: 1500});
|
||||
loadSessions();
|
||||
loadPendingSessions();
|
||||
// 自动打开刚接入的会话
|
||||
setTimeout(function() {
|
||||
openSession(sessionId);
|
||||
// 切换到进行中标签
|
||||
$('.sessions-tabs .tab-item[data-status="1"]').click();
|
||||
}, 500);
|
||||
} else {
|
||||
layer.msg(res.msg || '接入失败', {icon: 2});
|
||||
loadPendingSessions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -877,7 +929,8 @@ layui.use(['layer', 'upload'], function(){
|
||||
// 会话时长计时器
|
||||
function startDurationTimer(startTime) {
|
||||
if (durationTimer) clearInterval(durationTimer);
|
||||
sessionStartTime = new Date(startTime).getTime();
|
||||
// startTime 是 Unix 时间戳(秒),需要转换为毫秒
|
||||
sessionStartTime = startTime * 1000;
|
||||
|
||||
function updateDuration() {
|
||||
var now = Date.now();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user