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'; // 获取原始 Redis 客户端(不自动加前缀),用于直接操作完整 key 名 $rawClient = Redis::connection()->client(); do { [$cursor, $keys] = Redis::scan($cursor, ['match' => '*room:*:users', 'count' => 100]); foreach ($keys ?? [] as $key) { // 从 key 中提取房间 ID,支持带前缀的格式(如 chatroom-database-room:1:users) if (preg_match('/room:(\d+):users/', $key, $m)) { $roomId = (int) $m[1]; // 直接用原始客户端查找,避免 Laravel 自动加前缀导致 key 被拼接两次 $usernames = $rawClient->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; } }