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::hkeys() 查询在线用户。 * 这样可以避免 Redis SCAN + 前缀匹配不一致的问题, * 且 Redis::hkeys() 会自动正确地加上前缀,与 ChatStateService::userJoin() 一致。 * * @return array> 格式:[房间ID => [用户名, ...]] */ private function scanOnlineRooms(): array { $roomMap = []; // 从数据库取出所有房间 ID $roomIds = \App\Models\Room::pluck('id'); foreach ($roomIds as $roomId) { // Laravel 的 Redis facade 会自动加配置的前缀,与 ChatStateService 存入时完全一致 $usernames = Redis::hkeys("room:{$roomId}:users"); if (! empty($usernames)) { $roomMap[(int) $roomId] = $usernames; } } 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); $jjbGain = $this->parseRewardValue($jjbGainRaw); $actualJjbGain = 0; if ($jjbGain > 0) { $jjbMultiplier = $this->vipService->getJjbMultiplier($user); $actualJjbGain = (int) round($jjbGain * $jjbMultiplier); } // 2. 通过统一积分服务发放奖励(原子写入 + 流水记录) if ($actualExpGain > 0) { $this->currencyService->change( $user, 'exp', $actualExpGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId, ); } if ($actualJjbGain > 0) { $this->currencyService->change( $user, 'gold', $actualJjbGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId, ); } $user->refresh(); // 刷新获取最新属性(service 已原子更新) // 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}"; } $jjbDisplay = $user->jjb ?? 0; $gainStr = ! empty($gainParts) ? ' 本次获得:' . implode(',', $gainParts) : ''; // 格式:⏰ 自动存点 · LV.100 · 经验 10,468 · 金币 8,345 · 已满级 · 本次获得:经验+1,金币+3 $statusTag = $user->user_level >= $superLevel ? ' · 已满级 ✓' : ''; $content = "⏰ 自动存点 · LV.{$user->user_level} · 经验 {$user->exp_num} · 金币 {$jjbDisplay}{$statusTag}{$gainStr}"; $noticeMsg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统', 'to_user' => $username, // 定向推送给本人 'content' => $content, '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; } }