event->fresh(); // 防止重复触发 if (! $event || $event->status !== 'pending') { return; } $now = now(); $expiresAt = $now->copy()->addMinutes($event->expire_minutes); // 先标记为 active,防止并发重复触发 $updated = HolidayEvent::query() ->where('id', $event->id) ->where('status', 'pending') ->update([ 'status' => 'active', 'triggered_at' => $now, 'expires_at' => $expiresAt, ]); if (! $updated) { return; // 已被其他进程触发 } $event->refresh(); // 获取在线用户(满足 target_type 条件) $onlineIds = $this->getEligibleOnlineUsers($event, $chatState); if (empty($onlineIds)) { // 无合格在线用户,直接标记完成 $event->update(['status' => 'completed']); return; } // 按 max_claimants 限制人数 if ($event->max_claimants > 0 && count($onlineIds) > $event->max_claimants) { shuffle($onlineIds); $onlineIds = array_slice($onlineIds, 0, $event->max_claimants); } // 计算每人金额 $amounts = $this->distributeAmounts($event, count($onlineIds)); DB::transaction(function () use ($event, $onlineIds, $amounts, $now) { $claims = []; foreach ($onlineIds as $i => $userId) { $claims[] = [ 'event_id' => $event->id, 'user_id' => $userId, 'amount' => $amounts[$i] ?? 0, 'claimed_at' => $now, ]; } // 批量插入领取记录 HolidayClaim::insert($claims); }); // 广播全房间 WebSocket 事件 broadcast(new HolidayEventStarted($event->refresh())); // 向聊天室追加系统消息(写入 Redis + 落库) $this->pushSystemMessage($event, count($onlineIds), $chatState); // 处理重复活动(计算下次触发时间) $this->scheduleNextRepeat($event); } /** * 获取满足条件的在线用户 ID 列表。 * * @return array */ private function getEligibleOnlineUsers(HolidayEvent $event, ChatStateService $chatState): array { try { $key = 'room:1:users'; $users = Redis::hgetall($key); if (empty($users)) { return []; } $usernames = array_keys($users); // 根据 user_id 从 Redis value 或数据库查出 ID $ids = []; $fallbacks = []; foreach ($users as $username => $jsonInfo) { $info = json_decode($jsonInfo, true); if (isset($info['user_id'])) { $ids[] = (int) $info['user_id']; } else { $fallbacks[] = $username; } } if (! empty($fallbacks)) { $dbIds = User::whereIn('username', $fallbacks)->pluck('id')->map(fn ($id) => (int) $id)->all(); $ids = array_merge($ids, $dbIds); } $ids = array_values(array_unique($ids)); // 根据 target_type 过滤 return match ($event->target_type) { 'vip' => User::whereIn('id', $ids)->whereNotNull('vip_level_id')->pluck('id')->map(fn ($id) => (int) $id)->all(), 'level' => User::whereIn('id', $ids)->where('user_level', '>=', (int) ($event->target_value ?? 1))->pluck('id')->map(fn ($id) => (int) $id)->all(), default => $ids, }; } catch (\Throwable) { return []; } } /** * 按分配方式计算每人金额数组。 * * @return array */ private function distributeAmounts(HolidayEvent $event, int $count): array { if ($count <= 0) { return []; } if ($event->distribute_type === 'fixed') { // 定额模式:每人相同金额 $amount = $event->fixed_amount ?? (int) floor($event->total_amount / $count); return array_fill(0, $count, $amount); } // 随机模式:二倍均值算法 $total = $event->total_amount; $min = max(1, $event->min_amount ?? 1); $max = $event->max_amount ?? (int) ceil($total * 2 / $count); $amounts = []; $remaining = $total; for ($i = 0; $i < $count - 1; $i++) { $remainingPeople = $count - $i; $avgDouble = (int) floor($remaining * 2 / $remainingPeople); $cap = max($min, min($max, $avgDouble - 1, $remaining - ($remainingPeople - 1) * $min)); $amounts[] = random_int($min, max($min, $cap)); $remaining -= end($amounts); } $amounts[] = max($min, $remaining); shuffle($amounts); return $amounts; } /** * 向聊天室推送系统公告消息并写入 Redis + 落库。 */ private function pushSystemMessage(HolidayEvent $event, int $claimCount, ChatStateService $chatState): void { $typeLabel = $event->distribute_type === 'fixed' ? "每人固定 {$event->fixed_amount} 金币" : '随机分配'; $content = "🎊 【{$event->name}】节日福利开始啦!总奖池 🪙".number_format($event->total_amount) ." 金币,{$typeLabel},共 {$claimCount} 名在线用户可领取!点击弹窗按钮立即领取!"; $msg = [ 'id' => $chatState->nextMessageId(1), 'room_id' => 1, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, 'is_secret' => false, 'font_color' => '#f59e0b', 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage(1, $msg); broadcast(new MessageSent(1, $msg)); SaveMessageJob::dispatch($msg); } /** * 处理重复活动:计算下次触发时间并重置状态。 */ private function scheduleNextRepeat(HolidayEvent $event): void { $nextSendAt = match ($event->repeat_type) { 'daily' => $event->send_at->copy()->addDay(), 'weekly' => $event->send_at->copy()->addWeek(), 'monthly' => $event->send_at->copy()->addMonth(), default => null, // 'once' 或 'cron' 不自动重复 }; if ($nextSendAt) { $event->update([ 'status' => 'pending', 'send_at' => $nextSendAt, 'triggered_at' => null, 'expires_at' => null, 'claimed_count' => 0, 'claimed_amount' => 0, ]); } else { $event->update(['status' => 'completed']); } } }