1.优化游戏实时对局/admin/game/live页面卡顿的问题

This commit is contained in:
2026-05-26 13:42:34 +08:00
parent ae7af24565
commit 3a2af4d7c2
3 changed files with 95 additions and 51 deletions

View File

@@ -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);
}
}
/**

View File

@@ -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<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
*/

View File

@@ -200,7 +200,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import createAxios from '/@/utils/axios'
@@ -268,6 +268,8 @@ const serverSkewSeconds = ref(0)
/** 每秒递增,驱动派彩剩余秒本地刷新 */
const clockTick = ref(0)
let clockTimer: number | null = null
let payoutStuckRefreshTimer: number | null = null
let fallbackPollTimer: number | null = null
const wsLoading = ref(false)
const wsReady = ref(false)
@@ -327,6 +329,10 @@ function handleWsPayload(raw: unknown): void {
mergeLiveSnapshot(parsed.data as anyObj)
return
}
if (event === 'admin.live.opened') {
void loadSnapshot()
return
}
if (event === 'jackpot.hit' && parsed.data && typeof parsed.data === 'object') {
const jackpotData = parsed.data as anyObj
const hits = Array.isArray(jackpotData.hits) ? jackpotData.hits : []
@@ -340,6 +346,14 @@ function handleWsPayload(raw: unknown): void {
if (typeof periodData.server_time === 'number') {
syncServerClock(periodData.server_time)
}
const status = typeof periodData.status === 'string' ? periodData.status : ''
const periodNo = typeof periodData.period_no === 'string' ? periodData.period_no : ''
const currentNo = typeof snapshot.record?.period_no === 'string' ? snapshot.record.period_no : ''
if (status === 'betting' && periodNo !== '' && periodNo !== currentNo) {
void loadSnapshot()
} else if (status === 'finished' && snapshot.is_payout_phase) {
void loadSnapshot()
}
}
}
@@ -783,12 +797,40 @@ const countdownParts = computed(() => {
return { bet, draw, payout }
})
/** 派彩倒计时从 >0 变为 0 时主动拉 HTTP 快照 */
watch(payoutRemainingLive, (remain, prev) => {
if (!snapshot.is_payout_phase || remain !== 0) {
return
}
if (prev !== null && prev !== undefined && prev > 0) {
schedulePayoutEndRefresh(400)
}
})
function schedulePayoutEndRefresh(delayMs: number): void {
if (payoutStuckRefreshTimer !== null) {
window.clearTimeout(payoutStuckRefreshTimer)
}
payoutStuckRefreshTimer = window.setTimeout(() => {
payoutStuckRefreshTimer = null
if (!snapshot.is_payout_phase) {
return
}
void loadSnapshot()
}, delayMs)
}
onMounted(async () => {
updateIsMobile()
window.addEventListener('resize', updateIsMobile)
clockTimer = window.setInterval(() => {
clockTick.value++
}, 1000)
fallbackPollTimer = window.setInterval(() => {
if (snapshot.is_payout_phase || snapshot.maintenance_ui) {
void loadSnapshot()
}
}, 5000)
await loadSnapshot()
await reloadWsConfig()
connectWs()
@@ -801,6 +843,14 @@ onUnmounted(() => {
window.clearInterval(clockTimer)
clockTimer = null
}
if (fallbackPollTimer !== null) {
window.clearInterval(fallbackPollTimer)
fallbackPollTimer = null
}
if (payoutStuckRefreshTimer !== null) {
window.clearTimeout(payoutStuckRefreshTimer)
payoutStuckRefreshTimer = null
}
})
</script>