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(仅清聚合键)
|
* @param int|null $id 可为 null(仅清聚合键)
|
||||||
*/
|
*/
|
||||||
@@ -122,7 +122,33 @@ final class GameHotDataRedis
|
|||||||
self::redisDel(self::KEY_GR_ID . $id);
|
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);
|
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) {
|
if (!$record) {
|
||||||
return self::emptySnapshotPayload();
|
return self::emptySnapshotPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
$rid = (int) $record['id'];
|
$rid = (int) $record['id'];
|
||||||
self::ensureAiLocked($rid);
|
self::ensureAiLocked($rid);
|
||||||
$record = $freshFromDb ? self::reloadRecordFromDb($rid) : self::reloadRecord($rid);
|
$record = self::reloadRecord($rid);
|
||||||
if (!$record) {
|
if (!$record) {
|
||||||
return self::emptySnapshotPayload();
|
return self::emptySnapshotPayload();
|
||||||
}
|
}
|
||||||
@@ -280,10 +280,12 @@ final class GameLiveService
|
|||||||
$betRemaining = max(0, $betSeconds - $elapsed);
|
$betRemaining = max(0, $betSeconds - $elapsed);
|
||||||
$status = (int) $record['status'];
|
$status = (int) $record['status'];
|
||||||
|
|
||||||
|
$now = time();
|
||||||
$payoutUntil = isset($record['payout_until']) ? (int) $record['payout_until'] : 0;
|
$payoutUntil = isset($record['payout_until']) ? (int) $record['payout_until'] : 0;
|
||||||
$payoutRemaining = 0;
|
$payoutRemaining = 0;
|
||||||
if ($status === 3 && $payoutUntil > 0) {
|
$isPayoutPhase = $status === 3 && $payoutUntil > $now;
|
||||||
$payoutRemaining = max(0, $payoutUntil - time());
|
if ($isPayoutPhase) {
|
||||||
|
$payoutRemaining = $payoutUntil - $now;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bets = Db::name('bet_order')
|
$bets = Db::name('bet_order')
|
||||||
@@ -328,9 +330,8 @@ final class GameLiveService
|
|||||||
&& $elapsed < $periodSeconds;
|
&& $elapsed < $periodSeconds;
|
||||||
|
|
||||||
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
|
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
|
||||||
$hasActiveRound = GameRecordService::hasActiveRecord();
|
|
||||||
/** 关服且已无进行中局:派彩结束后的「完整维护」态(仅此时展示维护中 UI) */
|
/** 关服且已无进行中局:派彩结束后的「完整维护」态(仅此时展示维护中 UI) */
|
||||||
$maintenanceUi = !$runtimeEnabled && !$hasActiveRound;
|
$maintenanceUi = !$runtimeEnabled && !in_array($status, [0, 1, 2, 3], true);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'record' => $record,
|
'record' => $record,
|
||||||
@@ -357,14 +358,14 @@ final class GameLiveService
|
|||||||
'remaining_seconds' => $remaining,
|
'remaining_seconds' => $remaining,
|
||||||
'bet_remaining_seconds' => $betRemaining,
|
'bet_remaining_seconds' => $betRemaining,
|
||||||
'payout_remaining_seconds' => $payoutRemaining,
|
'payout_remaining_seconds' => $payoutRemaining,
|
||||||
'is_payout_phase' => $status === 3,
|
'is_payout_phase' => $isPayoutPhase,
|
||||||
'runtime_enabled' => $runtimeEnabled,
|
'runtime_enabled' => $runtimeEnabled,
|
||||||
'maintenance_ui' => $maintenanceUi,
|
'maintenance_ui' => $maintenanceUi,
|
||||||
/** 关闭游戏(维护)时仍允许完成当局、计算与预约开奖;仅阻止新用户下注与结束后自动开新期 */
|
/** 关闭游戏(维护)时仍允许完成当局、计算与预约开奖;仅阻止新用户下注与结束后自动开新期 */
|
||||||
'can_calculate' => $canCalculate,
|
'can_calculate' => $canCalculate,
|
||||||
'can_draw' => $canScheduleDraw,
|
'can_draw' => $canScheduleDraw,
|
||||||
'can_schedule_draw' => $canScheduleDraw,
|
'can_schedule_draw' => $canScheduleDraw,
|
||||||
'server_time' => time(),
|
'server_time' => $now,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +375,8 @@ final class GameLiveService
|
|||||||
private static function emptySnapshotPayload(): array
|
private static function emptySnapshotPayload(): array
|
||||||
{
|
{
|
||||||
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
|
$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;
|
$maintenanceUi = !$runtimeEnabled && !$hasActiveRound;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -655,10 +657,10 @@ final class GameLiveService
|
|||||||
*/
|
*/
|
||||||
public static function finalizePayoutGrace(): void
|
public static function finalizePayoutGrace(): void
|
||||||
{
|
{
|
||||||
|
$now = time();
|
||||||
$row = Db::name('game_record')
|
$row = Db::name('game_record')
|
||||||
->where('status', 3)
|
->where('status', 3)
|
||||||
->where('payout_until', '>', 0)
|
->whereRaw('((payout_until > 0 AND payout_until <= ?) OR payout_until IS NULL OR payout_until = 0)', [$now])
|
||||||
->where('payout_until', '<=', time())
|
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
->find();
|
->find();
|
||||||
if (!$row) {
|
if (!$row) {
|
||||||
@@ -690,8 +692,6 @@ final class GameLiveService
|
|||||||
}
|
}
|
||||||
GameHotDataCoordinator::afterGameRecordCommitted($id);
|
GameHotDataCoordinator::afterGameRecordCommitted($id);
|
||||||
GameRecordStatService::refreshForRecordId($id);
|
GameRecordStatService::refreshForRecordId($id);
|
||||||
GameHotDataRedis::gameRecordForget($id);
|
|
||||||
GameHotDataRedis::gameRecordForget(null);
|
|
||||||
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
|
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
|
||||||
self::publishSnapshot(null);
|
self::publishSnapshot(null);
|
||||||
self::publishImmediateBettingTickAfterFinalize();
|
self::publishImmediateBettingTickAfterFinalize();
|
||||||
@@ -893,7 +893,7 @@ final class GameLiveService
|
|||||||
|
|
||||||
public static function publishSnapshot(?int $recordId = null): void
|
public static function publishSnapshot(?int $recordId = null): void
|
||||||
{
|
{
|
||||||
$snapshot = self::buildSnapshot($recordId, true);
|
$snapshot = self::buildSnapshot($recordId);
|
||||||
self::publishPublicPeriodPayoutCountdown($snapshot);
|
self::publishPublicPeriodPayoutCountdown($snapshot);
|
||||||
self::publishPublicPeriodTick($snapshot);
|
self::publishPublicPeriodTick($snapshot);
|
||||||
}
|
}
|
||||||
@@ -943,7 +943,7 @@ final class GameLiveService
|
|||||||
*/
|
*/
|
||||||
private static function publishImmediateBettingTickAfterFinalize(): void
|
private static function publishImmediateBettingTickAfterFinalize(): void
|
||||||
{
|
{
|
||||||
$record = self::resolveRecordFromDb(null);
|
$record = GameHotDataRedis::gameRecordActive();
|
||||||
if (!is_array($record)) {
|
if (!is_array($record)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1177,44 +1177,12 @@ final class GameLiveService
|
|||||||
return GameHotDataRedis::gameRecordActive();
|
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
|
private static function reloadRecord(int $id): ?array
|
||||||
{
|
{
|
||||||
$row = GameHotDataRedis::gameRecordById($id);
|
$row = GameHotDataRedis::gameRecordById($id);
|
||||||
return $row ?: null;
|
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)。
|
* 封盘后计算并锁定 AI 号码(本期不变),并封盘(status 0→1)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -200,7 +200,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import createAxios from '/@/utils/axios'
|
import createAxios from '/@/utils/axios'
|
||||||
@@ -268,6 +268,8 @@ const serverSkewSeconds = ref(0)
|
|||||||
/** 每秒递增,驱动派彩剩余秒本地刷新 */
|
/** 每秒递增,驱动派彩剩余秒本地刷新 */
|
||||||
const clockTick = ref(0)
|
const clockTick = ref(0)
|
||||||
let clockTimer: number | null = null
|
let clockTimer: number | null = null
|
||||||
|
let payoutStuckRefreshTimer: number | null = null
|
||||||
|
let fallbackPollTimer: number | null = null
|
||||||
|
|
||||||
const wsLoading = ref(false)
|
const wsLoading = ref(false)
|
||||||
const wsReady = ref(false)
|
const wsReady = ref(false)
|
||||||
@@ -327,6 +329,10 @@ function handleWsPayload(raw: unknown): void {
|
|||||||
mergeLiveSnapshot(parsed.data as anyObj)
|
mergeLiveSnapshot(parsed.data as anyObj)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event === 'admin.live.opened') {
|
||||||
|
void loadSnapshot()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event === 'jackpot.hit' && parsed.data && typeof parsed.data === 'object') {
|
if (event === 'jackpot.hit' && parsed.data && typeof parsed.data === 'object') {
|
||||||
const jackpotData = parsed.data as anyObj
|
const jackpotData = parsed.data as anyObj
|
||||||
const hits = Array.isArray(jackpotData.hits) ? jackpotData.hits : []
|
const hits = Array.isArray(jackpotData.hits) ? jackpotData.hits : []
|
||||||
@@ -340,6 +346,14 @@ function handleWsPayload(raw: unknown): void {
|
|||||||
if (typeof periodData.server_time === 'number') {
|
if (typeof periodData.server_time === 'number') {
|
||||||
syncServerClock(periodData.server_time)
|
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 }
|
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 () => {
|
onMounted(async () => {
|
||||||
updateIsMobile()
|
updateIsMobile()
|
||||||
window.addEventListener('resize', updateIsMobile)
|
window.addEventListener('resize', updateIsMobile)
|
||||||
clockTimer = window.setInterval(() => {
|
clockTimer = window.setInterval(() => {
|
||||||
clockTick.value++
|
clockTick.value++
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
fallbackPollTimer = window.setInterval(() => {
|
||||||
|
if (snapshot.is_payout_phase || snapshot.maintenance_ui) {
|
||||||
|
void loadSnapshot()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
await loadSnapshot()
|
await loadSnapshot()
|
||||||
await reloadWsConfig()
|
await reloadWsConfig()
|
||||||
connectWs()
|
connectWs()
|
||||||
@@ -801,6 +843,14 @@ onUnmounted(() => {
|
|||||||
window.clearInterval(clockTimer)
|
window.clearInterval(clockTimer)
|
||||||
clockTimer = null
|
clockTimer = null
|
||||||
}
|
}
|
||||||
|
if (fallbackPollTimer !== null) {
|
||||||
|
window.clearInterval(fallbackPollTimer)
|
||||||
|
fallbackPollTimer = null
|
||||||
|
}
|
||||||
|
if (payoutStuckRefreshTimer !== null) {
|
||||||
|
window.clearTimeout(payoutStuckRefreshTimer)
|
||||||
|
payoutStuckRefreshTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user