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(