1.优化websocket接口,新增赔率参数

This commit is contained in:
2026-05-15 16:24:19 +08:00
parent 575aa279bd
commit 91229f4477
13 changed files with 434 additions and 40 deletions

View File

@@ -6,6 +6,7 @@ namespace app\admin\controller\test;
use app\common\controller\Backend;
use app\common\library\admin\WebSocketConfigHelper;
use app\common\service\GameWebSocketPayloadHelper;
use support\Response;
use Webman\Http\Request as WebmanRequest;
@@ -25,6 +26,7 @@ class GameCurrentStatus extends Backend
$subscribeTopics = [
'period.tick',
'user.streak',
'period.opened',
'period.locked',
'period.payout',
@@ -33,15 +35,22 @@ class GameCurrentStatus extends Backend
'auto.spin.progress',
];
$oddsPushTopics = GameWebSocketPayloadHelper::ODDS_PUSH_TOPICS;
$testPlayerOdds = GameWebSocketPayloadHelper::adminTestPlayerOddsSnapshot();
return $this->success('', [
'name' => 'ws.period',
'ws_url' => WebSocketConfigHelper::wsUrl($request),
'connect_tip' => 'After connected, topics are auto-subscribed. You can also send subscribe manually.',
'connect_tip' => '连接成功后将自动订阅下列主题。真实业务仅在有玩家下注/结算时推送赔率;本页联调会在订阅后额外推送带 is_test/preview 的演示帧(见下方测试玩家赔率)。',
'subscribe_topics' => $subscribeTopics,
'odds_push_topics' => $oddsPushTopics,
'player_odds_fields' => ['current_streak', 'streak_level', 'odds_factor', 'is_jackpot'],
'test_player_odds' => $testPlayerOdds,
'test_push_topics' => $oddsPushTopics,
'sample_messages' => [
'{"action":"ping"}',
'{"action":"subscribe","topics":["period.tick","period.opened"]}',
'{"action":"subscribe","topics":["bet.accepted","wallet.changed","auto.spin.progress"]}',
'{"action":"subscribe","topics":["period.tick","user.streak","period.opened","period.locked","period.payout"]}',
'{"action":"subscribe","topics":["user.streak","wallet.changed","bet.accepted","auto.spin.progress"]}',
],
]);
}

View File

@@ -14,6 +14,7 @@ use app\common\service\GameHotDataCoordinator;
use app\common\service\GameHotDataRedis;
use app\common\service\GameRecordService;
use app\common\service\GameWebSocketEventBus;
use app\common\service\GameWebSocketPayloadHelper;
use support\think\Db;
use Webman\Http\Request;
use support\Response;
@@ -49,6 +50,9 @@ class Game extends MobileBase
}
$user = $this->auth->getUser();
$currentStreakRaw = $user->current_streak ?? 0;
$currentStreakParsed = filter_var($currentStreakRaw, FILTER_VALIDATE_INT);
$currentStreak = $currentStreakParsed === false ? 0 : $currentStreakParsed;
return $this->mobileSuccess([
'server_time' => $now,
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
@@ -68,11 +72,12 @@ class Game extends MobileBase
BetChips::lobbyChipsPayload()
),
'dictionary' => $items,
'streak_win_reward' => StreakWinReward::lobbyPayload(),
'user_snapshot' => [
'coin' => $user->coin,
'current_streak' => $user->current_streak ?? 0,
],
'user_snapshot' => array_merge(
[
'coin' => $user->coin,
],
StreakWinReward::playerBetOddsForCurrentStreak($currentStreak)
),
]);
}
@@ -280,7 +285,7 @@ class Game extends MobileBase
}
GameHotDataCoordinator::afterUserCommitted($userId);
GameWebSocketEventBus::publish('bet.accepted', [
GameWebSocketEventBus::publish('bet.accepted', GameWebSocketPayloadHelper::mergeUserStreakInto([
'user_id' => $userId,
'period_no' => $period->period_no,
'numbers' => $numbers,
@@ -290,13 +295,13 @@ class Game extends MobileBase
'total_amount' => $totalAmount,
'balance_after' => $after,
'accepted_at' => time(),
]);
GameWebSocketEventBus::publish('wallet.changed', [
], $userId, $streakAtBet));
GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto([
'user_id' => $userId,
'balance_after' => $after,
'biz_type' => 'bet',
'changed_at' => time(),
]);
], $userId, $streakAtBet));
return $this->mobileSuccess([
'order_no' => $orderNo,
'period_no' => $period->period_no,

View File

@@ -184,13 +184,13 @@ final class DepositSettlement
throw new RuntimeException($e->getMessage());
}
GameWebSocketEventBus::publish('wallet.changed', [
GameWebSocketEventBus::publish('wallet.changed', \app\common\service\GameWebSocketPayloadHelper::mergeUserStreakInto([
'user_id' => $userId,
'balance_after' => $balanceAfter,
'biz_type' => 'deposit',
'order_no' => $orderNo,
'changed_at' => $now,
]);
], $userId));
return [
'order_id' => $orderId,

View File

@@ -107,22 +107,23 @@ final class StreakWinReward
}
/**
* lobbyInit 用:连胜赔率档位(与后台「连胜奖励」、派彩公式一致)
* 当前玩家本局适用赔率(非全表):按 current_streak 解析下一注中奖将使用的档位
*
* @return array{rows: list<array{streak: int, odds_factor: int, is_jackpot: bool}>}
* @return array{current_streak: int, streak_level: int, odds_factor: int, is_jackpot: bool}
*/
public static function lobbyPayload(): array
public static function playerBetOddsForCurrentStreak(int $currentStreak): array
{
$rows = [];
foreach (self::loadRows() as $row) {
$rows[] = [
'streak' => (int) ($row['streak'] ?? 0),
'odds_factor' => (int) ($row['odds_factor'] ?? 0),
'is_jackpot' => ($row['is_jackpot'] ?? false) === true,
];
if ($currentStreak < 0) {
$currentStreak = 0;
}
$row = self::rowForStreakAtBet($currentStreak);
return ['rows' => $rows];
return [
'current_streak' => $currentStreak,
'streak_level' => self::levelFromStreakAtBet($currentStreak),
'odds_factor' => (int) ($row['odds_factor'] ?? 1),
'is_jackpot' => ($row['is_jackpot'] ?? false) === true,
];
}
/**

View File

@@ -143,6 +143,7 @@ final class GameBetSettleService
'update_time' => $now,
]);
GameHotDataCoordinator::afterUserCommitted($userId);
GameWebSocketPayloadHelper::publishUserStreak($userId, $next);
}
$jackpotHits = [];
@@ -408,13 +409,13 @@ final class GameBetSettleService
'update_time' => $now,
]);
GameHotDataCoordinator::afterUserCommitted($userId);
GameWebSocketEventBus::publish('wallet.changed', [
GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto([
'user_id' => $userId,
'balance_after' => $after,
'biz_type' => 'payout',
'ref_id' => $betId,
'changed_at' => $now,
]);
], $userId));
return $after;
}

View File

@@ -0,0 +1,232 @@
<?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',
];
/**
* @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<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));
}
public static function publishUserStreak(int $userId, ?int $currentStreak = null): void
{
if ($userId <= 0) {
return;
}
GameWebSocketEventBus::publish(self::TOPIC_USER_STREAK, self::userStreakData($userId, $currentStreak));
}
/**
* 后台 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,
]);
}
}

View File

@@ -6,6 +6,7 @@ namespace app\process;
use app\common\service\GameWebSocketEventBus;
use app\common\service\GameLiveService;
use app\common\service\GameWebSocketPayloadHelper;
use Workerman\Connection\TcpConnection;
use Workerman\Timer;
@@ -169,6 +170,7 @@ class GameWebSocketServer
'event' => 'ws.subscribed',
'topics' => $connection->topics,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
self::pushAdminTestOddsPreview($connection, $connection->topics);
return;
}
@@ -194,4 +196,26 @@ class GameWebSocketServer
$connection->topics = [];
unset(self::$connections[$connection->id]);
}
/**
* 后台联调:订阅赔率相关主题后立即推送演示帧(库内样例玩家,带 is_test / preview
*
* @param list<string> $topics
*/
private static function pushAdminTestOddsPreview(TcpConnection $connection, array $topics): void
{
$frames = GameWebSocketPayloadHelper::adminTestPushFrames($topics);
if ($frames === []) {
return;
}
$serverTime = time();
foreach ($frames as $frame) {
$connection->send(json_encode([
'event' => $frame['event'],
'topic' => $frame['topic'],
'data' => $frame['data'],
'server_time' => $serverTime,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
}
}