1.优化ws游戏对局推送消息
This commit is contained in:
@@ -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)。
|
||||
*/
|
||||
|
||||
@@ -824,7 +824,7 @@
|
||||
#### 7.1.3 推送频率与触发规则(当前实现)
|
||||
|
||||
- `period.tick`:**仅在 `status ∈ {betting, locked}` 时每秒推送**(用于倒计时、状态同步;**不含**赔率全表)。
|
||||
- **派彩静默期**:`status=payouting`(开奖到派彩宽限期结束)期间**完全不推**,避免覆盖前端的中奖动画/弹窗。中奖玩家依靠同时触发的 `period.opened` / `period.payout` / `jackpot.hit` / `wallet.changed(biz_type=payout)` 完成展示。
|
||||
- **派彩静默期**:`status=payouting` 期间**不推** `period.tick`(避免彩池/倒计时干扰中奖动画)。改为每秒推送 **`period.payout.tick`**,仅含 `payout_remaining_seconds` / `payout_until` 等派彩倒计时字段。中奖展示仍靠 `period.opened` / `period.payout` / `jackpot.hit` / `wallet.changed(biz_type=payout)`。
|
||||
- **边界帧(每期仅一次)**:
|
||||
- `status=finished`:派彩宽限期结束、本期进入收尾时推送一帧,告知前端可以清理本期 UI。
|
||||
- `status=void`:本期被作废时推送一帧作为通知。
|
||||
|
||||
Reference in New Issue
Block a user