diff --git a/app/common/service/GameHotDataRedis.php b/app/common/service/GameHotDataRedis.php index 9934941..beeecc1 100644 --- a/app/common/service/GameHotDataRedis.php +++ b/app/common/service/GameHotDataRedis.php @@ -104,7 +104,7 @@ final class GameHotDataRedis } /** - * 对局写入后:刷新指定 id 的行缓存,并删除「活跃局 / 最新局」聚合键以免脏读 + * 对局写入后:刷新指定 id 行缓存,并回写「活跃局 / 最新局」聚合键(供 snapshot / WS 只读 Redis) * * @param int|null $id 可为 null(仅清聚合键) */ @@ -122,7 +122,33 @@ final class GameHotDataRedis self::redisDel(self::KEY_GR_ID . $id); } } - self::redisDel(self::KEY_GR_ACTIVE, self::KEY_GR_LATEST); + self::gameRecordRefreshAggregateCaches(); + } + + /** + * 写入后回写「活跃局 / 最新局」聚合缓存(读库一次,供 snapshot / WS 直推只读 Redis)。 + */ + public static function gameRecordRefreshAggregateCaches(): void + { + if (!self::enabled()) { + return; + } + $ttl = self::intConfig('ttl_game_record', 60); + $active = Db::name('game_record') + ->whereIn('status', [0, 1, 2, 3]) + ->order('id', 'desc') + ->find(); + if (is_array($active)) { + self::redisSetEx(self::KEY_GR_ACTIVE, $ttl, json_encode($active, JSON_UNESCAPED_UNICODE)); + } else { + self::redisDel(self::KEY_GR_ACTIVE); + } + $latest = Db::name('game_record')->order('id', 'desc')->find(); + if (is_array($latest)) { + self::redisSetEx(self::KEY_GR_LATEST, $ttl, json_encode($latest, JSON_UNESCAPED_UNICODE)); + } else { + self::redisDel(self::KEY_GR_LATEST); + } } /** diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index 7e190d6..446dbce 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -258,16 +258,16 @@ final class GameLiveService self::publishSnapshot(null); } - public static function buildSnapshot(?int $recordId = null, bool $freshFromDb = false): array + public static function buildSnapshot(?int $recordId = null): array { - $record = $freshFromDb ? self::resolveRecordFromDb($recordId) : self::resolveRecord($recordId); + $record = self::resolveRecord($recordId); if (!$record) { return self::emptySnapshotPayload(); } $rid = (int) $record['id']; self::ensureAiLocked($rid); - $record = $freshFromDb ? self::reloadRecordFromDb($rid) : self::reloadRecord($rid); + $record = self::reloadRecord($rid); if (!$record) { return self::emptySnapshotPayload(); } @@ -280,10 +280,12 @@ final class GameLiveService $betRemaining = max(0, $betSeconds - $elapsed); $status = (int) $record['status']; + $now = time(); $payoutUntil = isset($record['payout_until']) ? (int) $record['payout_until'] : 0; $payoutRemaining = 0; - if ($status === 3 && $payoutUntil > 0) { - $payoutRemaining = max(0, $payoutUntil - time()); + $isPayoutPhase = $status === 3 && $payoutUntil > $now; + if ($isPayoutPhase) { + $payoutRemaining = $payoutUntil - $now; } $bets = Db::name('bet_order') @@ -328,9 +330,8 @@ final class GameLiveService && $elapsed < $periodSeconds; $runtimeEnabled = GameRecordService::isLiveRuntimeEnabled(); - $hasActiveRound = GameRecordService::hasActiveRecord(); /** 关服且已无进行中局:派彩结束后的「完整维护」态(仅此时展示维护中 UI) */ - $maintenanceUi = !$runtimeEnabled && !$hasActiveRound; + $maintenanceUi = !$runtimeEnabled && !in_array($status, [0, 1, 2, 3], true); return [ 'record' => $record, @@ -357,14 +358,14 @@ final class GameLiveService 'remaining_seconds' => $remaining, 'bet_remaining_seconds' => $betRemaining, 'payout_remaining_seconds' => $payoutRemaining, - 'is_payout_phase' => $status === 3, + 'is_payout_phase' => $isPayoutPhase, 'runtime_enabled' => $runtimeEnabled, 'maintenance_ui' => $maintenanceUi, /** 关闭游戏(维护)时仍允许完成当局、计算与预约开奖;仅阻止新用户下注与结束后自动开新期 */ 'can_calculate' => $canCalculate, 'can_draw' => $canScheduleDraw, 'can_schedule_draw' => $canScheduleDraw, - 'server_time' => time(), + 'server_time' => $now, ]; } @@ -374,7 +375,8 @@ final class GameLiveService private static function emptySnapshotPayload(): array { $runtimeEnabled = GameRecordService::isLiveRuntimeEnabled(); - $hasActiveRound = GameRecordService::hasActiveRecord(); + $active = GameHotDataRedis::gameRecordActive(); + $hasActiveRound = is_array($active) && in_array((int) ($active['status'] ?? -1), [0, 1, 2, 3], true); $maintenanceUi = !$runtimeEnabled && !$hasActiveRound; return [ @@ -655,10 +657,10 @@ final class GameLiveService */ public static function finalizePayoutGrace(): void { + $now = time(); $row = Db::name('game_record') ->where('status', 3) - ->where('payout_until', '>', 0) - ->where('payout_until', '<=', time()) + ->whereRaw('((payout_until > 0 AND payout_until <= ?) OR payout_until IS NULL OR payout_until = 0)', [$now]) ->order('id', 'desc') ->find(); if (!$row) { @@ -690,8 +692,6 @@ 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(); @@ -893,7 +893,7 @@ final class GameLiveService public static function publishSnapshot(?int $recordId = null): void { - $snapshot = self::buildSnapshot($recordId, true); + $snapshot = self::buildSnapshot($recordId); self::publishPublicPeriodPayoutCountdown($snapshot); self::publishPublicPeriodTick($snapshot); } @@ -943,7 +943,7 @@ final class GameLiveService */ private static function publishImmediateBettingTickAfterFinalize(): void { - $record = self::resolveRecordFromDb(null); + $record = GameHotDataRedis::gameRecordActive(); if (!is_array($record)) { return; } @@ -1177,44 +1177,12 @@ final class GameLiveService return GameHotDataRedis::gameRecordActive(); } - /** - * WebSocket 推送专用:始终读库,避免 game_record 热缓存仍指向上一期 payouting 导致长时间不推 period.tick。 - * - * @return array|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|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)。 */ diff --git a/web/src/views/backend/game/live/index.vue b/web/src/views/backend/game/live/index.vue index 796f767..093038c 100644 --- a/web/src/views/backend/game/live/index.vue +++ b/web/src/views/backend/game/live/index.vue @@ -200,7 +200,7 @@