add
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace app\services\chat;
|
||||
|
||||
use app\models\chat\ChatSession;
|
||||
use app\models\chat\ChatAdminStatus;
|
||||
use think\facade\Db;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* 会话分配服务
|
||||
* Class AssignService
|
||||
* @package app\services\chat
|
||||
*/
|
||||
class AssignService
|
||||
{
|
||||
// Redis Key 前缀 (specs.md Section 3)
|
||||
private const LOCK_PREFIX = 'cs:lock:assign:';
|
||||
private const ONLINE_PREFIX = 'cs:online:agent:';
|
||||
private const LOAD_PREFIX = 'cs:agent:load:';
|
||||
private const QUEUE_KEY = 'cs:queue:pending';
|
||||
private const SESSION_OWNER_PREFIX = 'cs:session:owner:';
|
||||
|
||||
// 约束参数 (specs.md CC-01~CC-04)
|
||||
private const LOCK_TTL = 3000; // 分配锁超时 3秒
|
||||
private const MAX_SESSIONS = 10; // 单客服最大会话数
|
||||
|
||||
/**
|
||||
* 分配会话给客服
|
||||
* @param int $userId 用户ID
|
||||
* @param int $sessionId 会话ID
|
||||
* @return int|null 分配的客服ID,null表示无可用客服
|
||||
*/
|
||||
public function assignSession(int $userId, int $sessionId): ?int
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$lockKey = self::LOCK_PREFIX . $userId;
|
||||
|
||||
// 1. 获取分配锁 (防止并发双分配)
|
||||
$lockValue = uniqid('', true);
|
||||
$acquired = $redis->set($lockKey, $lockValue, ['NX', 'PX' => self::LOCK_TTL]);
|
||||
|
||||
if (!$acquired) {
|
||||
// 锁被占用,检查是否已有活跃会话
|
||||
return $this->getExistingSessionAgent($userId);
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 检查用户是否已有活跃会话
|
||||
$existingAgent = $this->getExistingSessionAgent($userId);
|
||||
if ($existingAgent !== null) {
|
||||
return $existingAgent;
|
||||
}
|
||||
|
||||
// 3. 获取所有在线客服
|
||||
$onlineAgents = $this->getOnlineAgents();
|
||||
if (empty($onlineAgents)) {
|
||||
// 无在线客服,进入留言队列
|
||||
$this->addToOfflineQueue($userId, $sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. 选择会话数最少的客服
|
||||
$selectedAgent = $this->selectLeastLoadAgent($onlineAgents);
|
||||
if ($selectedAgent === null) {
|
||||
// 所有客服已满载
|
||||
$this->addToOfflineQueue($userId, $sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. 更新会话归属
|
||||
$this->bindSessionToAgent($sessionId, $selectedAgent);
|
||||
|
||||
// 6. 增加客服负载计数
|
||||
$redis->incr(self::LOAD_PREFIX . $selectedAgent);
|
||||
|
||||
return $selectedAgent;
|
||||
|
||||
} finally {
|
||||
// 释放锁 (仅释放自己持有的锁)
|
||||
$this->releaseLock($lockKey, $lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线客服列表
|
||||
*/
|
||||
public function getOnlineAgents(): array
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$enabledAdmins = ChatAdminStatus::getEnabledAdminIds();
|
||||
$onlineAgents = [];
|
||||
|
||||
foreach ($enabledAdmins as $adminId) {
|
||||
if ($redis->exists(self::ONLINE_PREFIX . $adminId)) {
|
||||
$onlineAgents[] = $adminId;
|
||||
}
|
||||
}
|
||||
|
||||
return $onlineAgents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择会话数最少的客服
|
||||
*/
|
||||
public function selectLeastLoadAgent(array $onlineAgents): ?int
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$loads = [];
|
||||
|
||||
foreach ($onlineAgents as $agentId) {
|
||||
$load = (int)$redis->get(self::LOAD_PREFIX . $agentId) ?: 0;
|
||||
$maxSessions = $this->getAgentMaxSessions($agentId);
|
||||
|
||||
if ($load < $maxSessions) {
|
||||
$loads[$agentId] = $load;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($loads)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回负载最小的客服
|
||||
asort($loads);
|
||||
return array_key_first($loads);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到离线队列 (按余额降序)
|
||||
*/
|
||||
public function addToOfflineQueue(int $userId, int $sessionId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
// 获取用户余额
|
||||
$user = Db::name('user')->where('id', $userId)->find();
|
||||
$balance = $user['money'] ?? 0;
|
||||
|
||||
// ZSET score 使用负余额实现降序 (余额高的先处理)
|
||||
$score = -$balance;
|
||||
$member = json_encode([
|
||||
'userId' => $userId,
|
||||
'sessionId' => $sessionId,
|
||||
'time' => time()
|
||||
]);
|
||||
|
||||
$redis->zAdd(self::QUEUE_KEY, $score, $member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服上线时处理队列
|
||||
*/
|
||||
public function processOfflineQueue(int $adminId): array
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$processed = [];
|
||||
|
||||
while (true) {
|
||||
// 获取队列中优先级最高的会话 (score最小 = 余额最高)
|
||||
$items = $redis->zRange(self::QUEUE_KEY, 0, 0);
|
||||
|
||||
if (empty($items)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$item = json_decode($items[0], true);
|
||||
|
||||
// 检查客服是否还能接单
|
||||
$currentLoad = (int)$redis->get(self::LOAD_PREFIX . $adminId) ?: 0;
|
||||
$maxSessions = $this->getAgentMaxSessions($adminId);
|
||||
|
||||
if ($currentLoad >= $maxSessions) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 从队列移除
|
||||
$redis->zRem(self::QUEUE_KEY, $items[0]);
|
||||
|
||||
// 分配会话
|
||||
$this->bindSessionToAgent($item['sessionId'], $adminId);
|
||||
$redis->incr(self::LOAD_PREFIX . $adminId);
|
||||
|
||||
$processed[] = $item;
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放会话 (会话结束时调用)
|
||||
*/
|
||||
public function releaseSession(int $sessionId, int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
// 删除会话归属
|
||||
$redis->del(self::SESSION_OWNER_PREFIX . $sessionId);
|
||||
|
||||
// 减少客服负载
|
||||
$load = (int)$redis->get(self::LOAD_PREFIX . $adminId) ?: 0;
|
||||
if ($load > 0) {
|
||||
$redis->decr(self::LOAD_PREFIX . $adminId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客服在线状态
|
||||
*/
|
||||
public function setAgentOnline(int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->setex(self::ONLINE_PREFIX . $adminId, 60, '1');
|
||||
ChatAdminStatus::updateLastOnlineTime($adminId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新客服在线状态 (心跳续期)
|
||||
*/
|
||||
public function refreshAgentOnline(int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->expire(self::ONLINE_PREFIX . $adminId, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客服离线
|
||||
*/
|
||||
public function setAgentOffline(int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->del(self::ONLINE_PREFIX . $adminId);
|
||||
$redis->del(self::LOAD_PREFIX . $adminId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客服最大会话数
|
||||
*/
|
||||
private function getAgentMaxSessions(int $adminId): int
|
||||
{
|
||||
$status = ChatAdminStatus::getByAdminId($adminId);
|
||||
return $status['max_sessions'] ?? self::MAX_SESSIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户已有会话的客服ID
|
||||
*/
|
||||
private function getExistingSessionAgent(int $userId): ?int
|
||||
{
|
||||
$session = ChatSession::getActiveByUserId($userId);
|
||||
return $session['admin_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定会话到客服
|
||||
*/
|
||||
private function bindSessionToAgent(int $sessionId, int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
// 更新数据库
|
||||
ChatSession::where('id', $sessionId)->update([
|
||||
'admin_id' => $adminId,
|
||||
'status' => ChatSession::STATUS_ACTIVE,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
// 设置Redis映射
|
||||
$redis->set(self::SESSION_OWNER_PREFIX . $sessionId, $adminId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分配锁
|
||||
*/
|
||||
private function releaseLock(string $key, string $value): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
// Lua脚本保证原子性:仅当值匹配时才删除
|
||||
$script = <<<LUA
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
LUA;
|
||||
$redis->eval($script, [$key, $value], 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis实例
|
||||
*/
|
||||
private function getRedis(): \Redis
|
||||
{
|
||||
return Cache::store('redis')->handler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace app\services\chat;
|
||||
|
||||
use app\models\chat\ChatMessage;
|
||||
use app\models\chat\ChatSession;
|
||||
use app\utils\Snowflake;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* 消息服务
|
||||
* Class MessageService
|
||||
* @package app\services\chat
|
||||
*/
|
||||
class MessageService
|
||||
{
|
||||
// 约束参数 (specs.md CC-05~CC-09)
|
||||
private const MAX_RETRY = 3;
|
||||
private const RETRY_DELAYS = [1000, 2000, 4000]; // ms
|
||||
private const MAX_CONTENT_LENGTH = 500;
|
||||
private const RECONNECT_FETCH_LIMIT = 50;
|
||||
|
||||
/**
|
||||
* 创建消息
|
||||
* @param array $data [session_id, sender_type, sender_id, msg_type, content]
|
||||
* @return array 消息数据
|
||||
*/
|
||||
public function createMessage(array $data): array
|
||||
{
|
||||
// 生成雪花ID
|
||||
$msgId = Snowflake::getInstance()->nextId();
|
||||
|
||||
// 内容长度限制
|
||||
$content = $data['content'] ?? '';
|
||||
if (mb_strlen($content) > self::MAX_CONTENT_LENGTH) {
|
||||
$content = mb_substr($content, 0, self::MAX_CONTENT_LENGTH);
|
||||
}
|
||||
|
||||
$message = [
|
||||
'msg_id' => $msgId,
|
||||
'session_id' => $data['session_id'],
|
||||
'sender_type' => $data['sender_type'],
|
||||
'sender_id' => $data['sender_id'],
|
||||
'msg_type' => $data['msg_type'] ?? ChatMessage::MSG_TYPE_TEXT,
|
||||
'content' => $content,
|
||||
'status' => ChatMessage::STATUS_PENDING,
|
||||
'retry_count' => 0,
|
||||
'create_time' => time(),
|
||||
];
|
||||
|
||||
// 幂等性检查
|
||||
if (isset($data['client_msg_id']) && ChatMessage::existsByMsgId($data['client_msg_id'])) {
|
||||
return ChatMessage::where('msg_id', $data['client_msg_id'])->find()->toArray();
|
||||
}
|
||||
|
||||
// 入库
|
||||
$id = ChatMessage::insertGetId($message);
|
||||
$message['id'] = $id;
|
||||
|
||||
// 更新会话最后消息
|
||||
ChatSession::where('id', $data['session_id'])->update([
|
||||
'last_msg_id' => $msgId,
|
||||
'last_msg_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息到目标连接
|
||||
* @param int $msgId 消息ID
|
||||
* @param int $targetFd 目标连接FD
|
||||
* @param array $payload 消息内容
|
||||
* @return bool
|
||||
*/
|
||||
public function pushMessage(int $msgId, int $targetFd, array $payload): bool
|
||||
{
|
||||
$server = app('swoole.server');
|
||||
|
||||
for ($retry = 0; $retry <= self::MAX_RETRY; $retry++) {
|
||||
// 检查目标连接是否有效
|
||||
if (!$server->isEstablished($targetFd)) {
|
||||
$this->updateMessageStatus($msgId, ChatMessage::STATUS_PENDING);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试推送
|
||||
$result = $server->push($targetFd, json_encode($payload));
|
||||
|
||||
if ($result) {
|
||||
$this->updateMessageStatus($msgId, ChatMessage::STATUS_SENT);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 推送失败,记录重试次数
|
||||
$this->incrementRetryCount($msgId);
|
||||
|
||||
if ($retry < self::MAX_RETRY) {
|
||||
// 指数退避等待
|
||||
usleep(self::RETRY_DELAYS[$retry] * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 超过最大重试次数,标记为failed
|
||||
$this->updateMessageStatus($msgId, ChatMessage::STATUS_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
public function updateMessageStatus(int $msgId, int $status): void
|
||||
{
|
||||
$update = ['status' => $status];
|
||||
|
||||
if ($status === ChatMessage::STATUS_DELIVERED) {
|
||||
$update['delivered_time'] = time();
|
||||
} elseif ($status === ChatMessage::STATUS_READ) {
|
||||
$update['read_time'] = time();
|
||||
}
|
||||
|
||||
ChatMessage::where('msg_id', $msgId)->update($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话未读消息 (重连后拉取)
|
||||
*/
|
||||
public function getUnreadMessages(int $sessionId, int $receiverType, int $limit = null): array
|
||||
{
|
||||
$limit = $limit ?? self::RECONNECT_FETCH_LIMIT;
|
||||
return ChatMessage::getUnreadBySessionId($sessionId, $receiverType, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量标记消息为已读
|
||||
*/
|
||||
public function markMessagesAsRead(array $msgIds): void
|
||||
{
|
||||
if (empty($msgIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessage::whereIn('msg_id', $msgIds)
|
||||
->where('status', '<', ChatMessage::STATUS_READ)
|
||||
->update([
|
||||
'status' => ChatMessage::STATUS_READ,
|
||||
'read_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息为已送达
|
||||
*/
|
||||
public function markMessageDelivered(int $msgId): void
|
||||
{
|
||||
ChatMessage::where('msg_id', $msgId)
|
||||
->where('status', '<', ChatMessage::STATUS_DELIVERED)
|
||||
->update([
|
||||
'status' => ChatMessage::STATUS_DELIVERED,
|
||||
'delivered_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息历史
|
||||
*/
|
||||
public function getMessageHistory(int $sessionId, int $limit = 50, int $lastId = 0): array
|
||||
{
|
||||
return ChatMessage::getBySessionId($sessionId, $limit, $lastId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加重试次数
|
||||
*/
|
||||
private function incrementRetryCount(int $msgId): void
|
||||
{
|
||||
ChatMessage::where('msg_id', $msgId)->inc('retry_count')->update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息内容
|
||||
*/
|
||||
public function validateContent(string $content, int $msgType): array
|
||||
{
|
||||
if ($msgType === ChatMessage::MSG_TYPE_TEXT) {
|
||||
if (empty(trim($content))) {
|
||||
return ['valid' => false, 'error' => '消息内容不能为空'];
|
||||
}
|
||||
if (mb_strlen($content) > self::MAX_CONTENT_LENGTH) {
|
||||
return ['valid' => false, 'error' => '消息内容超过' . self::MAX_CONTENT_LENGTH . '字符限制'];
|
||||
}
|
||||
} elseif ($msgType === ChatMessage::MSG_TYPE_IMAGE) {
|
||||
if (empty($content) || !filter_var($content, FILTER_VALIDATE_URL)) {
|
||||
return ['valid' => false, 'error' => '图片URL无效'];
|
||||
}
|
||||
}
|
||||
|
||||
return ['valid' => true, 'error' => null];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace app\services\chat;
|
||||
|
||||
use app\models\chat\ChatSession;
|
||||
use app\models\chat\ChatMessage;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 会话服务
|
||||
* Class SessionService
|
||||
* @package app\services\chat
|
||||
*/
|
||||
class SessionService
|
||||
{
|
||||
// Redis Key 前缀
|
||||
private const USER_ACTIVE_SESSION_PREFIX = 'cs:user:active_session:';
|
||||
private const CONN_USER_PREFIX = 'cs:conn:user:';
|
||||
private const CONN_AGENT_PREFIX = 'cs:conn:agent:';
|
||||
|
||||
private AssignService $assignService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->assignService = new AssignService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
* @param int $userId 用户ID
|
||||
* @param int $source 来源 1=PC 2=Game 3=Portal
|
||||
* @return array 会话数据
|
||||
*/
|
||||
public function createSession(int $userId, int $source): array
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
// 检查是否已有活跃会话
|
||||
$existingSession = ChatSession::getActiveByUserId($userId);
|
||||
if ($existingSession) {
|
||||
return $existingSession;
|
||||
}
|
||||
|
||||
// 创建新会话
|
||||
$session = [
|
||||
'user_id' => $userId,
|
||||
'source' => $source,
|
||||
'status' => ChatSession::STATUS_PENDING,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
];
|
||||
|
||||
$sessionId = ChatSession::insertGetId($session);
|
||||
$session['id'] = $sessionId;
|
||||
|
||||
// 缓存用户活跃会话
|
||||
$redis->set(self::USER_ACTIVE_SESSION_PREFIX . $userId, $sessionId);
|
||||
|
||||
// 尝试分配客服
|
||||
$adminId = $this->assignService->assignSession($userId, $sessionId);
|
||||
if ($adminId !== null) {
|
||||
$session['admin_id'] = $adminId;
|
||||
$session['status'] = ChatSession::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户活跃会话
|
||||
*/
|
||||
public function getActiveSession(int $userId): ?array
|
||||
{
|
||||
return ChatSession::getActiveByUserId($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情(含用户信息)
|
||||
*/
|
||||
public function getSessionDetail(int $sessionId): ?array
|
||||
{
|
||||
$session = ChatSession::find($sessionId);
|
||||
if (!$session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$session = $session->toArray();
|
||||
|
||||
// 获取用户信息
|
||||
$user = Db::name('user')->where('id', $session['user_id'])->find();
|
||||
if ($user) {
|
||||
$session['user_info'] = [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'nickname' => $user['nickname'] ?? $user['username'],
|
||||
'money' => $user['money'],
|
||||
'agent_id' => $user['agent_id'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
*/
|
||||
public function endSession(int $sessionId, int $operatorId): bool
|
||||
{
|
||||
$session = ChatSession::find($sessionId);
|
||||
if (!$session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$session = $session->toArray();
|
||||
|
||||
// 只能结束进行中的会话
|
||||
if ($session['status'] === ChatSession::STATUS_ENDED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 更新会话状态
|
||||
ChatSession::where('id', $sessionId)->update([
|
||||
'status' => ChatSession::STATUS_ENDED,
|
||||
'end_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
// 清理Redis缓存
|
||||
$redis = $this->getRedis();
|
||||
$redis->del(self::USER_ACTIVE_SESSION_PREFIX . $session['user_id']);
|
||||
|
||||
// 释放客服会话配额
|
||||
if ($session['admin_id']) {
|
||||
$this->assignService->releaseSession($sessionId, $session['admin_id']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转接会话
|
||||
*/
|
||||
public function transferSession(int $sessionId, int $newAdminId): bool
|
||||
{
|
||||
$session = ChatSession::find($sessionId);
|
||||
if (!$session || $session['status'] !== ChatSession::STATUS_ACTIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldAdminId = $session['admin_id'];
|
||||
|
||||
// 更新会话归属
|
||||
ChatSession::where('id', $sessionId)->update([
|
||||
'admin_id' => $newAdminId,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
// 更新Redis
|
||||
$redis = $this->getRedis();
|
||||
$redis->set('cs:session:owner:' . $sessionId, $newAdminId);
|
||||
|
||||
// 调整负载计数
|
||||
if ($oldAdminId) {
|
||||
$oldLoad = (int)$redis->get('cs:agent:load:' . $oldAdminId) ?: 0;
|
||||
if ($oldLoad > 0) {
|
||||
$redis->decr('cs:agent:load:' . $oldAdminId);
|
||||
}
|
||||
}
|
||||
$redis->incr('cs:agent:load:' . $newAdminId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话评价
|
||||
*/
|
||||
public function rateSession(int $sessionId, int $rating, ?string $content = null): bool
|
||||
{
|
||||
$session = ChatSession::find($sessionId);
|
||||
if (!$session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 评分范围 1-5
|
||||
$rating = max(1, min(5, $rating));
|
||||
|
||||
ChatSession::where('id', $sessionId)->update([
|
||||
'rating' => $rating,
|
||||
'rating_content' => $content ? mb_substr($content, 0, 500) : null,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户连接
|
||||
*/
|
||||
public function registerUserConnection(int $userId, int $fd): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->setex(self::CONN_USER_PREFIX . $userId, 60, $fd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册客服连接
|
||||
*/
|
||||
public function registerAgentConnection(int $adminId, int $fd): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->setex(self::CONN_AGENT_PREFIX . $adminId, 60, $fd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新用户连接TTL
|
||||
*/
|
||||
public function refreshUserConnection(int $userId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->expire(self::CONN_USER_PREFIX . $userId, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新客服连接TTL
|
||||
*/
|
||||
public function refreshAgentConnection(int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->expire(self::CONN_AGENT_PREFIX . $adminId, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户连接FD
|
||||
*/
|
||||
public function getUserFd(int $userId): ?int
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$fd = $redis->get(self::CONN_USER_PREFIX . $userId);
|
||||
return $fd ? (int)$fd : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客服连接FD
|
||||
*/
|
||||
public function getAgentFd(int $adminId): ?int
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$fd = $redis->get(self::CONN_AGENT_PREFIX . $adminId);
|
||||
return $fd ? (int)$fd : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户连接
|
||||
*/
|
||||
public function clearUserConnection(int $userId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->del(self::CONN_USER_PREFIX . $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理客服连接
|
||||
*/
|
||||
public function clearAgentConnection(int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->del(self::CONN_AGENT_PREFIX . $adminId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客服会话列表
|
||||
*/
|
||||
public function getAgentSessions(int $adminId): array
|
||||
{
|
||||
$sessions = ChatSession::getActiveByAdminId($adminId);
|
||||
|
||||
// 附加用户信息和未读数
|
||||
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'] = ChatMessage::where('session_id', $session['id'])
|
||||
->where('sender_type', ChatMessage::SENDER_USER)
|
||||
->where('status', '<', ChatMessage::STATUS_READ)
|
||||
->count();
|
||||
}
|
||||
|
||||
return $sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待分配会话列表
|
||||
*/
|
||||
public function getPendingSessions(): array
|
||||
{
|
||||
$sessions = ChatSession::where('status', ChatSession::STATUS_PENDING)
|
||||
->order('create_time', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
return $sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis实例
|
||||
*/
|
||||
private function getRedis(): \Redis
|
||||
{
|
||||
return Cache::store('redis')->handler();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user