*/ public const ODDS_PUSH_TOPICS = [ 'user.streak', 'wallet.changed', 'bet.accepted', ]; /** * @return array{user_id: int, current_streak: int, streak_level: int, odds_factor: int, is_jackpot: bool} */ public static function userStreakData(int $userId, ?int $currentStreak = null): array { if ($userId <= 0) { return [ 'user_id' => 0, 'current_streak' => 0, 'streak_level' => 1, 'odds_factor' => 1, 'is_jackpot' => false, ]; } if ($currentStreak === null) { $row = GameHotDataRedis::userRow($userId); $raw = $row['current_streak'] ?? 0; $parsed = filter_var($raw, FILTER_VALIDATE_INT); $currentStreak = $parsed === false ? 0 : $parsed; } $odds = StreakWinReward::playerBetOddsForCurrentStreak($currentStreak); return [ 'user_id' => $userId, 'current_streak' => $odds['current_streak'], 'streak_level' => $odds['streak_level'], 'odds_factor' => $odds['odds_factor'], 'is_jackpot' => $odds['is_jackpot'], ]; } /** * @param array $payload * @return array */ public static function mergeUserStreakInto(array $payload, int $userId, ?int $currentStreak = null): array { if ($userId <= 0) { return $payload; } return array_merge($payload, self::userStreakData($userId, $currentStreak)); } /** * @param array $extra */ public static function publishUserStreak(int $userId, ?int $currentStreak = null, array $extra = []): void { if ($userId <= 0) { return; } $payload = self::userStreakData($userId, $currentStreak); if ($extra !== []) { $payload = array_merge($payload, $extra); } GameWebSocketEventBus::publish(self::TOPIC_USER_STREAK, $payload); } /** * 后台 WebSocket 联调:从库内选取样例玩家(优先 current_streak 最高)。 * * @return array|null */ public static function pickAdminTestUserRow(): ?array { $fields = ['id', 'username', 'uuid', 'phone', 'current_streak', 'coin']; $row = Db::name('user') ->where('status', 1) ->order('current_streak', 'desc') ->order('id', 'asc') ->field($fields) ->find(); if (is_array($row) && !empty($row['id'])) { return $row; } $fallback = Db::name('user') ->order('id', 'asc') ->field($fields) ->find(); return is_array($fallback) && !empty($fallback['id']) ? $fallback : null; } /** * wsConfig 与订阅后演示推送共用的玩家赔率快照。 * * @return array */ public static function adminTestPlayerOddsSnapshot(): array { $row = self::pickAdminTestUserRow(); if ($row === null) { $demoStreak = 3; $odds = StreakWinReward::playerBetOddsForCurrentStreak($demoStreak); return array_merge([ 'is_test' => true, 'preview' => true, 'user_id' => 0, 'username' => '演示玩家(库内无用户)', 'uuid' => '', 'phone' => '', 'coin' => '0.00', 'source' => 'synthetic', ], $odds); } $userIdRaw = $row['id'] ?? 0; $userIdParsed = filter_var($userIdRaw, FILTER_VALIDATE_INT); $userId = $userIdParsed === false ? 0 : $userIdParsed; $streakRaw = $row['current_streak'] ?? 0; $streakParsed = filter_var($streakRaw, FILTER_VALIDATE_INT); $currentStreak = $streakParsed === false ? 0 : $streakParsed; $odds = StreakWinReward::playerBetOddsForCurrentStreak($currentStreak); return array_merge([ 'is_test' => true, 'preview' => true, 'user_id' => $userId, 'username' => (string) ($row['username'] ?? ''), 'uuid' => (string) ($row['uuid'] ?? ''), 'phone' => (string) ($row['phone'] ?? ''), 'coin' => (string) ($row['coin'] ?? '0.00'), 'source' => 'db_user', ], $odds); } /** * 后台测试连接订阅赔率主题后,推送演示帧(非真实业务事件)。 * * @param list $subscribedTopics * @return list}> */ public static function adminTestPushFrames(array $subscribedTopics): array { $snapshot = self::adminTestPlayerOddsSnapshot(); $userIdRaw = $snapshot['user_id'] ?? 0; $userIdParsed = filter_var($userIdRaw, FILTER_VALIDATE_INT); $userId = $userIdParsed === false ? 0 : $userIdParsed; $streakRaw = $snapshot['current_streak'] ?? 0; $streakParsed = filter_var($streakRaw, FILTER_VALIDATE_INT); $currentStreak = $streakParsed === false ? 0 : $streakParsed; $topicSet = []; foreach ($subscribedTopics as $topic) { if (!is_string($topic)) { continue; } $value = trim($topic); if ($value !== '') { $topicSet[$value] = true; } } $frames = []; if (isset($topicSet[self::TOPIC_USER_STREAK])) { $frames[] = [ 'topic' => self::TOPIC_USER_STREAK, 'event' => self::TOPIC_USER_STREAK, 'data' => $snapshot, ]; } if (isset($topicSet['wallet.changed'])) { $frames[] = [ 'topic' => 'wallet.changed', 'event' => 'wallet.changed', 'data' => self::mergeOddsFieldsFromSnapshot([ 'is_test' => true, 'preview' => true, 'user_id' => $userId, 'balance_after' => (string) ($snapshot['coin'] ?? '0.00'), 'biz_type' => 'admin_test_preview', 'changed_at' => time(), ], $snapshot), ]; } if (isset($topicSet['bet.accepted'])) { $frames[] = [ 'topic' => 'bet.accepted', 'event' => 'bet.accepted', 'data' => self::mergeOddsFieldsFromSnapshot([ 'is_test' => true, 'preview' => true, 'user_id' => $userId, 'period_no' => 'ADMIN-TEST-PREVIEW', 'numbers' => [1, 2, 3], 'bet_id' => 1, 'single_bet_amount' => '1.00', 'numbers_count' => 3, 'total_amount' => '3.00', 'balance_after' => (string) ($snapshot['coin'] ?? '0.00'), 'accepted_at' => time(), ], $snapshot), ]; } return $frames; } /** * @param array $payload * @param array $snapshot * @return array */ private static function mergeOddsFieldsFromSnapshot(array $payload, array $snapshot): array { return array_merge($payload, [ 'current_streak' => $snapshot['current_streak'] ?? 0, 'streak_level' => $snapshot['streak_level'] ?? 1, 'odds_factor' => $snapshot['odds_factor'] ?? 1, 'is_jackpot' => ($snapshot['is_jackpot'] ?? false) === true, ]); } }