ChatService.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. <?php
  2. namespace addons\shopro\library\chat;
  3. use addons\shopro\exception\ShoproException;
  4. use addons\shopro\library\chat\traits\Helper;
  5. use addons\shopro\library\chat\traits\Session;
  6. use addons\shopro\library\chat\traits\NspData;
  7. use addons\shopro\library\chat\traits\BindUId;
  8. use app\admin\model\shopro\chat\User as ChatUser;
  9. use app\admin\model\shopro\Config;
  10. use PHPSocketIO\SocketIO;
  11. use PHPSocketIO\Socket;
  12. use PHPSocketIO\Nsp;
  13. use Workerman\Timer;
  14. /**
  15. * ChatService 服务类
  16. */
  17. class ChatService
  18. {
  19. /**
  20. * session 存储助手
  21. */
  22. use Session;
  23. /**
  24. * 绑定 UID 助手
  25. */
  26. use BindUId;
  27. /**
  28. * nsp 对象存储全局数据
  29. */
  30. use NspData;
  31. /**
  32. * 助手方法
  33. */
  34. use Helper;
  35. /**
  36. * 当前 phpsocket.io 实例
  37. *
  38. * @var SocketIO
  39. */
  40. protected $io;
  41. /**
  42. * 当前socket 连接
  43. *
  44. * @var Socket
  45. */
  46. protected $socket;
  47. /**
  48. * 当前 命名空间
  49. *
  50. * @var Nsp
  51. */
  52. public $nsp;
  53. /**
  54. * getter 实例
  55. *
  56. * @var Getter
  57. */
  58. protected $getter;
  59. /**
  60. * 初始化
  61. *
  62. * @param Socket $socket
  63. * @param SocketIO $io
  64. * @param Nsp $nsp
  65. * @param Getter $getter
  66. */
  67. public function __construct(Socket $socket = null, SocketIO $io, Nsp $nsp, Getter $getter)
  68. {
  69. $this->socket = $socket;
  70. $this->io = $io;
  71. $this->nsp = $nsp;
  72. $this->getter = $getter;
  73. }
  74. /**
  75. * 通过 client_id 将当前 client_id,加入对应 room
  76. *
  77. * @param string $client_id
  78. * @param string $room
  79. * @return boolean
  80. */
  81. public function joinByClientId($client_id, $room)
  82. {
  83. // 找到 client_id 对应的 socket 实例
  84. $client = $this->nsp->sockets[$client_id];
  85. $client->join($room);
  86. return true;
  87. }
  88. /**
  89. * 通过 session_id 将 session_id 对应的所有客户端加入 room
  90. *
  91. * @param string $room
  92. * @param string $session_id
  93. * @param string $type
  94. * @return void
  95. */
  96. public function joinBySessionId($session_id, $type, $room)
  97. {
  98. // 当前用户 uid 绑定的所有客户端 clientIds
  99. $clientIds = $this->getClientIdByUId($session_id, $type);
  100. // 将当前 session_id 绑定的 client_id 都加入当前客服组
  101. foreach ($clientIds as $client_id) {
  102. $this->joinByClientId($client_id, $room);
  103. }
  104. }
  105. /**
  106. * 通过 client_id 将当前 client_id,离开对应 room
  107. *
  108. * @param string $client_id
  109. * @param string $room
  110. * @return boolean
  111. */
  112. public function leaveByClientId($client_id, $room)
  113. {
  114. // 找到 client_id 对应的 socket 实例
  115. $client = $this->nsp->sockets[$client_id];
  116. $client->leave($room);
  117. return true;
  118. }
  119. /**
  120. * 判断 auth 是否登录
  121. *
  122. * @return boolean
  123. */
  124. public function isLogin()
  125. {
  126. $user = $this->session('auth_user');
  127. return $user ? true : false;
  128. }
  129. /**
  130. * auth 登录
  131. *
  132. * @param array $data 登录参数,token session_id 等
  133. * @return boolean
  134. */
  135. public function authLogin($data)
  136. {
  137. if ($this->isLogin()) {
  138. return true;
  139. }
  140. $auth = $this->session('auth');
  141. $user = null;
  142. $token = $data['token'] ?? ''; // fastadmin token
  143. $session_id = $data['session_id'] ?? ''; // session_id 如果没有,则后端生成
  144. $session_id = $session_id ?: $this->session('session_id'); // 如果没有,默认取缓存中的
  145. // 根据 token 获取当前登录的用户
  146. if ($token) {
  147. $user = $this->getter('db')->getAuthByToken($token, $auth);
  148. }
  149. if (!$user && $session_id) {
  150. $chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
  151. $auth_id = $chatUser ? $chatUser['auth_id'] : 0;
  152. $user = $this->getter('db')->getAuthById($auth_id, $auth);
  153. }
  154. // 初始化连接,需要获取 session_id
  155. if (!$session_id) {
  156. // 如果没有 session_id
  157. if ($user) {
  158. // 如果存在 user
  159. $chatUser = $this->getter('db')->getChatUserByAuth($user['id'], $auth);
  160. $session_id = $chatUser ? $chatUser['session_id'] : '';
  161. }
  162. }
  163. if (!$session_id) {
  164. // 如果依然没有 session_id, 随机生成 session_id
  165. $session_id = md5(time() . mt_rand(1000000, 9999999));
  166. }
  167. // 更新顾客用户信息
  168. $chatUser = $this->updateChatUser($session_id, $user, $auth);
  169. $this->session('chat_user', $chatUser->toArray()); // 转为数组
  170. $this->session('session_id', $session_id);
  171. $this->session('auth_user', $user ? $user->toArray() : $user);
  172. // bind auth标示session_id,绑定 client_id
  173. $this->bindUId($this->session('session_id'), $auth);
  174. if ($user) {
  175. $this->loginOk();
  176. return true;
  177. }
  178. return false;
  179. }
  180. /**
  181. * auth 登录成功,注册相关事件
  182. *
  183. * @return void
  184. */
  185. public function loginOk()
  186. {
  187. $auth = $this->session('auth');
  188. $session_id = $this->session('session_id');
  189. // 将登陆的 auth 记录下来
  190. // $this->nspSessionIdAdd($session_id, $auth);
  191. }
  192. /**
  193. * 更新 chat_user
  194. *
  195. * @param string $session_id
  196. * @param array $auth
  197. * @param string $auth
  198. * @return array|object
  199. */
  200. public function updateChatUser($session_id, $authUser, $auth)
  201. {
  202. // 这里只根据 session_id 查询,
  203. // 当前 session_id 如果已经绑定了 auth 和 auth_id,会被 authUser 覆盖掉,无论是否是同一个 auth 和 auth_id
  204. // 不在 根据 authUser 或者 session_id 同时查
  205. // 用户匿名使用客服,登录绑定用户之后,又退出登录其他用户,那么这个 session_id 和老的聊天记录直接归新用户所有
  206. $chatUser = ChatUser::where('auth', $auth)->where(function ($query) use ($session_id, $authUser) {
  207. $query->where('session_id', $session_id);
  208. // ->whereOr(function ($query) use ($authUser) {
  209. // $query->where('auth_id', '<>', 0)
  210. // ->where('auth_id', ($authUser ? $authUser['id'] : 0));
  211. // });
  212. })->find();
  213. $defaultUser = Config::getConfigs('basic.user');
  214. $defaultAvatar = $defaultUser['avatar'] ?? null;
  215. // $defaultNickname = $defaultUser['nickname'] ?? null;
  216. if (!$chatUser) {
  217. $chatUser = new ChatUser();
  218. $chatUser->session_id = $session_id;
  219. $chatUser->auth = $auth;
  220. $chatUser->auth_id = $authUser ? $authUser['id'] : 0;
  221. $chatUser->nickname = $authUser ? $authUser['nickname'] : ('游客-' . substr($session_id, 0, 5));
  222. $chatUser->avatar = $authUser ? $authUser->getData('avatar') : $defaultAvatar;
  223. $chatUser->customer_service_id = 0; // 断开连接的时候存入
  224. $chatUser->last_time = time();
  225. } else {
  226. if ($authUser) {
  227. // 更新用户信息
  228. $chatUser->auth = $auth;
  229. $chatUser->auth_id = $authUser['id'] ?? 0;
  230. $chatUser->nickname = $authUser['nickname'] ? $authUser['nickname'] : ('游客-' . substr($session_id, 0, 5));
  231. $chatUser->avatar = $authUser['avatar'] ? $authUser->getData('avatar') : $defaultAvatar;
  232. }
  233. $chatUser->last_time = time(); // 更新时间
  234. }
  235. $chatUser->save();
  236. return $chatUser;
  237. }
  238. /**
  239. * 检测并且分配客服
  240. *
  241. * @return void
  242. */
  243. public function checkAndAllocatCustomerService()
  244. {
  245. $room_id = $this->session('room_id');
  246. $session_id = $this->session('session_id');
  247. $auth = $this->session('auth');
  248. $customerService = null;
  249. // 获取用户临时存的 客服
  250. $customer_service_id = $this->nspGetConnectionCustomerServiceId($room_id, $session_id);
  251. if ($customer_service_id) {
  252. $customerService = $this->getter('socket')->getCustomerServiceById($room_id, $customer_service_id);
  253. }
  254. if (!$customerService) {
  255. // 获取当前顾客有没有其他端正在连接客服,如果有,直接获取该客服
  256. $customerService = $this->getter('socket')->getCustomerServiceBySessionId($room_id, $session_id);
  257. }
  258. if (!$customerService) {
  259. $chatBasic = $this->getConfig('basic');
  260. if ($chatBasic['auto_customer_service']) {
  261. // 自动分配客服
  262. $chatUser = $this->session('chat_user');
  263. $customerService = $this->allocatCustomerService($room_id, $chatUser);
  264. }
  265. }
  266. // 分配了客服
  267. if ($customerService) {
  268. // 将当前 用户与 客服绑定
  269. $this->bindCustomerServiceBySessionId($room_id, $session_id, $customerService);
  270. } else {
  271. // 加入等待组中
  272. $room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
  273. $this->joinBySessionId($session_id, $auth, $room);
  274. // 将用户 session_id 加入到等待排名中,自动排重
  275. $this->nspWaitingAdd($room_id, $session_id);
  276. }
  277. return $customerService;
  278. }
  279. /**
  280. * 分配客服
  281. *
  282. * @param string $room_id, 客服房间号
  283. * @return array
  284. */
  285. private function allocatCustomerService($room_id, $chatUser)
  286. {
  287. $config = $this->getConfig('basic');
  288. $last_customer_service = $config['last_customer_service'] ?? 1;
  289. $allocate = $config['allocate'] ?? 'busy';
  290. // 分配的客服
  291. $currentCustomerService = null;
  292. // 使用上次客服
  293. if ($last_customer_service) {
  294. // 获取上次连接的信息
  295. $lastServiceLog = $this->getter('db')->getLastServiceLogByChatUser($room_id, $chatUser['id']);
  296. if ($lastServiceLog) {
  297. // 获取上次客服信息
  298. $currentCustomerService = $this->getter('socket')->getCustomerServiceById($room_id, $lastServiceLog['customer_service_id']); // 通过连接房间,获取socket 连接里面上次客服,不在线为 null
  299. }
  300. }
  301. // 没有客服,随机分配
  302. if (!$currentCustomerService) {
  303. // 在线客服列表
  304. $onlineCustomerServices = $this->getter('socket')->getCustomerServicesByRoomId($room_id);
  305. if ($onlineCustomerServices) {
  306. if ($allocate == 'busy') {
  307. // 将客服列表,按照工作繁忙程度正序排序, 这里没有离线的客服
  308. $onlineCustomerServices = array_column($onlineCustomerServices, null, 'busy_percent');
  309. ksort($onlineCustomerServices);
  310. // 取忙碌度最小,并且客服为 正常在线状态
  311. foreach ($onlineCustomerServices as $customerService) {
  312. if ($customerService['status'] == 'online') {
  313. $currentCustomerService = $customerService;
  314. break;
  315. }
  316. }
  317. } else if ($allocate == 'turns') {
  318. // 按照最后接入时间正序排序,这里没有离线的客服
  319. $onlineCustomerServices = array_column($onlineCustomerServices, null, 'last_time');
  320. ksort($onlineCustomerServices);
  321. // 取最后接待最早,并且客服为 正常在线状态
  322. foreach ($onlineCustomerServices as $customerService) {
  323. if ($customerService['status'] == 'online') {
  324. $currentCustomerService = $customerService;
  325. break;
  326. }
  327. }
  328. } else if ($allocate == 'random') {
  329. // 随机获取一名客服
  330. // 挑出来状态为在线的客服
  331. $onlineStatus = [];
  332. foreach ($onlineCustomerServices as $customerService) {
  333. if ($customerService['status'] == 'online') {
  334. $onlineStatus[] = $customerService;
  335. }
  336. }
  337. $onlineStatus = array_column($onlineStatus, null, 'id');
  338. $customer_service_id = 0;
  339. if ($onlineStatus) {
  340. $customer_service_id = array_rand($onlineStatus);
  341. }
  342. $currentCustomerService = $onlineStatus[$customer_service_id] ?? null;
  343. }
  344. if (!$currentCustomerService) {
  345. // 如果都不是 online 状态(说明全是 busy),默认取第一条
  346. $currentCustomerService = $onlineCustomerServices[0] ?? null;
  347. }
  348. }
  349. }
  350. return $currentCustomerService;
  351. }
  352. /**
  353. * 通过 session_id 记录客服信息,并且加入对应的客服组
  354. *
  355. * @param string $room_id 客服房间号
  356. * @param string $session_id 用户
  357. * @param array $customerService 客服信息
  358. * @return void
  359. */
  360. public function bindCustomerServiceBySessionId($room_id, $session_id, $customerService)
  361. {
  362. // 当前用户 uid 绑定的所有客户端 clientIds
  363. $clientIds = $this->getClientIdByUId($session_id, 'customer');
  364. // 将当前 session_id 绑定的 client_id 都加入当前客服组
  365. foreach ($clientIds as $client_id) {
  366. self::bindCustomerServiceByClientId($room_id, $client_id, $customerService);
  367. }
  368. // 添加 serviceLog
  369. $chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
  370. $this->getter('db')->createServiceLog($room_id, $chatUser, $customerService);
  371. }
  372. /**
  373. * 顾客session 记录客服信息,并且加入对应的客服组
  374. *
  375. * @param string $room_id 客服房间号
  376. * @param string $client_id 用户
  377. * @param array $customerService 客服信息
  378. * @return null
  379. */
  380. public function bindCustomerServiceByClientId($room_id, $client_id, $customerService)
  381. {
  382. // 更新用户的客服信息
  383. $this->updateSessionByClientId($client_id, [
  384. 'customer_service_id' => $customerService['id'],
  385. 'customer_service' => $customerService
  386. ]);
  387. // 更新客服的最后接入用户时间
  388. $this->getter()->updateCustomerServiceInfo($customerService['id'], ['last_time' => time()]);
  389. // 加入对应客服组,统计客服信息,通知用户客服上线等
  390. $room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
  391. $this->joinByClientId($client_id, $room);
  392. // 从等待接入组移除
  393. $room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
  394. $this->leaveByClientId($client_id, $room);
  395. }
  396. /**
  397. * 通过 session_id 将用户移除客服组
  398. *
  399. * @param string $room_id 房间号
  400. * @param string $session_id 用户
  401. * @param array $customerService 客服信息
  402. * @return null
  403. */
  404. public function unBindCustomerServiceBySessionId($room_id, $session_id, $customerService)
  405. {
  406. // 当前用户 uid 绑定的所有客户端 clientIds
  407. $clientIds = $this->getClientIdByUId($session_id, 'customer');
  408. // 将当前 session_id 绑定的 client_id 都加入当前客服组
  409. foreach ($clientIds as $client_id) {
  410. $this->unBindCustomerService($room_id, $client_id, $customerService);
  411. }
  412. }
  413. /**
  414. * 将用户的客服信息移除
  415. *
  416. * @param string $room_id 房间号
  417. * @param string $client_id 用户
  418. * @param array $customerService 客服信息
  419. * @return null
  420. */
  421. public function unBindCustomerService($room_id, $client_id, $customerService)
  422. {
  423. // 清空连接的客服的 session
  424. $this->updateSessionByClientId($client_id, [
  425. 'customer_service_id' => 0,
  426. 'customer_service' => []
  427. ]);
  428. // 移除对应客服组
  429. $room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
  430. $this->leaveByClientId($client_id, $room);
  431. }
  432. /**
  433. * 客服转接,移除旧的客服,加入新的客服
  434. *
  435. * @param string $room_id 客服房间号
  436. * @param string $session_id 用户
  437. * @param array $customerService 客服信息
  438. * @param array $newCustomerService 新客服信息
  439. * @return void
  440. */
  441. public function transferCustomerServiceBySessionId($room_id, $session_id, $customerService, $newCustomerService)
  442. {
  443. // 获取session_id 的 chatUser
  444. $chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
  445. // 结束 serviceLog,如果没有,会创建一条记录
  446. $this->endService($room_id, $chatUser, $customerService);
  447. // 解绑老客服
  448. $this->unBindCustomerServiceBySessionId($room_id, $session_id, $customerService);
  449. // 接入新客服
  450. $this->bindCustomerServiceBySessionId($room_id, $session_id, $newCustomerService);
  451. }
  452. /**
  453. * 结束服务
  454. *
  455. * @param string $room_id 客服房间号
  456. * @param object $chatUser 顾客信息
  457. * @param array $customerService 客服信息
  458. * @return void
  459. */
  460. public function endService($room_id, $chatUser, $customerService)
  461. {
  462. // 结束掉服务记录
  463. $this->getter('db')->endServiceLog($room_id, $chatUser, $customerService);
  464. // 记录客户最后服务的客服
  465. $chatUser->customer_service_id = $customerService['id'] ?? 0;
  466. $chatUser->save();
  467. }
  468. /**
  469. * 断开用户的服务
  470. *
  471. * @param string $room_id
  472. * @param string $session_id
  473. * @param array $customerService
  474. * @return void
  475. */
  476. public function breakCustomerServiceBySessionId($room_id, $session_id, $customerService)
  477. {
  478. // 获取session_id 的 chatUser
  479. $chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
  480. if ($chatUser) {
  481. // 结束 serviceLog,如果没有,会创建一条记录
  482. $this->endService($room_id, $chatUser, $customerService);
  483. }
  484. // 解绑老客服
  485. $this->unBindCustomerServiceBySessionId($room_id, $session_id, $customerService);
  486. }
  487. /**
  488. * 检查当前用户的客服身份
  489. *
  490. * @param string $room_id 房间号
  491. * @param string $auth 用户类型
  492. * @param integer $auth_id 用户 id
  493. * @return boolean|object
  494. */
  495. public function getCustomerServicesByAuth($auth_id, $auth)
  496. {
  497. $customerServices = $this->getter('db')->getCustomerServicesByAuth($auth_id, $auth);
  498. return $customerServices;
  499. }
  500. /**
  501. * 检查当前用户在当前房间是否是客服身份
  502. *
  503. * @param string $room_id 房间号
  504. * @param string $auth 用户类型
  505. * @param integer $auth_id 用户 id
  506. * @return boolean|object
  507. */
  508. public function checkIsCustomerService($room_id, $auth_id, $auth)
  509. {
  510. $currentCustomerService = $this->getter('db')->getCustomerServiceByAuthAndRoom($room_id, $auth_id, $auth);
  511. return $currentCustomerService ? : false;
  512. }
  513. /**
  514. * 客服登录
  515. *
  516. * @param string $room_id
  517. * @param integer $auth_id
  518. * @param string $auth
  519. * @return boolean
  520. */
  521. public function customerServiceLogin($room_id, $auth_id, $auth) : bool
  522. {
  523. if ($customerService = $this->checkIsCustomerService($room_id, $auth_id, $auth)) {
  524. // 保存 客服信息
  525. $this->session('customer_service_id', $customerService['id']);
  526. $this->session('customer_service', $customerService->toArray()); // toArray 减少内存占用
  527. return true;
  528. }
  529. return false;
  530. }
  531. /**
  532. * 客服上线
  533. *
  534. * @param string $room_id
  535. * @param integer $customer_service_id
  536. * @return void
  537. */
  538. public function customerServiceOnline($room_id, $customer_service_id)
  539. {
  540. // 当前 socket 绑定 客服 id
  541. $this->bindUId($customer_service_id, 'customer_service');
  542. // 更新客服状态
  543. $this->getter()->updateCustomerServiceStatus($customer_service_id, 'online');
  544. // 只把当前连接加入在线客服组,作为服务对象,多个连接同时登录一个客服,状态相互隔离
  545. $this->socket->join($this->getRoomName('identify', ['identify' => 'customer_service']));
  546. // 只把当前连接加入当前频道的客服组,为后续多商户做准备
  547. $this->socket->join($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
  548. // 保存当前客服身份
  549. $this->session('identify', 'customer_service');
  550. }
  551. /**
  552. * 客服离线
  553. *
  554. * @param string $room_id
  555. * @param integer $customer_service_id
  556. * @return void
  557. */
  558. public function customerServiceOffline($room_id, $customer_service_id)
  559. {
  560. // 只把当前连接移除在线客服组,多个连接同时登录一个客服,状态相互隔离
  561. $this->socket->leave($this->getRoomName('identify', ['identify' => 'customer_service']));
  562. // 只把当前连接移除当前频道的客服组,为后续多商户做准备
  563. $this->socket->leave($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
  564. // 当前 socket 解绑 客服 id
  565. $this->unBindUId($customer_service_id, 'customer_service');
  566. // 更新客服状态为离线
  567. $this->getter()->updateCustomerServiceStatus($customer_service_id, 'offline');
  568. }
  569. /**
  570. * 客服忙碌
  571. *
  572. * @param string $room_id
  573. * @param integer $customer_service_id
  574. * @return void
  575. */
  576. public function customerServiceBusy($room_id, $customer_service_id)
  577. {
  578. // 当前 socket 绑定 客服 id
  579. $this->bindUId($customer_service_id, 'customer_service');
  580. // (尝试重新加入,避免用户是从离线切换过来的)只把当前连接加入在线客服组,作为服务对象,多个连接同时登录一个客服,状态相互隔离
  581. $this->socket->join($this->getRoomName('identify', ['identify' => 'customer_service']));
  582. // (尝试重新加入,避免用户是从离线切换过来的)只把当前连接加入当前频道的客服组,为后续多商户做准备
  583. $this->socket->join($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
  584. // 更新客服状态为忙碌
  585. $this->getter()->updateCustomerServiceStatus($customer_service_id, 'busy');
  586. }
  587. /**
  588. * 客服退出
  589. *
  590. * @param [type] $room_id
  591. * @param [type] $customer_service_id
  592. * @return void
  593. */
  594. public function customerServiceLogout($room_id, $customer_service_id)
  595. {
  596. // 退出房间
  597. $this->session('room_id', null);
  598. // 移除当前客服身份
  599. $this->session('identify', null);
  600. // 移除客服信息
  601. $this->session('customer_service_id', null);
  602. $this->session('customer_service', null); // toArray 减少内存占用
  603. }
  604. /**
  605. * 顾客上线
  606. *
  607. * @return void
  608. */
  609. public function customerOnline()
  610. {
  611. // 用户信息
  612. $session_id = $this->session('session_id');
  613. $auth = $this->session('auth');
  614. // 当前 socket 绑定 顾客 id
  615. $this->bindUId($session_id, 'customer');
  616. // 加入在线顾客组,作为被服务对象
  617. $this->socket->join($this->getRoomName('identify', ['identify' => 'customer']));
  618. // 保存当前顾客身份
  619. $this->session('identify', 'customer');
  620. }
  621. /**
  622. * 顾客下线
  623. *
  624. * @return void
  625. */
  626. public function customerOffline()
  627. {
  628. // 用户信息
  629. $session_id = $this->session('session_id');
  630. $customer_service_id = $this->session('customer_service_id');
  631. $room_id = $this->session('room_id');
  632. $customerService = $this->session('customer_service');
  633. $chatUser = $this->session('chat_user');
  634. // 退出房间
  635. $this->session('room_id', null);
  636. // 当前 socket 绑定 顾客 id
  637. $this->unbindUId($session_id, 'customer');
  638. // 离开在线顾客组,作为被服务对象
  639. $this->socket->leave($this->getRoomName('identify', ['identify' => 'customer']));
  640. // 移除当前顾客身份
  641. $this->session('identify', null);
  642. // 如果有客服正在服务,移除
  643. if ($customer_service_id) {
  644. // 离开所在客服的服务对象组
  645. $this->socket->leave($this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customer_service_id]));
  646. // 移除客服信息
  647. $this->session('customer_service_id', null);
  648. $this->session('customer_service', null);
  649. // 获取session_id 的 chatUser
  650. $chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
  651. if ($this->getter('socket')->isOnLineCustomerById($session_id)) {
  652. // 结束 serviceLog,如果没有,会创建一条记录
  653. $this->endService($room_id, $chatUser, $customerService);
  654. }
  655. } else {
  656. // 离开等待组
  657. $this->socket->leave($this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]));
  658. }
  659. }
  660. /**
  661. * 断开连接解绑
  662. *
  663. * @return void
  664. */
  665. public function disconnectUnbindAll()
  666. {
  667. // 因为 session 是存在 socket 上的,服务端重启断开连接,或者刷新浏览器 socket 会重新连接,所以存的 session 会全部清空
  668. // 服务端重启 会导致 bind 到 io 实例的 bind 的数据丢失,但是如果是前端用户刷新浏览器,discount 事件中就必须要解绑 bind 数据
  669. $room_id = $this->session('room_id');
  670. $session_id = $this->session('session_id');
  671. $auth = $this->session('auth');
  672. $identify = $this->session('identify') ? : '';
  673. $customerService = $this->session('customer_service');
  674. // 解绑顾客身份
  675. if ($identify == 'customer') {
  676. $this->unbindUId($session_id, 'customer');
  677. if ($customerService) {
  678. // 连接着客服,将客服信息暂存 nsp 中,防止刷新重新连接
  679. $this->nspConnectionAdd($room_id, $session_id, $customerService['id']);
  680. // 如果有客服,定时判断,如果客服掉线了,关闭
  681. Timer::add(10, function () use ($room_id, $session_id, $customerService) {
  682. // 十秒之后顾客不在线,说明是真的下线了
  683. if (!$this->getter('socket')->isOnLineCustomerById($session_id)) {
  684. $chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
  685. $this->endService($room_id, $chatUser, $customerService);
  686. }
  687. }, null, false);
  688. }
  689. }
  690. // 解绑客服身份
  691. if ($identify == 'customer_service') {
  692. $customer_service_id = $this->session('customer_service_id');
  693. $this->unbindUId($customer_service_id, 'customer_service');
  694. }
  695. // 将当前的 用户与 client 解绑
  696. $this->unbindUId($session_id, $auth);
  697. }
  698. }