You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
ymww_backend/addons/shopro/library/chat/ChatService.php

856 lines
29 KiB

<?php
namespace addons\shopro\library\chat;
use addons\shopro\exception\ShoproException;
use addons\shopro\library\chat\traits\Helper;
use addons\shopro\library\chat\traits\Session;
use addons\shopro\library\chat\traits\NspData;
use addons\shopro\library\chat\traits\BindUId;
use app\admin\model\shopro\chat\User as ChatUser;
use app\admin\model\shopro\Config;
use PHPSocketIO\SocketIO;
use PHPSocketIO\Socket;
use PHPSocketIO\Nsp;
use Workerman\Timer;
/**
* ChatService 服务类
*/
class ChatService
{
/**
* session 存储助手
*/
use Session;
/**
* 绑定 UID 助手
*/
use BindUId;
/**
* nsp 对象存储全局数据
*/
use NspData;
/**
* 助手方法
*/
use Helper;
/**
* 当前 phpsocket.io 实例
*
* @var SocketIO
*/
protected $io;
/**
* 当前socket 连接
*
* @var Socket
*/
protected $socket;
/**
* 当前 命名空间
*
* @var Nsp
*/
public $nsp;
/**
* getter 实例
*
* @var Getter
*/
protected $getter;
/**
* 初始化
*
* @param Socket $socket
* @param SocketIO $io
* @param Nsp $nsp
* @param Getter $getter
*/
public function __construct(Socket $socket = null, SocketIO $io, Nsp $nsp, Getter $getter)
{
$this->socket = $socket;
$this->io = $io;
$this->nsp = $nsp;
$this->getter = $getter;
}
/**
* 通过 client_id 将当前 client_id,加入对应 room
*
* @param string $client_id
* @param string $room
* @return boolean
*/
public function joinByClientId($client_id, $room)
{
// 找到 client_id 对应的 socket 实例
$client = $this->nsp->sockets[$client_id];
$client->join($room);
return true;
}
/**
* 通过 session_id 将 session_id 对应的所有客户端加入 room
*
* @param string $room
* @param string $session_id
* @param string $type
* @return void
*/
public function joinBySessionId($session_id, $type, $room)
{
// 当前用户 uid 绑定的所有客户端 clientIds
$clientIds = $this->getClientIdByUId($session_id, $type);
// 将当前 session_id 绑定的 client_id 都加入当前客服组
foreach ($clientIds as $client_id) {
$this->joinByClientId($client_id, $room);
}
}
/**
* 通过 client_id 将当前 client_id,离开对应 room
*
* @param string $client_id
* @param string $room
* @return boolean
*/
public function leaveByClientId($client_id, $room)
{
// 找到 client_id 对应的 socket 实例
$client = $this->nsp->sockets[$client_id];
$client->leave($room);
return true;
}
/**
* 判断 auth 是否登录
*
* @return boolean
*/
public function isLogin()
{
$user = $this->session('auth_user');
return $user ? true : false;
}
/**
* auth 登录
*
* @param array $data 登录参数,token session_id 等
* @return boolean
*/
public function authLogin($data)
{
if ($this->isLogin()) {
return true;
}
$auth = $this->session('auth');
$user = null;
$token = $data['token'] ?? ''; // fastadmin token
$session_id = $data['session_id'] ?? ''; // session_id 如果没有,则后端生成
$session_id = $session_id ?: $this->session('session_id'); // 如果没有,默认取缓存中的
// 根据 token 获取当前登录的用户
if ($token) {
$user = $this->getter('db')->getAuthByToken($token, $auth);
}
if (!$user && $session_id) {
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
$auth_id = $chatUser ? $chatUser['auth_id'] : 0;
$user = $this->getter('db')->getAuthById($auth_id, $auth);
}
// 初始化连接,需要获取 session_id
if (!$session_id) {
// 如果没有 session_id
if ($user) {
// 如果存在 user
$chatUser = $this->getter('db')->getChatUserByAuth($user['id'], $auth);
$session_id = $chatUser ? $chatUser['session_id'] : '';
}
}
if (!$session_id) {
// 如果依然没有 session_id, 随机生成 session_id
$session_id = md5(time() . mt_rand(1000000, 9999999));
}
// 更新顾客用户信息
$chatUser = $this->updateChatUser($session_id, $user, $auth);
$this->session('chat_user', $chatUser->toArray()); // 转为数组
$this->session('session_id', $session_id);
$this->session('auth_user', $user ? $user->toArray() : $user);
// bind auth标示session_id,绑定 client_id
$this->bindUId($this->session('session_id'), $auth);
if ($user) {
$this->loginOk();
return true;
}
return false;
}
/**
* auth 登录成功,注册相关事件
*
* @return void
*/
public function loginOk()
{
$auth = $this->session('auth');
$session_id = $this->session('session_id');
// 将登陆的 auth 记录下来
// $this->nspSessionIdAdd($session_id, $auth);
}
/**
* 更新 chat_user
*
* @param string $session_id
* @param array $auth
* @param string $auth
* @return array|object
*/
public function updateChatUser($session_id, $authUser, $auth)
{
// 这里只根据 session_id 查询,
// 当前 session_id 如果已经绑定了 auth 和 auth_id,会被 authUser 覆盖掉,无论是否是同一个 auth 和 auth_id
// 不在 根据 authUser 或者 session_id 同时查
// 用户匿名使用客服,登录绑定用户之后,又退出登录其他用户,那么这个 session_id 和老的聊天记录直接归新用户所有
$chatUser = ChatUser::where('auth', $auth)->where(function ($query) use ($session_id, $authUser) {
$query->where('session_id', $session_id);
// ->whereOr(function ($query) use ($authUser) {
// $query->where('auth_id', '<>', 0)
// ->where('auth_id', ($authUser ? $authUser['id'] : 0));
// });
})->find();
$defaultUser = Config::getConfigs('basic.user');
$defaultAvatar = $defaultUser['avatar'] ?? null;
// $defaultNickname = $defaultUser['nickname'] ?? null;
if (!$chatUser) {
$chatUser = new ChatUser();
$chatUser->session_id = $session_id;
$chatUser->auth = $auth;
$chatUser->auth_id = $authUser ? $authUser['id'] : 0;
$chatUser->nickname = $authUser ? $authUser['nickname'] : ('游客-' . substr($session_id, 0, 5));
$chatUser->avatar = $authUser ? $authUser->getData('avatar') : $defaultAvatar;
$chatUser->customer_service_id = 0; // 断开连接的时候存入
$chatUser->last_time = time();
} else {
if ($authUser) {
// 更新用户信息
$chatUser->auth = $auth;
$chatUser->auth_id = $authUser['id'] ?? 0;
$chatUser->nickname = $authUser['nickname'] ? $authUser['nickname'] : ('游客-' . substr($session_id, 0, 5));
$chatUser->avatar = $authUser['avatar'] ? $authUser->getData('avatar') : $defaultAvatar;
}
$chatUser->last_time = time(); // 更新时间
}
$chatUser->save();
return $chatUser;
}
/**
* 检测并且分配客服
*
* @return void
*/
public function checkAndAllocatCustomerService()
{
$room_id = $this->session('room_id');
$session_id = $this->session('session_id');
$auth = $this->session('auth');
$customerService = null;
// 获取用户临时存的 客服
$customer_service_id = $this->nspGetConnectionCustomerServiceId($room_id, $session_id);
if ($customer_service_id) {
$customerService = $this->getter('socket')->getCustomerServiceById($room_id, $customer_service_id);
}
if (!$customerService) {
// 获取当前顾客有没有其他端正在连接客服,如果有,直接获取该客服
$customerService = $this->getter('socket')->getCustomerServiceBySessionId($room_id, $session_id);
}
if (!$customerService) {
$chatBasic = $this->getConfig('basic');
if ($chatBasic['auto_customer_service']) {
// 自动分配客服
$chatUser = $this->session('chat_user');
$customerService = $this->allocatCustomerService($room_id, $chatUser);
}
}
// 分配了客服
if ($customerService) {
// 将当前 用户与 客服绑定
$this->bindCustomerServiceBySessionId($room_id, $session_id, $customerService);
} else {
// 加入等待组中
$room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
$this->joinBySessionId($session_id, $auth, $room);
// 将用户 session_id 加入到等待排名中,自动排重
$this->nspWaitingAdd($room_id, $session_id);
}
return $customerService;
}
/**
* 分配客服
*
* @param string $room_id, 客服房间号
* @return array
*/
private function allocatCustomerService($room_id, $chatUser)
{
$config = $this->getConfig('basic');
$last_customer_service = $config['last_customer_service'] ?? 1;
$allocate = $config['allocate'] ?? 'busy';
// 分配的客服
$currentCustomerService = null;
// 使用上次客服
if ($last_customer_service) {
// 获取上次连接的信息
$lastServiceLog = $this->getter('db')->getLastServiceLogByChatUser($room_id, $chatUser['id']);
if ($lastServiceLog) {
// 获取上次客服信息
$currentCustomerService = $this->getter('socket')->getCustomerServiceById($room_id, $lastServiceLog['customer_service_id']); // 通过连接房间,获取socket 连接里面上次客服,不在线为 null
}
}
// 没有客服,随机分配
if (!$currentCustomerService) {
// 在线客服列表
$onlineCustomerServices = $this->getter('socket')->getCustomerServicesByRoomId($room_id);
if ($onlineCustomerServices) {
if ($allocate == 'busy') {
// 将客服列表,按照工作繁忙程度正序排序, 这里没有离线的客服
$onlineCustomerServices = array_column($onlineCustomerServices, null, 'busy_percent');
ksort($onlineCustomerServices);
// 取忙碌度最小,并且客服为 正常在线状态
foreach ($onlineCustomerServices as $customerService) {
if ($customerService['status'] == 'online') {
$currentCustomerService = $customerService;
break;
}
}
} else if ($allocate == 'turns') {
// 按照最后接入时间正序排序,这里没有离线的客服
$onlineCustomerServices = array_column($onlineCustomerServices, null, 'last_time');
ksort($onlineCustomerServices);
// 取最后接待最早,并且客服为 正常在线状态
foreach ($onlineCustomerServices as $customerService) {
if ($customerService['status'] == 'online') {
$currentCustomerService = $customerService;
break;
}
}
} else if ($allocate == 'random') {
// 随机获取一名客服
// 挑出来状态为在线的客服
$onlineStatus = [];
foreach ($onlineCustomerServices as $customerService) {
if ($customerService['status'] == 'online') {
$onlineStatus[] = $customerService;
}
}
$onlineStatus = array_column($onlineStatus, null, 'id');
$customer_service_id = 0;
if ($onlineStatus) {
$customer_service_id = array_rand($onlineStatus);
}
$currentCustomerService = $onlineStatus[$customer_service_id] ?? null;
}
if (!$currentCustomerService) {
// 如果都不是 online 状态(说明全是 busy),默认取第一条
$currentCustomerService = $onlineCustomerServices[0] ?? null;
}
}
}
return $currentCustomerService;
}
/**
* 通过 session_id 记录客服信息,并且加入对应的客服组
*
* @param string $room_id 客服房间号
* @param string $session_id 用户
* @param array $customerService 客服信息
* @return void
*/
public function bindCustomerServiceBySessionId($room_id, $session_id, $customerService)
{
// 当前用户 uid 绑定的所有客户端 clientIds
$clientIds = $this->getClientIdByUId($session_id, 'customer');
// 将当前 session_id 绑定的 client_id 都加入当前客服组
foreach ($clientIds as $client_id) {
self::bindCustomerServiceByClientId($room_id, $client_id, $customerService);
}
// 添加 serviceLog
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
$this->getter('db')->createServiceLog($room_id, $chatUser, $customerService);
}
/**
* 顾客session 记录客服信息,并且加入对应的客服组
*
* @param string $room_id 客服房间号
* @param string $client_id 用户
* @param array $customerService 客服信息
* @return null
*/
public function bindCustomerServiceByClientId($room_id, $client_id, $customerService)
{
// 更新用户的客服信息
$this->updateSessionByClientId($client_id, [
'customer_service_id' => $customerService['id'],
'customer_service' => $customerService
]);
// 更新客服的最后接入用户时间
$this->getter()->updateCustomerServiceInfo($customerService['id'], ['last_time' => time()]);
// 加入对应客服组,统计客服信息,通知用户客服上线等
$room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
$this->joinByClientId($client_id, $room);
// 从等待接入组移除
$room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
$this->leaveByClientId($client_id, $room);
}
/**
* 通过 session_id 将用户移除客服组
*
* @param string $room_id 房间号
* @param string $session_id 用户
* @param array $customerService 客服信息
* @return null
*/
public function unBindCustomerServiceBySessionId($room_id, $session_id, $customerService)
{
// 当前用户 uid 绑定的所有客户端 clientIds
$clientIds = $this->getClientIdByUId($session_id, 'customer');
// 将当前 session_id 绑定的 client_id 都加入当前客服组
foreach ($clientIds as $client_id) {
$this->unBindCustomerService($room_id, $client_id, $customerService);
}
}
/**
* 将用户的客服信息移除
*
* @param string $room_id 房间号
* @param string $client_id 用户
* @param array $customerService 客服信息
* @return null
*/
public function unBindCustomerService($room_id, $client_id, $customerService)
{
// 清空连接的客服的 session
$this->updateSessionByClientId($client_id, [
'customer_service_id' => 0,
'customer_service' => []
]);
// 移除对应客服组
$room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
$this->leaveByClientId($client_id, $room);
}
/**
* 客服转接,移除旧的客服,加入新的客服
*
* @param string $room_id 客服房间号
* @param string $session_id 用户
* @param array $customerService 客服信息
* @param array $newCustomerService 新客服信息
* @return void
*/
public function transferCustomerServiceBySessionId($room_id, $session_id, $customerService, $newCustomerService)
{
// 获取session_id 的 chatUser
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
// 结束 serviceLog,如果没有,会创建一条记录
$this->endService($room_id, $chatUser, $customerService);
// 解绑老客服
$this->unBindCustomerServiceBySessionId($room_id, $session_id, $customerService);
// 接入新客服
$this->bindCustomerServiceBySessionId($room_id, $session_id, $newCustomerService);
}
/**
* 结束服务
*
* @param string $room_id 客服房间号
* @param object $chatUser 顾客信息
* @param array $customerService 客服信息
* @return void
*/
public function endService($room_id, $chatUser, $customerService)
{
// 结束掉服务记录
$this->getter('db')->endServiceLog($room_id, $chatUser, $customerService);
// 记录客户最后服务的客服
$chatUser->customer_service_id = $customerService['id'] ?? 0;
$chatUser->save();
}
/**
* 断开用户的服务
*
* @param string $room_id
* @param string $session_id
* @param array $customerService
* @return void
*/
public function breakCustomerServiceBySessionId($room_id, $session_id, $customerService)
{
// 获取session_id 的 chatUser
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
if ($chatUser) {
// 结束 serviceLog,如果没有,会创建一条记录
$this->endService($room_id, $chatUser, $customerService);
}
// 解绑老客服
$this->unBindCustomerServiceBySessionId($room_id, $session_id, $customerService);
}
/**
* 检查当前用户的客服身份
*
* @param string $room_id 房间号
* @param string $auth 用户类型
* @param integer $auth_id 用户 id
* @return boolean|object
*/
public function getCustomerServicesByAuth($auth_id, $auth)
{
$customerServices = $this->getter('db')->getCustomerServicesByAuth($auth_id, $auth);
return $customerServices;
}
/**
* 检查当前用户在当前房间是否是客服身份
*
* @param string $room_id 房间号
* @param string $auth 用户类型
* @param integer $auth_id 用户 id
* @return boolean|object
*/
public function checkIsCustomerService($room_id, $auth_id, $auth)
{
$currentCustomerService = $this->getter('db')->getCustomerServiceByAuthAndRoom($room_id, $auth_id, $auth);
return $currentCustomerService ? : false;
}
/**
* 客服登录
*
* @param string $room_id
* @param integer $auth_id
* @param string $auth
* @return boolean
*/
public function customerServiceLogin($room_id, $auth_id, $auth) : bool
{
if ($customerService = $this->checkIsCustomerService($room_id, $auth_id, $auth)) {
// 保存 客服信息
$this->session('customer_service_id', $customerService['id']);
$this->session('customer_service', $customerService->toArray()); // toArray 减少内存占用
return true;
}
return false;
}
/**
* 客服上线
*
* @param string $room_id
* @param integer $customer_service_id
* @return void
*/
public function customerServiceOnline($room_id, $customer_service_id)
{
// 当前 socket 绑定 客服 id
$this->bindUId($customer_service_id, 'customer_service');
// 更新客服状态
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'online');
// 只把当前连接加入在线客服组,作为服务对象,多个连接同时登录一个客服,状态相互隔离
$this->socket->join($this->getRoomName('identify', ['identify' => 'customer_service']));
// 只把当前连接加入当前频道的客服组,为后续多商户做准备
$this->socket->join($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
// 保存当前客服身份
$this->session('identify', 'customer_service');
}
/**
* 客服离线
*
* @param string $room_id
* @param integer $customer_service_id
* @return void
*/
public function customerServiceOffline($room_id, $customer_service_id)
{
// 只把当前连接移除在线客服组,多个连接同时登录一个客服,状态相互隔离
$this->socket->leave($this->getRoomName('identify', ['identify' => 'customer_service']));
// 只把当前连接移除当前频道的客服组,为后续多商户做准备
$this->socket->leave($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
// 当前 socket 解绑 客服 id
$this->unBindUId($customer_service_id, 'customer_service');
// 更新客服状态为离线
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'offline');
}
/**
* 客服忙碌
*
* @param string $room_id
* @param integer $customer_service_id
* @return void
*/
public function customerServiceBusy($room_id, $customer_service_id)
{
// 当前 socket 绑定 客服 id
$this->bindUId($customer_service_id, 'customer_service');
// (尝试重新加入,避免用户是从离线切换过来的)只把当前连接加入在线客服组,作为服务对象,多个连接同时登录一个客服,状态相互隔离
$this->socket->join($this->getRoomName('identify', ['identify' => 'customer_service']));
// (尝试重新加入,避免用户是从离线切换过来的)只把当前连接加入当前频道的客服组,为后续多商户做准备
$this->socket->join($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
// 更新客服状态为忙碌
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'busy');
}
/**
* 客服退出
*
* @param [type] $room_id
* @param [type] $customer_service_id
* @return void
*/
public function customerServiceLogout($room_id, $customer_service_id)
{
// 退出房间
$this->session('room_id', null);
// 移除当前客服身份
$this->session('identify', null);
// 移除客服信息
$this->session('customer_service_id', null);
$this->session('customer_service', null); // toArray 减少内存占用
}
/**
* 顾客上线
*
* @return void
*/
public function customerOnline()
{
// 用户信息
$session_id = $this->session('session_id');
$auth = $this->session('auth');
// 当前 socket 绑定 顾客 id
$this->bindUId($session_id, 'customer');
// 加入在线顾客组,作为被服务对象
$this->socket->join($this->getRoomName('identify', ['identify' => 'customer']));
// 保存当前顾客身份
$this->session('identify', 'customer');
}
/**
* 顾客下线
*
* @return void
*/
public function customerOffline()
{
// 用户信息
$session_id = $this->session('session_id');
$customer_service_id = $this->session('customer_service_id');
$room_id = $this->session('room_id');
$customerService = $this->session('customer_service');
$chatUser = $this->session('chat_user');
// 退出房间
$this->session('room_id', null);
// 当前 socket 绑定 顾客 id
$this->unbindUId($session_id, 'customer');
// 离开在线顾客组,作为被服务对象
$this->socket->leave($this->getRoomName('identify', ['identify' => 'customer']));
// 移除当前顾客身份
$this->session('identify', null);
// 如果有客服正在服务,移除
if ($customer_service_id) {
// 离开所在客服的服务对象组
$this->socket->leave($this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customer_service_id]));
// 移除客服信息
$this->session('customer_service_id', null);
$this->session('customer_service', null);
// 获取session_id 的 chatUser
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
if ($this->getter('socket')->isOnLineCustomerById($session_id)) {
// 结束 serviceLog,如果没有,会创建一条记录
$this->endService($room_id, $chatUser, $customerService);
}
} else {
// 离开等待组
$this->socket->leave($this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]));
}
}
/**
* 断开连接解绑
*
* @return void
*/
public function disconnectUnbindAll()
{
// 因为 session 是存在 socket 上的,服务端重启断开连接,或者刷新浏览器 socket 会重新连接,所以存的 session 会全部清空
// 服务端重启 会导致 bind 到 io 实例的 bind 的数据丢失,但是如果是前端用户刷新浏览器,discount 事件中就必须要解绑 bind 数据
$room_id = $this->session('room_id');
$session_id = $this->session('session_id');
$auth = $this->session('auth');
$identify = $this->session('identify') ? : '';
$customerService = $this->session('customer_service');
// 解绑顾客身份
if ($identify == 'customer') {
$this->unbindUId($session_id, 'customer');
if ($customerService) {
// 连接着客服,将客服信息暂存 nsp 中,防止刷新重新连接
$this->nspConnectionAdd($room_id, $session_id, $customerService['id']);
// 如果有客服,定时判断,如果客服掉线了,关闭
Timer::add(10, function () use ($room_id, $session_id, $customerService) {
// 十秒之后顾客不在线,说明是真的下线了
if (!$this->getter('socket')->isOnLineCustomerById($session_id)) {
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
$this->endService($room_id, $chatUser, $customerService);
}
}, null, false);
}
}
// 解绑客服身份
if ($identify == 'customer_service') {
$customer_service_id = $this->session('customer_service_id');
$this->unbindUId($customer_service_id, 'customer_service');
}
// 将当前的 用户与 client 解绑
$this->unbindUId($session_id, $auth);
}
}