1.优化游戏实时对局/admin/game/live页面卡顿的问题
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)。
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user