From a562ecca7241e4594599531cfa1cb5dc2d1effb7 Mon Sep 17 00:00:00 2001 From: lkddi Date: Thu, 2 Apr 2026 16:21:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=81=8A=E5=A4=A9=E5=AE=A4?= =?UTF-8?q?=E7=A6=BB=E5=BC=80=E6=92=AD=E6=8A=A5=EF=BC=9A=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E7=A6=BB=E5=BC=80=E6=8C=89=E9=92=AE=E6=97=B6?= =?UTF-8?q?=E7=BB=95=E8=BF=87=E9=98=9F=E5=88=97=E9=98=B2=E6=8A=96=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=8F=91=E9=80=81=E7=A6=BB=E5=BC=80=E5=B9=BF?= =?UTF-8?q?=E6=92=AD=EF=BC=8C=E8=A7=A3=E5=86=B3=E6=9C=AC=E5=9C=B0=E6=97=A0?= =?UTF-8?q?=E9=98=9F=E5=88=97=E8=BF=90=E8=A1=8C=E6=97=B6=E6=92=AD=E6=8A=A5?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/AuthController.php | 11 +++++ app/Http/Controllers/ChatController.php | 42 +++++++++++++------ resources/views/chat/frame.blade.php | 6 +-- .../views/chat/partials/scripts.blade.php | 2 +- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index ae0d923..df48b28 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -184,6 +184,17 @@ class AuthController extends Controller 'out_time' => now(), 'out_info' => '正常退出了聊天室', ]); + + // [NEW] 同步清除该用户在所有房间的在线状态和心跳,确保其如果马上重登,能触发全新入场欢迎 + try { + $chatState = app(\App\Services\ChatStateService::class); + $roomIds = $chatState->getUserRooms($user->username); + foreach ($roomIds as $roomId) { + $chatState->userLeave($roomId, $user->username); + } + } catch (\Exception $e) { + // 忽略清理缓存时发生的异常 + } } Auth::logout(); diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index a470263..7ecbbb7 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -69,7 +69,17 @@ class ChatController extends Controller $user->update(['in_time' => now()]); // 0. 判断是否已经是当前房间的在线状态 - $isAlreadyInRoom = $this->chatState->isUserInRoom($id, $user->username); + $hasKey = $this->chatState->isUserInRoom($id, $user->username); + // 增强校验:判断心跳是否还存在。如果遇到没有启动队列任务的情况,离线任务未能清理脏数据,心跳必定过期。 + $isHeartbeatAlive = (bool) \Illuminate\Support\Facades\Redis::exists("room:{$id}:alive:{$user->username}"); + + // 如果虽然在名单里,但心跳早已丢失(可能直接关浏览器且队列未跑),视为全新进房 + if ($hasKey && ! $isHeartbeatAlive) { + $this->chatState->userLeave($id, $user->username); // 强制洗净状态 + $hasKey = false; + } + + $isAlreadyInRoom = $hasKey; // 1. 先将用户从其他所有房间的在线名单中移除(切换房间时旧记录自动清理) // 避免直接跳转页面时 leave 接口未触发导致"幽灵在线"问题 @@ -180,7 +190,8 @@ class ChatController extends Controller 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($id, $generalWelcomeMsg); - broadcast(new MessageSent($id, $generalWelcomeMsg))->toOthers(); + // 修复:之前使用了 ->toOthers() 导致自己看不到自己的进场提示 + broadcast(new MessageSent($id, $generalWelcomeMsg)); } } @@ -192,13 +203,6 @@ class ChatController extends Controller $toUser = $msg['to_user'] ?? ''; $fromUser = $msg['from_user'] ?? ''; $isSecret = ! empty($msg['is_secret']); - $action = $msg['action'] ?? ''; - $welcomeUser = $msg['welcome_user'] ?? ''; - - // 过滤自己的进出场提示,避免自己被自己刷屏 - if (($action === 'system_welcome' || $action === 'admin_welcome' || empty($action)) && $welcomeUser === $username) { - return false; - } // 公众发言(对大家说):所有人都可以看到 if ($toUser === '大家' || $toUser === '') { @@ -610,11 +614,23 @@ class ChatController extends Controller return response()->json(['status' => 'error'], 401); } - // 不立刻执行离线逻辑,而是给个 3 秒的防抖延迟 - // 这样如果用户只是刷新页面,很快在 init 中又会重新加入房间(记录的 join_time 会大于当前 leave 时的 leaveTime) - // Job 中就不会执行完整的离线播报和注销流程 $leaveTime = microtime(true); - \App\Jobs\ProcessUserLeave::dispatch($id, clone $user, $leaveTime)->delay(now()->addSeconds(3)); + $isExplicit = strval($request->query('explicit')) === '1'; + + if ($isExplicit) { + // 人工显式点击“离开”,不再进行浏览器刷新的防抖,直接同步执行清算和播报。 + // 这对本地没有开启 Queue Worker 的环境尤为重要,能保证大家立刻看到消息。 + // 为了防止 ProcessUserLeave 中的时间对比失败,我们直接删掉 join_time 表示彻底离线。 + \Illuminate\Support\Facades\Redis::del("room:{$id}:join_time:{$user->username}"); + + $job = new \App\Jobs\ProcessUserLeave($id, clone $user, $leaveTime); + dispatch_sync($job); + } else { + // 不立刻执行离线逻辑,而是给个 3 秒的防抖延迟 + // 这样如果用户只是刷新页面,很快在 init 中又会重新加入房间(记录的 join_time 会大于当前 leave 时的 leaveTime) + // Job 中就不会执行完整的离线播报和注销流程 + \App\Jobs\ProcessUserLeave::dispatch($id, clone $user, $leaveTime)->delay(now()->addSeconds(3)); + } return response()->json(['status' => 'success']); } diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 44139d5..aa6fd57 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -235,11 +235,11 @@ * 优先级:如果有新人礼包特效,优先播放新人大礼包;如果没有,再播放周卡特效 */ setTimeout(() => { - if (window.EffectManager) { + if (typeof EffectManager !== 'undefined') { @if (!empty($newbieEffect)) - window.EffectManager.play('{{ $newbieEffect }}'); + EffectManager.play('{{ $newbieEffect }}'); @elseif (!empty($weekEffect)) - window.EffectManager.play('{{ $weekEffect }}'); + EffectManager.play('{{ $weekEffect }}'); @endif } }, 1000); diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 56e3937..78e3c28 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -1413,7 +1413,7 @@ clearTimeout(visibilityTimer); try { - await fetch(window.chatContext.leaveUrl, { + await fetch(window.chatContext.leaveUrl + '?explicit=1', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(