修复聊天室离开播报:显式点击离开按钮时绕过队列防抖,同步发送离开广播,解决本地无队列运行时播报丢失的问题

This commit is contained in:
2026-04-02 16:21:35 +08:00
parent fa5e37f003
commit a562ecca72
4 changed files with 44 additions and 17 deletions

View File

@@ -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();

View File

@@ -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']);
}

View File

@@ -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);

View File

@@ -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(