diff --git a/app/Services/ChatStateService.php b/app/Services/ChatStateService.php index 7f73239..de3f68d 100644 --- a/app/Services/ChatStateService.php +++ b/app/Services/ChatStateService.php @@ -28,6 +28,8 @@ class ChatStateService { $key = "room:{$roomId}:users"; Redis::hset($key, $username, json_encode($info, JSON_UNESCAPED_UNICODE)); + // 写入活跃标记(90秒 TTL,心跳每次刷新;超出则被认定为掉线) + Redis::setex("room:{$roomId}:alive:{$username}", 90, 1); } /** @@ -40,6 +42,19 @@ class ChatStateService { $key = "room:{$roomId}:users"; Redis::hdel($key, $username); + // 同时删除活跃标记 + Redis::del("room:{$roomId}:alive:{$username}"); + } + + /** + * 刷新用户心跳活跃标记(心跳接口调用)。 + * + * @param int $roomId 房间ID + * @param string $username 用户名 + */ + public function refreshAlive(int $roomId, string $username): void + { + Redis::setex("room:{$roomId}:alive:{$username}", 90, 1); } /** @@ -85,6 +100,13 @@ class ChatStateService $result = []; foreach ($users as $username => $jsonInfo) { + // 过滤掌心跳已超出活跃窗口的用户(掉线超 90秒) + if (! Redis::exists("room:{$roomId}:alive:{$username}")) { + // 懒清理:顺手将僵尸记录从 hash 中移除 + Redis::hdel($key, $username); + + continue; + } $result[$username] = json_decode($jsonInfo, true); } diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index b931cca..ced62d4 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -42,15 +42,22 @@ let _maxMsgId = 0; // 记录当前收到的最大消息 ID // ── Tab 切换 ────────────────────────────────────── + let _roomsRefreshTimer = null; + function switchTab(tab) { // 切换名单/房间 面板 ['users', 'rooms'].forEach(t => { document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none'; document.getElementById('tab-' + t)?.classList.toggle('active', t === tab); }); - // 房间 Tab:拉取在线人数列表 + // 房间 Tab:立即拉取 + 每 30 秒自动刷新在线人数 if (tab === 'rooms') { loadRoomsOnlineStatus(); + clearInterval(_roomsRefreshTimer); + _roomsRefreshTimer = setInterval(loadRoomsOnlineStatus, 30000); + } else { + clearInterval(_roomsRefreshTimer); + _roomsRefreshTimer = null; } }