removeNodeDevices($nodeId, $userId); if (!empty($ips)) { $fields = []; foreach ($ips as $ip) { $fields["{$nodeId}:{$ip}"] = $timestamp; } Redis::hMset($key, $fields); Redis::expire($key, self::TTL); } $this->notifyUpdate($userId); } /** * 获取某节点的所有设备数据 * 返回: {userId: [ip1, ip2, ...], ...} */ public function getNodeDevices(int $nodeId): array { $keys = Redis::keys(self::PREFIX . '*'); $prefix = "{$nodeId}:"; $result = []; foreach ($keys as $key) { $actualKey = $this->removeRedisPrefix($key); $uid = (int) substr($actualKey, strlen(self::PREFIX)); $data = Redis::hgetall($actualKey); foreach ($data as $field => $timestamp) { if (str_starts_with($field, $prefix)) { $ip = substr($field, strlen($prefix)); $result[$uid][] = $ip; } } } return $result; } /** * 删除某节点某用户的设备 */ public function removeNodeDevices(int $nodeId, int $userId): void { $key = self::PREFIX . $userId; $prefix = "{$nodeId}:"; foreach (Redis::hkeys($key) as $field) { if (str_starts_with($field, $prefix)) { Redis::hdel($key, $field); } } } /** * 清除节点所有设备数据(用于节点断开连接) */ public function clearAllNodeDevices(int $nodeId): array { $oldDevices = $this->getNodeDevices($nodeId); $prefix = "{$nodeId}:"; foreach ($oldDevices as $userId => $ips) { $key = self::PREFIX . $userId; foreach (Redis::hkeys($key) as $field) { if (str_starts_with($field, $prefix)) { Redis::hdel($key, $field); } } } return array_keys($oldDevices); } /** * get user device count (deduplicated by IP, filter expired data) */ public function getDeviceCount(int $userId): int { $data = Redis::hgetall(self::PREFIX . $userId); $now = time(); $ips = []; foreach ($data as $field => $timestamp) { if ($now - $timestamp <= self::TTL) { $ips[] = substr($field, strpos($field, ':') + 1); } } return count(array_unique($ips)); } /** * get user device count (for alivelist interface) */ public function getAliveList(Collection $users): array { if ($users->isEmpty()) { return []; } $result = []; foreach ($users as $user) { $count = $this->getDeviceCount($user->id); if ($count > 0) { $result[$user->id] = $count; } } return $result; } /** * get devices of multiple users (for sync.devices, filter expired data) */ public function getUsersDevices(array $userIds): array { $result = []; $now = time(); foreach ($userIds as $userId) { $data = Redis::hgetall(self::PREFIX . $userId); if (!empty($data)) { $ips = []; foreach ($data as $field => $timestamp) { if ($now - $timestamp <= self::TTL) { $ips[] = substr($field, strpos($field, ':') + 1); } } if (!empty($ips)) { $result[$userId] = array_unique($ips); } } } return $result; } /** * notify update (throttle control) */ public function notifyUpdate(int $userId): void { $dbThrottleKey = "device:db_throttle:{$userId}"; // if (Redis::setnx($dbThrottleKey, 1)) { // Redis::expire($dbThrottleKey, self::DB_THROTTLE); User::query() ->whereKey($userId) ->update([ 'online_count' => $this->getDeviceCount($userId), 'last_online_at' => now(), ]); // } } }