1.优化ws游戏对局推送消息

This commit is contained in:
2026-05-26 11:26:09 +08:00
parent bad6641eeb
commit 7edc3ec010
2 changed files with 119 additions and 5 deletions

View File

@@ -24,6 +24,8 @@ final class GameLiveService
private const EVT_PERIOD_LOCKED = 'period.locked';
private const EVT_PERIOD_OPENED = 'period.opened';
private const EVT_PERIOD_PAYOUT = 'period.payout';
/** 派彩宽限期内每秒倒计时(不含彩池/下注列表,仅剩余秒数) */
private const EVT_PERIOD_PAYOUT_TICK = 'period.payout.tick';
/** period.tick 边界帧去重finished / void 每期只推一次TTL 兼顾跨进程与跨期重启 */
private const TICK_BOUNDARY_DEDUP_KEY_PREFIX = 'dfw:v1:ws:tick:boundary:';
@@ -252,16 +254,16 @@ final class GameLiveService
self::publishSnapshot(null);
}
public static function buildSnapshot(?int $recordId = null): array
public static function buildSnapshot(?int $recordId = null, bool $freshFromDb = false): array
{
$record = self::resolveRecord($recordId);
$record = $freshFromDb ? self::resolveRecordFromDb($recordId) : self::resolveRecord($recordId);
if (!$record) {
return self::emptySnapshotPayload();
}
$rid = (int) $record['id'];
self::ensureAiLocked($rid);
$record = self::reloadRecord($rid);
$record = $freshFromDb ? self::reloadRecordFromDb($rid) : self::reloadRecord($rid);
if (!$record) {
return self::emptySnapshotPayload();
}
@@ -681,8 +683,11 @@ final class GameLiveService
}
GameHotDataCoordinator::afterGameRecordCommitted($id);
GameRecordStatService::refreshForRecordId($id);
GameHotDataRedis::gameRecordForget($id);
GameHotDataRedis::gameRecordForget(null);
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
self::publishSnapshot(null);
self::publishImmediateBettingTickAfterFinalize();
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, $lock['token'], $lock['redis_lock']);
}
@@ -881,10 +886,87 @@ final class GameLiveService
public static function publishSnapshot(?int $recordId = null): void
{
$snapshot = self::buildSnapshot($recordId);
$snapshot = self::buildSnapshot($recordId, true);
self::publishPublicPeriodPayoutCountdown($snapshot);
self::publishPublicPeriodTick($snapshot);
}
/**
* 派彩宽限期内每秒推送倒计时(仅剩余秒数,不含彩池变化)。
*/
private static function publishPublicPeriodPayoutCountdown(array $snapshot): void
{
$record = $snapshot['record'] ?? null;
if (!is_array($record)) {
return;
}
$dbStatus = filter_var($record['status'] ?? 0, FILTER_VALIDATE_INT);
if ($dbStatus !== 3) {
return;
}
$payoutUntil = filter_var($record['payout_until'] ?? 0, FILTER_VALIDATE_INT);
if ($payoutUntil === false || $payoutUntil <= 0) {
return;
}
$periodId = filter_var($record['id'] ?? 0, FILTER_VALIDATE_INT);
if ($periodId === false) {
$periodId = 0;
}
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
$now = time();
$resultNumber = null;
$resultParsed = filter_var($record['result_number'] ?? null, FILTER_VALIDATE_INT);
if ($resultParsed !== false && $resultParsed > 0) {
$resultNumber = $resultParsed;
}
GameWebSocketEventBus::publish(self::EVT_PERIOD_PAYOUT_TICK, [
'period_id' => $periodId,
'period_no' => $periodNo,
'status' => 'payouting',
'payout_until' => $payoutUntil,
'payout_seconds' => self::getPayoutGraceSeconds(),
'payout_remaining_seconds' => max(0, $payoutUntil - $now),
'result_number' => $resultNumber,
'server_time' => $now,
]);
}
/**
* 派彩结束并创建新期后,立即推送一帧 betting避免热缓存仍指向上一期 payouting 导致长时间无 tick
*/
private static function publishImmediateBettingTickAfterFinalize(): void
{
$record = self::resolveRecordFromDb(null);
if (!is_array($record)) {
return;
}
$dbStatus = filter_var($record['status'] ?? 0, FILTER_VALIDATE_INT);
if ($dbStatus !== 0) {
return;
}
$periodId = filter_var($record['id'] ?? 0, FILTER_VALIDATE_INT);
if ($periodId === false || $periodId <= 0) {
return;
}
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
if ($periodNo === '') {
return;
}
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
$elapsed = max(0, time() - (int) ($record['period_start_at'] ?? time()));
GameWebSocketEventBus::publish(self::EVT_PERIOD_TICK, [
'period_id' => $periodId,
'period_no' => $periodNo,
'status' => 'betting',
'countdown' => max(0, $periodSeconds - $elapsed),
'bet_close_in' => max(0, $betSeconds - $elapsed),
'result_number' => null,
'runtime_enabled' => GameRecordService::isLiveRuntimeEnabled(),
'server_time' => time(),
]);
}
/**
* 移动端公共频道:每秒心跳,含期号、倒计时、阶段(对齐 lobbyInit/periodCurrent 语义)
*/
@@ -1088,12 +1170,44 @@ final class GameLiveService
return GameHotDataRedis::gameRecordActive();
}
/**
* WebSocket 推送专用:始终读库,避免 game_record 热缓存仍指向上一期 payouting 导致长时间不推 period.tick。
*
* @return array<string, mixed>|null
*/
private static function resolveRecordFromDb(?int $recordId): ?array
{
if ($recordId !== null && $recordId > 0) {
$row = Db::name('game_record')->where('id', $recordId)->find();
return is_array($row) ? $row : null;
}
$row = Db::name('game_record')
->whereIn('status', [0, 1, 2, 3])
->order('id', 'desc')
->find();
return is_array($row) ? $row : null;
}
private static function reloadRecord(int $id): ?array
{
$row = GameHotDataRedis::gameRecordById($id);
return $row ?: null;
}
/**
* @return array<string, mixed>|null
*/
private static function reloadRecordFromDb(int $id): ?array
{
if ($id <= 0) {
return null;
}
$row = Db::name('game_record')->where('id', $id)->find();
return is_array($row) ? $row : null;
}
/**
* 封盘后计算并锁定 AI 号码本期不变并封盘status 0→1
*/