From 40247af0d6537c9197ca37e901fc2dd2e096c0ee Mon Sep 17 00:00:00 2001
From: zhenhui <1276357500@qq.com>
Date: Tue, 26 May 2026 13:51:45 +0800
Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E6=B8=B8=E6=88=8F=E5=AE=9E?=
=?UTF-8?q?=E6=97=B6=E5=AF=B9=E5=B1=80/admin/game/live=E9=A1=B5=E9=9D=A2?=
=?UTF-8?q?=E7=9A=84=E6=8A=A5=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/admin/controller/game/Live.php | 2 +
web/src/views/backend/game/live/index.vue | 131 +++++++++++++++++-----
2 files changed, 104 insertions(+), 29 deletions(-)
diff --git a/app/admin/controller/game/Live.php b/app/admin/controller/game/Live.php
index 01eb851..dac1736 100644
--- a/app/admin/controller/game/Live.php
+++ b/app/admin/controller/game/Live.php
@@ -57,7 +57,9 @@ class Live extends Backend
'period.locked',
'period.opened',
'period.payout',
+ 'period.payout.tick',
'bet.accepted',
+ 'bet.win',
'wallet.changed',
'auto.spin.progress',
];
diff --git a/web/src/views/backend/game/live/index.vue b/web/src/views/backend/game/live/index.vue
index 093038c..25d0cd7 100644
--- a/web/src/views/backend/game/live/index.vue
+++ b/web/src/views/backend/game/live/index.vue
@@ -37,7 +37,7 @@
>
{{ t('game.live.void_btn') }}
- {{ t('Refresh') }}
+ {{ t('Refresh') }}
@@ -203,6 +203,7 @@
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
+import axios from 'axios'
import createAxios from '/@/utils/axios'
interface Snapshot {
@@ -270,6 +271,8 @@ const clockTick = ref(0)
let clockTimer: number | null = null
let payoutStuckRefreshTimer: number | null = null
let fallbackPollTimer: number | null = null
+/** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */
+let snapshotLoadPromise: Promise | null = null
const wsLoading = ref(false)
const wsReady = ref(false)
@@ -330,7 +333,7 @@ function handleWsPayload(raw: unknown): void {
return
}
if (event === 'admin.live.opened') {
- void loadSnapshot()
+ void loadSnapshot({ force: true })
return
}
if (event === 'jackpot.hit' && parsed.data && typeof parsed.data === 'object') {
@@ -341,19 +344,63 @@ function handleWsPayload(raw: unknown): void {
}
return
}
+ if (event === 'period.payout.tick' && parsed.data && typeof parsed.data === 'object') {
+ mergePeriodPayoutTick(parsed.data as anyObj)
+ return
+ }
if (event === 'period.tick' && parsed.data && typeof parsed.data === 'object') {
- const periodData = parsed.data as anyObj
- 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) {
+ handlePeriodTickEvent(parsed.data as anyObj)
+ return
+ }
+ if (event === 'bet.accepted' && parsed.data && typeof parsed.data === 'object') {
+ if (!wsConnected.value) {
void loadSnapshot()
}
+ return
+ }
+}
+
+/** 用 period.tick 轻量字段刷新倒计时(不触发 HTTP,避免与 WS 每秒 snapshot 冲突) */
+function mergePeriodTickFields(periodData: anyObj): void {
+ if (typeof periodData.server_time === 'number') {
+ syncServerClock(periodData.server_time)
+ }
+ if (typeof periodData.countdown === 'number') {
+ snapshot.remaining_seconds = Math.max(0, periodData.countdown)
+ }
+ if (typeof periodData.bet_close_in === 'number') {
+ snapshot.bet_remaining_seconds = Math.max(0, periodData.bet_close_in)
+ }
+}
+
+function mergePeriodPayoutTick(data: anyObj): void {
+ if (typeof data.server_time === 'number') {
+ syncServerClock(data.server_time)
+ }
+ const remain = numberValue(data.payout_remaining_seconds)
+ if (remain !== null) {
+ snapshot.payout_remaining_seconds = Math.max(0, remain)
+ snapshot.is_payout_phase = remain > 0 || snapshot.is_payout_phase === true
+ }
+}
+
+function handlePeriodTickEvent(periodData: anyObj): void {
+ mergePeriodTickFields(periodData)
+ 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({ force: true })
+ return
+ }
+ if (status === 'finished' && snapshot.is_payout_phase) {
+ if (!wsConnected.value) {
+ void loadSnapshot()
+ }
+ return
+ }
+ if (currentNo === '' && periodNo !== '') {
+ void loadSnapshot({ force: true })
}
}
@@ -643,16 +690,40 @@ const payoutRemainingLive = computed(() => {
return snapshot.payout_remaining_seconds ?? 0
})
-async function loadSnapshot() {
- loading.value = true
- try {
- const res = await createAxios({ url: '/admin/game.Live/snapshot', method: 'get', showCodeMessage: false })
- if (res.code === 1 && res.data) {
- mergeLiveSnapshot(res.data as anyObj)
- }
- } finally {
- loading.value = false
+function isRequestCanceled(err: unknown): boolean {
+ return axios.isCancel(err) || (err instanceof Error && err.name === 'CanceledError')
+}
+
+async function loadSnapshot(options?: { force?: boolean }): Promise {
+ if (!options?.force && wsConnected.value && snapshot.record) {
+ return
}
+ if (snapshotLoadPromise) {
+ return snapshotLoadPromise
+ }
+ snapshotLoadPromise = (async () => {
+ loading.value = true
+ try {
+ const res = await createAxios({
+ url: '/admin/game.Live/snapshot',
+ method: 'get',
+ showCodeMessage: false,
+ showErrorMessage: false,
+ cancelDuplicateRequest: false,
+ })
+ if (res.code === 1 && res.data) {
+ mergeLiveSnapshot(res.data as anyObj)
+ }
+ } catch (err) {
+ if (!isRequestCanceled(err)) {
+ throw err
+ }
+ } finally {
+ loading.value = false
+ snapshotLoadPromise = null
+ }
+ })()
+ return snapshotLoadPromise
}
async function onRuntimeSwitch(val: boolean | string | number): void {
@@ -679,10 +750,10 @@ async function onRuntimeSwitch(val: boolean | string | number): void {
if (res.code === 1 && res.data) {
mergeLiveSnapshot(res.data as anyObj)
} else {
- await loadSnapshot()
+ await loadSnapshot({ force: true })
}
} catch {
- await loadSnapshot()
+ await loadSnapshot({ force: true })
} finally {
runtimeSwitchLoading.value = false
pendingRuntimeTarget.value = null
@@ -768,7 +839,7 @@ async function onDrawWithNumber(targetNumber: number) {
},
showSuccessMessage: true,
})
- await loadSnapshot()
+ await loadSnapshot({ force: true })
} finally {
drawLoading.value = false
}
@@ -816,7 +887,9 @@ function schedulePayoutEndRefresh(delayMs: number): void {
if (!snapshot.is_payout_phase) {
return
}
- void loadSnapshot()
+ if (!wsConnected.value) {
+ void loadSnapshot({ force: true })
+ }
}, delayMs)
}
@@ -827,11 +900,11 @@ onMounted(async () => {
clockTick.value++
}, 1000)
fallbackPollTimer = window.setInterval(() => {
- if (snapshot.is_payout_phase || snapshot.maintenance_ui) {
- void loadSnapshot()
+ if (!wsConnected.value) {
+ void loadSnapshot({ force: true })
}
- }, 5000)
- await loadSnapshot()
+ }, 3000)
+ await loadSnapshot({ force: true })
await reloadWsConfig()
connectWs()
})