feat: 增强开奖 API 的币种支持并优化钱包处理逻辑
更新 getDrawCurrent、getDrawResults 与 getDrawResultByNo 方法,新增币种参数支持,以适配玩家币种偏好。 优化 HallBettingGrid 及相关组件:支持币种切换时自动刷新钱包数据。 重构钱包处理逻辑,简化余额更新流程并提升用户体验。 新增会话过期相关多语言提示文案,并优化现有翻译内容,提升多语言环境下的提示清晰度。
This commit is contained in:
@@ -4,9 +4,12 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { getDrawCurrent } from "@/api/draw";
|
||||
import { isHallAwaitingDrawProcessing, isHallSealedCountdownUi } from "@/features/draw/draw-status-meta";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { getLotteryEcho } from "@/lib/lottery-echo";
|
||||
import { startWalletRefreshBurst } from "@/lib/wallet-refresh-burst";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
||||
import type { DrawCurrentPayload, DrawCurrentResponse } from "@/types/api/draw-current";
|
||||
import type { DrawCurrentPayload } from "@/types/api/draw-current";
|
||||
|
||||
/** 大厅共享的当期快照(由 {@link useHallDrawLive} 产出,供期号条与下注表共用)。 */
|
||||
export type HallDrawLiveSnapshot = {
|
||||
@@ -40,6 +43,33 @@ function secondsUntilIso(iso: string | null | undefined, effectiveNowMs: number)
|
||||
/**
|
||||
* 以服务端 `server_now_ms` 为锚、本地时钟仅负责推进,倒计时与 ISO 时刻一致。
|
||||
*/
|
||||
function mergeJackpotForCurrency(
|
||||
incoming: DrawCurrentPayload | null,
|
||||
previous: DrawCurrentPayload | null | undefined,
|
||||
activeCurrency: string,
|
||||
): DrawCurrentPayload | null {
|
||||
if (incoming === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const incomingCode = (
|
||||
incoming.jackpot_currency_code ?? incoming.jackpot?.currency_code ?? ""
|
||||
)
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const wanted = activeCurrency.trim().toUpperCase();
|
||||
|
||||
if (incomingCode !== "" && incomingCode !== wanted && previous?.jackpot) {
|
||||
return {
|
||||
...incoming,
|
||||
jackpot_currency_code: wanted,
|
||||
jackpot: previous.jackpot,
|
||||
};
|
||||
}
|
||||
|
||||
return incoming;
|
||||
}
|
||||
|
||||
function applySnapshotDrift(
|
||||
payload: DrawCurrentPayload,
|
||||
emittedAtMs: number,
|
||||
@@ -64,6 +94,7 @@ function applySnapshotDrift(
|
||||
* 已集成网络连接管理,WebSocket断开时自动切换到轮询模式。
|
||||
*/
|
||||
export function useHallDrawLive(): HallDrawLiveSnapshot {
|
||||
const { activeCurrency } = useActivePlayerCurrency();
|
||||
const [raw, setRaw] = useState<DrawCurrentPayload | null | undefined>(undefined);
|
||||
const [serverNowMs, setServerNowMs] = useState(() => Date.now());
|
||||
const [emittedAtMs, setEmittedAtMs] = useState(() => Date.now());
|
||||
@@ -78,40 +109,44 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
|
||||
const setDrawPollingIntervalId = useNetworkConnectionStore(
|
||||
(s) => s.setDrawPollingIntervalId,
|
||||
);
|
||||
const setWalletPollingIntervalId = useNetworkConnectionStore(
|
||||
(s) => s.setWalletPollingIntervalId,
|
||||
);
|
||||
const setWalletPollingExpiryAt = useNetworkConnectionStore(
|
||||
(s) => s.setWalletPollingExpiryAt,
|
||||
);
|
||||
const clearDrawPolling = useNetworkConnectionStore((s) => s.clearDrawPolling);
|
||||
|
||||
const latestSnapshotMsRef = useRef(0);
|
||||
|
||||
const applySnapshot = useCallback((anchorMs: number, data: DrawCurrentPayload | null) => {
|
||||
if (anchorMs < latestSnapshotMsRef.current) {
|
||||
return;
|
||||
}
|
||||
latestSnapshotMsRef.current = anchorMs;
|
||||
setServerNowMs(anchorMs);
|
||||
setRaw(data);
|
||||
setEmittedAtMs(anchorMs);
|
||||
}, []);
|
||||
const applySnapshot = useCallback(
|
||||
(anchorMs: number, data: DrawCurrentPayload | null) => {
|
||||
if (anchorMs < latestSnapshotMsRef.current) {
|
||||
return;
|
||||
}
|
||||
latestSnapshotMsRef.current = anchorMs;
|
||||
setServerNowMs(anchorMs);
|
||||
setRaw((prev) => mergeJackpotForCurrency(data, prev, activeCurrency));
|
||||
setEmittedAtMs(anchorMs);
|
||||
},
|
||||
[activeCurrency],
|
||||
);
|
||||
|
||||
const mergeFromWs = useCallback((evt: HallWsEnvelope) => {
|
||||
const anchor = evt.emitted_at_ms ?? Date.now();
|
||||
applySnapshot(anchor, evt.data);
|
||||
}, [applySnapshot]);
|
||||
const mergeFromWs = useCallback(
|
||||
(evt: HallWsEnvelope) => {
|
||||
const anchor = evt.emitted_at_ms ?? Date.now();
|
||||
applySnapshot(anchor, evt.data);
|
||||
},
|
||||
[applySnapshot],
|
||||
);
|
||||
|
||||
const mergeCountdownFromWs = useCallback((evt: HallWsEnvelope) => {
|
||||
if (evt.data === null) return;
|
||||
const anchor = evt.emitted_at_ms ?? Date.now();
|
||||
applySnapshot(anchor, evt.data);
|
||||
}, [applySnapshot]);
|
||||
const mergeCountdownFromWs = useCallback(
|
||||
(evt: HallWsEnvelope) => {
|
||||
if (evt.data === null) return;
|
||||
const anchor = evt.emitted_at_ms ?? Date.now();
|
||||
applySnapshot(anchor, evt.data);
|
||||
},
|
||||
[applySnapshot],
|
||||
);
|
||||
|
||||
const load = useCallback(async (options?: { force?: boolean }) => {
|
||||
try {
|
||||
setError(null);
|
||||
const d = await getDrawCurrent();
|
||||
const d = await getDrawCurrent({ currency: activeCurrency });
|
||||
const wsConnected = useNetworkConnectionStore.getState().isWebSocketConnected;
|
||||
if (!options?.force && wsConnected && d.server_now_ms < latestSnapshotMsRef.current) {
|
||||
return;
|
||||
@@ -121,7 +156,15 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
|
||||
setError("draw.loadFailedRefresh");
|
||||
setRaw(undefined);
|
||||
}
|
||||
}, [applySnapshot]);
|
||||
}, [activeCurrency, applySnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => {
|
||||
void load({ force: true });
|
||||
};
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
}, [load]);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
@@ -155,124 +198,26 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// WebSocket 订阅 + 降级轮询逻辑
|
||||
// WebSocket 订阅(期号 HTTP 轮询由下方单一 effect 负责)
|
||||
useEffect(() => {
|
||||
const echo = getLotteryEcho();
|
||||
if (!echo) return;
|
||||
|
||||
// 监听 WebSocket 事件
|
||||
const channel = echo.channel("lottery-hall");
|
||||
|
||||
// 设置事件监听
|
||||
channel.listen(".draw.countdown", mergeCountdownFromWs);
|
||||
channel.listen(".draw.status_change", mergeFromWs);
|
||||
channel.listen(".result.published", (evt: HallWsEnvelope) => {
|
||||
// 开奖结果发布时触发钱包轮询
|
||||
mergeFromWs(evt);
|
||||
|
||||
// 触发钱包余额轮询(开奖后需要更新余额)
|
||||
const intervalId = window.setInterval(() => {
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
}, 30_000);
|
||||
setWalletPollingIntervalId(intervalId);
|
||||
// 设置2分钟后停止轮询
|
||||
setWalletPollingExpiryAt(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
// 2分钟后自动清理
|
||||
window.setTimeout(() => {
|
||||
window.clearInterval(intervalId);
|
||||
setWalletPollingIntervalId(null);
|
||||
}, 2 * 60 * 1000);
|
||||
startWalletRefreshBurst();
|
||||
});
|
||||
|
||||
// 连接状态变化处理
|
||||
const handleConnected = () => {
|
||||
// WebSocket 连接成功,停止降级轮询
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (currentPollingId) {
|
||||
window.clearInterval(currentPollingId);
|
||||
setDrawPollingIntervalId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnected = () => {
|
||||
// WebSocket 断开,启动降级轮询(如果还没启动)
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (!currentPollingId) {
|
||||
const intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, 30_000); // WebSocket断开时使用30秒轮询
|
||||
setDrawPollingIntervalId(intervalId);
|
||||
}
|
||||
};
|
||||
|
||||
// 订阅 Echo 的连接事件(如果支持)
|
||||
if (echo.connector?.pusher) {
|
||||
echo.connector.pusher.connection.bind("connected", handleConnected);
|
||||
echo.connector.pusher.connection.bind("disconnected", handleDisconnected);
|
||||
}
|
||||
|
||||
// 如果当前是轮询模式,启动轮询
|
||||
if (mode === "polling" || mode === "offline") {
|
||||
handleDisconnected();
|
||||
}
|
||||
|
||||
return () => {
|
||||
channel.stopListening(".draw.countdown");
|
||||
channel.stopListening(".draw.status_change");
|
||||
channel.stopListening(".result.published");
|
||||
if (echo.connector?.pusher) {
|
||||
echo.connector.pusher.connection.unbind("connected", handleConnected);
|
||||
echo.connector.pusher.connection.unbind(
|
||||
"disconnected",
|
||||
handleDisconnected,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
mergeCountdownFromWs,
|
||||
mergeFromWs,
|
||||
load,
|
||||
setDrawPollingIntervalId,
|
||||
setWalletPollingIntervalId,
|
||||
setWalletPollingExpiryAt,
|
||||
mode,
|
||||
]);
|
||||
|
||||
// WebSocket 断开时的兜底轮询(独立于 Echo 事件监听)
|
||||
useEffect(() => {
|
||||
let intervalId: number | null = null;
|
||||
|
||||
// 只在 WebSocket 未连接且没有轮询计时器时启动轮询
|
||||
if (!isWebSocketConnected && mode !== "websocket") {
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (!currentPollingId) {
|
||||
const initialLoadId = window.setTimeout(() => {
|
||||
void load();
|
||||
}, 0);
|
||||
|
||||
// 设置30秒轮询
|
||||
intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, 30_000);
|
||||
setDrawPollingIntervalId(intervalId);
|
||||
return () => {
|
||||
window.clearTimeout(initialLoadId);
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId);
|
||||
setDrawPollingIntervalId(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId);
|
||||
setDrawPollingIntervalId(null);
|
||||
}
|
||||
};
|
||||
}, [isWebSocketConnected, mode, load, setDrawPollingIntervalId]);
|
||||
}, [mergeCountdownFromWs, mergeFromWs]);
|
||||
|
||||
const display: DrawCurrentPayload | null | undefined =
|
||||
raw === undefined || raw === null
|
||||
@@ -339,28 +284,32 @@ export function useHallDrawLive(): HallDrawLiveSnapshot {
|
||||
nowMs,
|
||||
);
|
||||
|
||||
// 封盘/待开奖/开奖中:调度推进状态前每 3 秒拉一次,避免 0:00 卡几十秒
|
||||
// 单一期号 HTTP 轮询:降级 30s / 待开奖 3s / 常态保险 45s(避免多套 interval 叠加)
|
||||
useEffect(() => {
|
||||
if (!needsFastDrawPoll) {
|
||||
return;
|
||||
}
|
||||
const intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, 3000);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [needsFastDrawPoll, load]);
|
||||
const wsOk = isWebSocketConnected && mode === "websocket";
|
||||
const intervalMs = !wsOk ? 30_000 : needsFastDrawPoll ? 3_000 : 45_000;
|
||||
|
||||
const initialTimer = window.setTimeout(() => {
|
||||
void load();
|
||||
}, 0);
|
||||
|
||||
// WebSocket 已连接时的兜底轮询(tick 延迟时的保险;常态下由 WS 推送,避免 HTTP 覆盖新快照)
|
||||
useEffect(() => {
|
||||
if (isWebSocketConnected && !needsFastDrawPoll) {
|
||||
return;
|
||||
}
|
||||
const intervalMs = needsFastDrawPoll ? 15_000 : 45_000;
|
||||
const intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, intervalMs);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [load, needsFastDrawPoll, isWebSocketConnected]);
|
||||
setDrawPollingIntervalId(intervalId);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(initialTimer);
|
||||
clearDrawPolling();
|
||||
};
|
||||
}, [
|
||||
isWebSocketConnected,
|
||||
mode,
|
||||
needsFastDrawPoll,
|
||||
load,
|
||||
setDrawPollingIntervalId,
|
||||
clearDrawPolling,
|
||||
]);
|
||||
|
||||
return { raw, display, serverNowMs, nowMs, error, reload: load, isBettable };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user