diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index 2ec825d..32c8747 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -91,17 +91,27 @@ class AutoSaveExp extends Command foreach ($roomMap as $roomId => $usernames) { foreach ($usernames as $username) { - // 如果本轮已经处理过该用户,跳过发放经验(防止多房间/多开重复吃经验) + // 1. 如果本轮已经处理过该用户,跳过发放经验(防止多房间/多开重复吃经验) if (isset($processedUsernames[$username])) { continue; } + // 2. 利用 Redis 缓存锁,防止在短时间内(200秒内)重复进行自动存点奖励(防御多进程并发/重复调度) + $cooldownKey = "user:auto_save_cooldown:{$username}"; + if (Redis::exists($cooldownKey)) { + continue; + } + $user = $usersByUsername->get($username); if (! $user) { continue; } $this->processUser($user, $roomId, $expGainRaw, $jjbGainRaw, $superLevel); + + // 3. 设置冷却标记,200秒后过期(本任务5分钟运行一次,200秒冷却期既防重复,又不影响下一次存点) + Redis::setex($cooldownKey, 200, 1); + $processedUsernames[$username] = true; $totalProcessed++; } diff --git a/tests/Feature/AutoSaveExpCommandTest.php b/tests/Feature/AutoSaveExpCommandTest.php index 4bdeec5..b3792b4 100644 --- a/tests/Feature/AutoSaveExpCommandTest.php +++ b/tests/Feature/AutoSaveExpCommandTest.php @@ -155,4 +155,36 @@ class AutoSaveExpCommandTest extends TestCase $user->refresh(); $this->assertEquals(15, $user->exp_num); } + + /** + * 测试自动存点命令会通过 Redis 冷却锁,防止在短时间内并发重复调度执行。 + */ + public function test_auto_save_cooldown_prevents_overlapping_process(): void + { + Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '5']); + Cache::flush(); + + $room = Room::create(['room_name' => 'cd']); + $user = User::factory()->create([ + 'username' => 'cooldown-user', + 'exp_num' => 10, + ]); + + 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(); + + // 验证第一次加了经验 (10 + 5 = 15) + $user->refresh(); + $this->assertEquals(15, $user->exp_num); + + // 紧接着在冷却时间内(200秒内)模拟第二次并发执行 + $this->artisan('chatroom:auto-save-exp')->assertSuccessful(); + + // 验证因为有 Redis 冷却锁,第二次执行被拦截,经验仍为 15,并未再次增加 + $user->refresh(); + $this->assertEquals(15, $user->exp_num); + } }