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 已原子更新) $user->load('activePosition.position'); // 确保职务及职位关联已加载 // 3. 自动升降级逻辑 // - 有在职职务的用户:等级固定为职务对应等级,不随经验变化 // - 管理员(>= superLevel):不变动 // - 普通用户:按经验计算等级,支持升降级 $oldLevel = $user->user_level; $leveledUp = false; $activeUP = $user->activePosition; // 已在 refresh 后加载 if ($activeUP?->position) { // 有在职职务:等级锁定为职务设定值,确保不被经验系统覆盖 $requiredLevel = (int) $activeUP->position->level; if ($requiredLevel > 0 && $user->user_level !== $requiredLevel) { $user->user_level = $requiredLevel; } } elseif ($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); // 触发微信机器人私聊通知 (等级提升) try { $wechatService = app(\App\Services\WechatBot\WechatNotificationService::class); $wechatService->notifyLevelChange($user, $oldLevel, $newLevel); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('WechatBot level change notification failed', ['error' => $e->getMessage()]); } } // 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)); // 6. 同步更新在职用户的勤务时长 $this->tickDutyLog($user, $roomId); } /** * 解析奖励值配置字符串,支持固定值和随机范围两种格式。 * * 格式说明: * - 固定值:直接写数字,如 "5" → 返回 5 * - 随机范围:如 "3-10" → 返回 [3, 10] 之间的随机整数 * * @param string $raw 原始配置字符串 */ 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; } /** * 自动存点时同步更新或创建在职用户的勤务日志。 * * 逻辑同 ChatController::tickDutyLog: * 1. 无在职职务 → 跳过 * 2. 今日已有开放日志 → 刷新 duration_seconds * 3. 今日无日志 → 新建,login_at 取 user->in_time(进房时间) * * @param \App\Models\User $user 已 refresh 的用户实例 * @param int $roomId 所在房间 ID */ private function tickDutyLog(User $user, int $roomId): void { // 无论有无职务,均记录在线流水 $activeUP = $user->activePosition; // ① 今日未关闭的开放日志 → 刷新时长 $openLog = PositionDutyLog::query() ->where('user_id', $user->id) ->whereNull('logout_at') ->whereDate('login_at', today()) ->first(); if ($openLog) { DB::table('position_duty_logs') ->where('id', $openLog->id) ->update([ 'duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))'), 'updated_at' => now(), ]); return; } // ② 今日无开放日志 → 新建 // 若今日已有已关闭的日志(是重建场景),必须用 now(),防止重用旧 in_time 累积膨胀 $hasClosedToday = PositionDutyLog::query() ->where('user_id', $user->id) ->whereDate('login_at', today()) ->whereNotNull('logout_at') ->exists(); $loginAt = (! $hasClosedToday && $user->in_time && $user->in_time->isToday()) ? $user->in_time : now(); PositionDutyLog::create([ 'user_id' => $user->id, 'user_position_id' => $activeUP?->id, 'login_at' => $loginAt, 'ip_address' => '0.0.0.0', 'room_id' => $roomId, ]); } }