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() })