房间 ID 数组 */ public function getUserRooms(string $username): array { $rooms = []; $prefix = config('database.redis.options.prefix', ''); $cursor = '0'; do { // scan 带前缀通配,返回的 key 也带前缀 [$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]); foreach ($keys ?? [] as $fullKey) { // 去掉前缀得到 Laravel Redis Facade 认识的短少 key $shortKey = $prefix ? ltrim(substr($fullKey, strlen($prefix)), '') : $fullKey; if (Redis::hexists($shortKey, $username)) { preg_match('/room:(\d+):users/', $shortKey, $m); if (isset($m[1])) { $rooms[] = (int) $m[1]; } } } } while ($cursor !== '0'); return $rooms; } /** * 获取指定房间的所有在线用户列表。 * * @param int $roomId 房间ID */ public function getRoomUsers(int $roomId): array { $key = "room:{$roomId}:users"; $users = Redis::hgetall($key); $result = []; foreach ($users as $username => $jsonInfo) { $result[$username] = json_decode($jsonInfo, true); } return $result; } /** * 扫描 Redis,返回当前所有有在线用户的房间 ID 数组(用于全局广播)。 * * @return array */ public function getAllActiveRoomIds(): array { $roomIds = []; $prefix = config('database.redis.options.prefix', ''); $cursor = '0'; do { // scan 带前缀通配,返回的 key 也带前缀 [$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]); foreach ($keys ?? [] as $fullKey) { $shortKey = $prefix ? substr($fullKey, strlen($prefix)) : $fullKey; // 只有 hash 非空(有在线用户)才算活跃房间 if (Redis::hlen($shortKey) > 0) { preg_match('/room:(\d+):users/', $shortKey, $m); if (isset($m[1])) { $roomIds[] = (int) $m[1]; } } } } while ($cursor !== '0'); return array_unique($roomIds); } /** * 将一条新发言推入 Redis 列表,并限制最大保留数量,防止内存泄漏。 * * @param int $roomId 房间ID * @param array $message 发言数据包 * @param int $maxKeep 最大保留条数 */ public function pushMessage(int $roomId, array $message, int $maxKeep = 100): void { $key = "room:{$roomId}:messages"; Redis::rpush($key, json_encode($message, JSON_UNESCAPED_UNICODE)); // 仅保留最新的 $maxKeep 条,旧的自动截断弹弃 (-$maxKeep 到 -1 的区间保留) Redis::ltrim($key, -$maxKeep, -1); } /** * 清除指定房间的所有消息缓存(管理员全员清屏)。 * * @param int $roomId 房间ID */ public function clearMessages(int $roomId): void { $key = "room:{$roomId}:messages"; Redis::del($key); } /** * 清除指定房间内,关于某个用户的旧欢迎消息(支持普通人、管理员、新人)。 * 保证聊天记录里只保留最新的一条,解决频繁进出造成的刷屏问题。 * * @param int $roomId 房间ID * @param string $username 用户名 */ public function removeOldWelcomeMessages(int $roomId, string $username): void { $key = "room:{$roomId}:messages"; $messages = Redis::lrange($key, 0, -1); if (empty($messages)) { return; } $filtered = []; foreach ($messages as $msgJson) { $msg = json_decode($msgJson, true); // 只要消息里带了 welcome_user 且等于当前用户,就抛弃这条旧的 if ($msg && isset($msg['welcome_user']) && $msg['welcome_user'] === $username) { continue; } $filtered[] = $msgJson; } // 重新写回 Redis(如果发生了过滤) if (count($filtered) !== count($messages)) { Redis::del($key); if (! empty($filtered)) { Redis::rpush($key, ...$filtered); } } } /** * 获取指定房间的新发言记录。 * 在高频长轮询或前端断线重连拉取时使用。 * * @param int $roomId 房间ID * @param int $lastId 客户端收到的最后一条发言的ID */ public function getNewMessages(int $roomId, int $lastId): array { $key = "room:{$roomId}:messages"; $messages = Redis::lrange($key, 0, -1); // 获取当前缓存的全部 $newMessages = []; foreach ($messages as $msgJson) { $msg = json_decode($msgJson, true); if (isset($msg['id']) && $msg['id'] > $lastId) { $newMessages[] = $msg; } } return $newMessages; } /** * 分布式发号器:为房间内的新消息生成绝对递增的 ID。 * 解决了 MySQL 自增在极致并发下依赖读锁的问题。 * * @param int $roomId 房间ID */ public function nextMessageId(int $roomId): int { $key = "room:{$roomId}:message_seq"; return Redis::incr($key); } /** * 读取系统参数并设置合理的缓存。 * 替代每次都去 MySQL query 的性能损耗。 * * @param string $alias 别名 */ public function getSysParam(string $alias): ?string { return Cache::remember("sys_param:{$alias}", 60, function () use ($alias) { return SysParam::where('alias', $alias)->value('body'); }); } /** * 更新系统参数并刷新 Cache 缓存。 * * @param string $alias 别名 * @param mixed $value 参数值 */ public function setSysParam(string $alias, mixed $value): void { Cache::put("sys_param:{$alias}", $value); } /** * 提供一个分布式的 Redis 互斥锁包围。 * 防止并发抢占资源(如同时创建重名房间,同时修改某一敏感属性)。 * * @param string $key 锁名称 * @param callable $callback 获得锁后执行的闭包 * @param int $timeout 锁超时时间(秒) * @return mixed */ public function withLock(string $key, callable $callback, int $timeout = 5) { $lockKey = "lock:{$key}"; // 尝试获取锁,set nx ex $isLocked = Redis::set($lockKey, 1, 'EX', $timeout, 'NX'); if (! $isLocked) { // 获取锁失败,业务可自行决定抛异常或重试 throw new \Exception("The lock {$key} is currently held by another process."); } try { return $callback(); } finally { Redis::del($lockKey); } } }