Files
webman-buildadmin/app/common/service/GameWebSocketPayloadHelper.php

303 lines
9.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\service;
use app\common\library\game\StreakWinReward;
use support\think\Db;
/**
* 移动端 WebSocket仅推送当前玩家本局适用赔率非 streak_win_reward 全表)。
*/
final class GameWebSocketPayloadHelper
{
public const TOPIC_USER_STREAK = 'user.streak';
/** @var list<string> */
public const ODDS_PUSH_TOPICS = [
'user.streak',
'wallet.changed',
'bet.accepted',
];
/**
* 下发给客户端前从 data 中移除的字段(服务端入队/路由仍保留完整载荷)。
*
* @var list<string>
*/
public const OUTBOUND_STRIP_KEYS = [
'user_id',
'uuid',
'phone',
'balance_before',
'channel_id',
'review_admin_id',
'operator_admin_id',
'idempotency_key',
];
/**
* @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'],
];
}
/**
* 出站 WebSocket 帧 data 脱敏:移除 user_id 等(连接已绑定用户,无需在载荷中重复暴露)。
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public static function sanitizeOutboundData(array $data): array
{
return self::stripSensitiveKeysRecursive($data, 0);
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private static function stripSensitiveKeysRecursive(array $data, int $depth): array
{
if ($depth > 8) {
return $data;
}
$out = [];
foreach ($data as $key => $value) {
if (!is_string($key)) {
continue;
}
if (in_array($key, self::OUTBOUND_STRIP_KEYS, true)) {
continue;
}
if (is_array($value)) {
$isList = array_is_list($value);
$child = [];
foreach ($value as $k => $item) {
if (is_array($item)) {
$child[$k] = self::stripSensitiveKeysRecursive($item, $depth + 1);
} else {
$child[$k] = $item;
}
}
$out[$key] = $isList ? array_values($child) : $child;
continue;
}
$out[$key] = $value;
}
return $out;
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
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<string, mixed> $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<string, mixed>|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<string, mixed>
*/
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<string> $subscribedTopics
* @return list<array{topic: string, event: string, data: array<string, mixed>}>
*/
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<string, mixed> $payload
* @param array<string, mixed> $snapshot
* @return array<string, mixed>
*/
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,
]);
}
}