Pro/application/admin/controller/Chat.php
li 626b428db1 feat(admin): 添加客服手动接入会话功能并修复时长计算
- 新增 acceptSession 接口支持客服手动接入待分配会话
- 待接入列表显示"接入"按钮,点击后自动分配给当前客服
- 接入前检查客服会话数上限,防止超载
- 修复会话时长计算错误:Unix 时间戳需乘以 1000 转换为毫秒

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 03:20:17 +08:00

464 lines
15 KiB
PHP
Raw 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\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()]);
}
}
}