From 827da2058ff2c6b201b3615c4f10ff78cbd27128 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Tue, 26 May 2026 17:27:00 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=8A=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E4=B8=8B=E4=B8=80=E6=9C=9Fbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/GameLiveService.php | 23 +++++++----- app/process/GameWebSocketServer.php | 3 ++ web/src/views/backend/game/live/index.vue | 45 ++++++++++++++++++++++- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index ac94ada..1919dca 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -616,6 +616,9 @@ final class GameLiveService if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) { Db::commit(); $periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : ''; + GameHotDataCoordinator::afterGameRecordCommitted($rid); + self::publishSnapshot($rid); + return [ 'ok' => true, 'msg' => __('Draw completed; paying out'), @@ -755,15 +758,7 @@ final class GameLiveService public static function tickAutoDraw(): void { - if (!GameRecordService::isAutoCreateEnabled()) { - $record = Db::name('game_record') - ->whereIn('status', [0, 1]) - ->order('id', 'asc') - ->find(); - $record = is_array($record) ? $record : null; - } else { - $record = self::resolveRecordForAutoDraw(); - } + $record = self::resolveRecordForAutoDraw(); if (!$record || !in_array((int) $record['status'], [0, 1], true)) { return; } @@ -772,7 +767,15 @@ final class GameLiveService if ($elapsed < $periodSeconds) { return; } - self::drawResult((int) $record['id'], null); + $rid = (int) $record['id']; + $out = self::drawResult($rid, null); + if (!($out['ok'] ?? false)) { + Log::warning('tickAutoDraw: drawResult failed', [ + 'record_id' => $rid, + 'period_no' => $record['period_no'] ?? '', + 'msg' => $out['msg'] ?? '', + ]); + } } /** diff --git a/app/process/GameWebSocketServer.php b/app/process/GameWebSocketServer.php index d73be55..1405ab2 100644 --- a/app/process/GameWebSocketServer.php +++ b/app/process/GameWebSocketServer.php @@ -85,6 +85,9 @@ class GameWebSocketServer if (!$hasAdminSubscriber) { return; } + // 与 GameLiveTicker 对齐:仅推快照时若 live ticker 未运行会导致倒计时归零但永不开奖 + GameLiveService::finalizePayoutGrace(); + GameLiveService::tickAutoDraw(); $snapshot = GameLiveService::buildSnapshot(null); $payload = json_encode([ 'event' => 'admin.live.snapshot', diff --git a/web/src/views/backend/game/live/index.vue b/web/src/views/backend/game/live/index.vue index 9a5657b..6b24095 100644 --- a/web/src/views/backend/game/live/index.vue +++ b/web/src/views/backend/game/live/index.vue @@ -278,6 +278,8 @@ const serverSkewSeconds = ref(0) const clockTick = ref(0) let clockTimer: number | null = null let payoutStuckRefreshTimer: number | null = null +let drawStuckRefreshTimer: number | null = null +let drawStuckSeconds = 0 let fallbackPollTimer: number | null = null let betStreamRefreshTimer: number | null = null /** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */ @@ -1003,10 +1005,44 @@ function schedulePayoutEndRefresh(delayMs: number): void { if (!snapshot.is_payout_phase) { return } - if (!wsConnected.value) { + void loadSnapshot({ force: true }) + }, delayMs) +} + +/** 下注/开奖倒计时均已归零但仍未进入派彩(常见于 live ticker 未跑或开奖锁竞争失败) */ +function isPrePayoutDrawStuck(): boolean { + if (snapshot.is_payout_phase || !snapshot.can_calculate) { + return false + } + const bet = snapshot.bet_remaining_seconds ?? 0 + const draw = snapshot.remaining_seconds ?? 0 + if (bet > 0 || draw > 0) { + return false + } + const st = numberValue(snapshot.record?.status) + return st === 0 || st === 1 +} + +function tickPrePayoutDrawStuckRecovery(): void { + if (!isPrePayoutDrawStuck()) { + drawStuckSeconds = 0 + if (drawStuckRefreshTimer !== null) { + window.clearTimeout(drawStuckRefreshTimer) + drawStuckRefreshTimer = null + } + return + } + drawStuckSeconds++ + if (drawStuckSeconds < 8 || drawStuckRefreshTimer !== null) { + return + } + drawStuckRefreshTimer = window.setTimeout(() => { + drawStuckRefreshTimer = null + drawStuckSeconds = 0 + if (isPrePayoutDrawStuck()) { void loadSnapshot({ force: true }) } - }, delayMs) + }, 300) } onMounted(async () => { @@ -1014,6 +1050,7 @@ onMounted(async () => { window.addEventListener('resize', updateIsMobile) clockTimer = window.setInterval(() => { clockTick.value++ + tickPrePayoutDrawStuckRecovery() }, 1000) fallbackPollTimer = window.setInterval(() => { if (!wsConnected.value) { @@ -1040,6 +1077,10 @@ onUnmounted(() => { window.clearTimeout(payoutStuckRefreshTimer) payoutStuckRefreshTimer = null } + if (drawStuckRefreshTimer !== null) { + window.clearTimeout(drawStuckRefreshTimer) + drawStuckRefreshTimer = null + } if (betStreamRefreshTimer !== null) { window.clearTimeout(betStreamRefreshTimer) betStreamRefreshTimer = null