From 2044feec1230c5bd5445ed7bc841562485f524f6 Mon Sep 17 00:00:00 2001 From: lkddi Date: Fri, 27 Feb 2026 12:39:23 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E6=96=B0=E5=A2=9E=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=AD=98=E7=82=B9=E6=8C=87=E4=BB=A4=EF=BC=8C?= =?UTF-8?q?=E6=AF=8F5=E5=88=86=E9=92=9F=E4=B8=BA=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8F=91=E6=94=BE=E7=BB=8F=E9=AA=8C=E9=87=91?= =?UTF-8?q?=E5=B8=81=E5=B9=B6=E6=8E=A8=E9=80=81=E7=B3=BB=E7=BB=9F=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/AutoSaveExp.php | 235 +++++++++++++++++++++++++++ routes/console.php | 3 + 2 files changed, 238 insertions(+) create mode 100644 app/Console/Commands/AutoSaveExp.php diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php new file mode 100644 index 0000000..0b2848d --- /dev/null +++ b/app/Console/Commands/AutoSaveExp.php @@ -0,0 +1,235 @@ +scanOnlineRooms(); + + if (empty($roomMap)) { + $this->info('当前没有在线用户,跳过存点。'); + return Command::SUCCESS; + } + + // 统计本次处理总人次(一个用户在多个房间会被计算多次) + $totalProcessed = 0; + + foreach ($roomMap as $roomId => $usernames) { + foreach ($usernames as $username) { + $this->processUser($username, $roomId, $expGainRaw, $jjbGainRaw, $superLevel); + $totalProcessed++; + } + } + + $this->info("自动存点完成,共处理 {$totalProcessed} 个在线用户。"); + + return Command::SUCCESS; + } + + /** + * 扫描 Redis,获取所有活跃房间及其在线用户列表。 + * + * @return array> 格式:[房间ID => [用户名, ...]] + */ + private function scanOnlineRooms(): array + { + $roomMap = []; + $cursor = '0'; + + do { + [$cursor, $keys] = Redis::scan($cursor, ['match' => 'room:*:users', 'count' => 100]); + foreach ($keys ?? [] as $key) { + // 从 "room:123:users" 提取房间 ID + if (preg_match('/room:(\d+):users/', $key, $m)) { + $roomId = (int) $m[1]; + $usernames = Redis::hkeys($key); + if (! empty($usernames)) { + $roomMap[$roomId] = $usernames; + } + } + } + } while ($cursor !== '0'); + + return $roomMap; + } + + /** + * 为单个在线用户执行存点逻辑,并在其所在房间推送存点通知。 + * + * @param string $username 用户名 + * @param int $roomId 所在房间ID + * @param string $expGainRaw 经验奖励原始配置(支持 "1" 或 "1-10" 范围) + * @param string $jjbGainRaw 金币奖励原始配置 + * @param int $superLevel 管理员等级阈值 + */ + private function processUser( + string $username, + int $roomId, + string $expGainRaw, + string $jjbGainRaw, + int $superLevel + ): void { + $user = User::where('username', $username)->first(); + if (! $user) { + return; + } + + // 1. 发放经验奖励(支持 VIP 倍率) + $expGain = $this->parseRewardValue($expGainRaw); + $expMultiplier = $this->vipService->getExpMultiplier($user); + $actualExpGain = (int) round($expGain * $expMultiplier); + $user->exp_num += $actualExpGain; + + // 2. 发放金币奖励(支持 VIP 倍率) + $jjbGain = $this->parseRewardValue($jjbGainRaw); + $actualJjbGain = 0; + if ($jjbGain > 0) { + $jjbMultiplier = $this->vipService->getJjbMultiplier($user); + $actualJjbGain = (int) round($jjbGain * $jjbMultiplier); + $user->jjb = ($user->jjb ?? 0) + $actualJjbGain; + } + + // 3. 自动升降级(管理员不参与) + $oldLevel = $user->user_level; + $leveledUp = false; + + if ($oldLevel < $superLevel) { + $newLevel = Sysparam::calculateLevel($user->exp_num); + if ($newLevel !== $oldLevel && $newLevel < $superLevel) { + $user->user_level = $newLevel; + $leveledUp = ($newLevel > $oldLevel); + } + } + + $user->save(); + + // 4. 若升级,向全频道广播升级消息 + if ($leveledUp) { + $sysMsg = [ + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => "🌟 天道酬勤!恭喜侠客【{$user->username}】挂机苦修,境界突破至 LV.{$user->user_level}!", + 'is_secret' => false, + 'font_color' => '#d97706', + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + $this->chatState->pushMessage($roomId, $sysMsg); + broadcast(new MessageSent($roomId, $sysMsg)); + SaveMessageJob::dispatch($sysMsg); + } + + // 5. 向用户私人推送"系统为你自动存点"信息,在其聊天框显示 + $gainParts = []; + if ($actualExpGain > 0) { + $gainParts[] = "经验+{$actualExpGain}"; + } + if ($actualJjbGain > 0) { + $gainParts[] = "金币+{$actualJjbGain}"; + } + $gainStr = ! empty($gainParts) ? '(' . implode(',', $gainParts) . ')' : ''; + $jjbDisplay = $user->jjb ?? 0; + + $levelInfo = $user->user_level >= $superLevel + ? "级别({$user->user_level});经验({$user->exp_num});金币({$jjbDisplay}枚);已满级。" + : "级别({$user->user_level});经验({$user->exp_num});金币({$jjbDisplay}枚)。"; + + $noticeMsg = [ + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => '系统', + 'to_user' => $username, // 定向推送给本人 + 'content' => "【系统为你自动存点】{$levelInfo} {$gainStr}", + 'is_secret' => true, // 私信模式,只有本人能看到 + 'font_color' => '#16a34a', // 草绿色 + 'action' => '', + 'sent_at' => now()->toDateTimeString(), + ]; + + $this->chatState->pushMessage($roomId, $noticeMsg); + broadcast(new MessageSent($roomId, $noticeMsg)); + } + + /** + * 解析奖励值配置字符串,支持固定值和随机范围两种格式。 + * + * 格式说明: + * - 固定值:直接写数字,如 "5" → 返回 5 + * - 随机范围:如 "3-10" → 返回 [3, 10] 之间的随机整数 + * + * @param string $raw 原始配置字符串 + * @return int + */ + private function parseRewardValue(string $raw): int + { + $raw = trim($raw); + if (str_contains($raw, '-')) { + [$min, $max] = explode('-', $raw, 2); + return rand((int) $min, (int) $max); + } + + return (int) $raw; + } +} diff --git a/routes/console.php b/routes/console.php index 16042f6..e55f0b2 100644 --- a/routes/console.php +++ b/routes/console.php @@ -10,3 +10,6 @@ Artisan::command('inspire', function () { // 每天凌晨 3 点清理超过 30 天的聊天记录 Schedule::command('messages:purge')->dailyAt('03:00'); + +// 每 5 分钟为所有在线用户自动存点(经验/金币/等级) +Schedule::command('chatroom:auto-save-exp')->everyFiveMinutes();