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 = <<eval($script, [$key, $value], 1); } /** * 获取Redis实例 */ private function getRedis(): \Redis { return Cache::store('redis')->handler(); } }