From a20bcf13beb60469b9868c9a426612da3663469f Mon Sep 17 00:00:00 2001 From: pllx Date: Mon, 6 Jul 2026 22:39:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=8A=A8=E5=AD=98?= =?UTF-8?q?=E7=82=B9=E6=8C=82=E6=9C=BA=E7=BB=8F=E9=AA=8C=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=B4=AF=E5=8A=A0=E5=92=8C=E7=A6=BB=E7=BA=BF=E5=83=B5=E5=B0=B8?= =?UTF-8?q?=E5=90=83=E7=BB=8F=E9=AA=8C=E7=9A=84=E6=BC=8F=E6=B4=9E=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AF=B9=E5=BA=94=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/AutoSaveExp.php | 22 ++++++++- tests/Feature/AutoSaveExpCommandTest.php | 57 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index 9b40202..2ec825d 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -87,14 +87,22 @@ class AutoSaveExp extends Command $totalProcessed = 0; $usersByUsername = $this->preloadOnlineUsers($roomMap); + $processedUsernames = []; + foreach ($roomMap as $roomId => $usernames) { foreach ($usernames as $username) { + // 如果本轮已经处理过该用户,跳过发放经验(防止多房间/多开重复吃经验) + if (isset($processedUsernames[$username])) { + continue; + } + $user = $usersByUsername->get($username); if (! $user) { continue; } $this->processUser($user, $roomId, $expGainRaw, $jjbGainRaw, $superLevel); + $processedUsernames[$username] = true; $totalProcessed++; } } @@ -125,7 +133,19 @@ class AutoSaveExp extends Command // Laravel 的 Redis facade 会自动加配置的前缀,与 ChatStateService 存入时完全一致 $usernames = Redis::hkeys("room:{$roomId}:users"); if (! empty($usernames)) { - $roomMap[(int) $roomId] = $usernames; + $activeUsernames = []; + foreach ($usernames as $username) { + // 检查活跃标记是否存在(90秒内) + if (Redis::exists("room:{$roomId}:alive:{$username}")) { + $activeUsernames[] = $username; + } else { + // 懒清理僵尸数据 + Redis::hdel("room:{$roomId}:users", $username); + } + } + if (! empty($activeUsernames)) { + $roomMap[(int) $roomId] = $activeUsernames; + } } } diff --git a/tests/Feature/AutoSaveExpCommandTest.php b/tests/Feature/AutoSaveExpCommandTest.php index 5936044..4bdeec5 100644 --- a/tests/Feature/AutoSaveExpCommandTest.php +++ b/tests/Feature/AutoSaveExpCommandTest.php @@ -87,6 +87,7 @@ class AutoSaveExpCommandTest extends TestCase ]); Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE)); + Redis::setex("room:{$room->id}:alive:{$user->username}", 90, 1); $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); @@ -98,4 +99,60 @@ class AutoSaveExpCommandTest extends TestCase $this->assertStringContainsString('部门 办公厅 · 职务 🏛️ 厅长 · 会员 👑 至尊会员', $notice['content']); $this->assertStringContainsString('LV.100 · 经验 168762 · 金币 1100017 · 已满级 ✓', $notice['content']); } + + /** + * 测试自动存点命令会清理掉线不活跃的用户(懒清理)并不发放奖励。 + */ + public function test_auto_save_cleans_up_inactive_zombie_users(): void + { + Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '5']); + Cache::flush(); + + $room = Room::create(['room_name' => 'zmb']); + $user = User::factory()->create([ + 'username' => 'zombie-user', + 'exp_num' => 10, + ]); + + // 只把用户加入哈希表,但不设置 alive 活跃键 + Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE)); + + $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); + + // 验证用户已经被从房间在线哈希表中懒清理剔除 + $this->assertFalse((bool) Redis::hexists("room:{$room->id}:users", $user->username)); + + // 验证用户的经验没有增加(仍然是 10) + $user->refresh(); + $this->assertEquals(10, $user->exp_num); + } + + /** + * 测试自动存点命令会全局去重,防止玩家在多房间同时挂机时重复吃挂机奖励。 + */ + public function test_auto_save_deduplicates_multiple_rooms_rewards(): void + { + Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '5']); + Cache::flush(); + + $room1 = Room::create(['room_name' => 'r1']); + $room2 = Room::create(['room_name' => 'r2']); + $user = User::factory()->create([ + 'username' => 'multi-room-user', + 'exp_num' => 10, + ]); + + // 玩家同时在两个房间在线,且两边都有心跳活跃标记 + Redis::hset("room:{$room1->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE)); + Redis::setex("room:{$room1->id}:alive:{$user->username}", 90, 1); + + Redis::hset("room:{$room2->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE)); + Redis::setex("room:{$room2->id}:alive:{$user->username}", 90, 1); + + $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); + + // 验证用户只被加了一次经验(10 + 5 = 15,而不是 10 + 5 + 5 = 20) + $user->refresh(); + $this->assertEquals(15, $user->exp_num); + } }