fix(hall): freeze ticket preview submissions
This commit is contained in:
@@ -184,6 +184,14 @@ type ClosedPlayCleanupData = {
|
||||
cleanup_lines?: Array<{ client_line_no?: number; play_code?: string }>;
|
||||
};
|
||||
|
||||
type TicketPreviewSubmissionSnapshot = {
|
||||
drawId: string;
|
||||
currencyCode: string;
|
||||
clientTraceId: string;
|
||||
lines: TicketLineInput[];
|
||||
expectedConfigVersions: TicketPreviewData["config_versions"];
|
||||
};
|
||||
|
||||
type PlayToggleWsEvent = {
|
||||
play_code?: string;
|
||||
enabled?: boolean;
|
||||
@@ -601,7 +609,6 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
const [syncAmountColumns, setSyncAmountColumns] = useState<Record<string, boolean>>({});
|
||||
const [quickFillState, setQuickFillState] = useState<QuickFillState>(() => loadQuickFillState());
|
||||
const [quickFillExpanded, setQuickFillExpanded] = useState(false);
|
||||
const [riskStateDrawNo, setRiskStateDrawNo] = useState<string | null>(display?.draw_no ?? null);
|
||||
const [liveSoldOutNumbers, setLiveSoldOutNumbers] = useState<Set<string>>(() => new Set());
|
||||
const [liveWarningNumbers, setLiveWarningNumbers] = useState<Set<string>>(() => new Set());
|
||||
const [debouncedSummary, setDebouncedSummary] = useState({ bet: 0, rebate: 0, actual: 0 });
|
||||
@@ -615,6 +622,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
});
|
||||
/** 单次预览→确认共用,重试 place 复用,避免重复扣款 */
|
||||
const placeTraceIdRef = useRef<string | null>(null);
|
||||
const previewRequestSeqRef = useRef(0);
|
||||
const previewSubmissionRef = useRef<TicketPreviewSubmissionSnapshot | null>(null);
|
||||
const newPlaceTraceId = (): string =>
|
||||
typeof crypto !== "undefined" && crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
@@ -623,6 +632,13 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
const clearPlaceTraceId = useCallback(() => {
|
||||
placeTraceIdRef.current = null;
|
||||
}, []);
|
||||
const invalidatePreview = useCallback(() => {
|
||||
previewRequestSeqRef.current += 1;
|
||||
previewSubmissionRef.current = null;
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
}, [clearPlaceTraceId]);
|
||||
const catalogSeqRef = useRef(0);
|
||||
const walletSeqRef = useRef(0);
|
||||
const selectedCatalogProviderCode =
|
||||
@@ -697,16 +713,14 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
void loadCatalog();
|
||||
const source = (ev as CustomEvent<{ source?: PlayCatalogRefreshSource }>).detail
|
||||
?.source;
|
||||
if (source === "play_config" || source === "risk_cap") {
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
if (source !== undefined) {
|
||||
invalidatePreview();
|
||||
toast.message(t("hall.playConfig.updated"));
|
||||
}
|
||||
};
|
||||
window.addEventListener(PLAY_CATALOG_REFRESH_EVENT, onCatalogRefresh);
|
||||
return () => window.removeEventListener(PLAY_CATALOG_REFRESH_EVENT, onCatalogRefresh);
|
||||
}, [clearPlaceTraceId, loadCatalog, t]);
|
||||
}, [invalidatePreview, loadCatalog, t]);
|
||||
|
||||
const openPlays = useMemo(() => {
|
||||
if (catalogState.kind !== "ok") return [];
|
||||
@@ -801,11 +815,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
|
||||
const drawNo = display?.draw_no ?? null;
|
||||
|
||||
if (riskStateDrawNo !== drawNo) {
|
||||
setRiskStateDrawNo(drawNo);
|
||||
setLiveSoldOutNumbers(new Set());
|
||||
setLiveWarningNumbers(new Set());
|
||||
}
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return;
|
||||
setLiveSoldOutNumbers(new Set());
|
||||
setLiveWarningNumbers(new Set());
|
||||
invalidatePreview();
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [currencyCode, drawNo, invalidatePreview]);
|
||||
|
||||
const alertRows = useMemo(
|
||||
() => display?.risk_pool_alerts ?? [],
|
||||
@@ -1169,9 +1191,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
if (evt.enabled === false && typeof evt.play_code === "string") {
|
||||
const removed = hasAmountsForPlay(evt.play_code);
|
||||
clearAmountsForPlay(evt.play_code);
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
invalidatePreview();
|
||||
toast.warning(
|
||||
removed
|
||||
? t("hall.playConfig.playClosedDraftCleared", {
|
||||
@@ -1185,9 +1205,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
};
|
||||
|
||||
const onOddsUpdate = (evt: OddsUpdateWsEvent) => {
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
invalidatePreview();
|
||||
toast.message(evt.message ?? t("hall.playConfig.oddsUpdated"));
|
||||
};
|
||||
|
||||
@@ -1207,6 +1225,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
next.delete(normalized);
|
||||
return next;
|
||||
});
|
||||
invalidatePreview();
|
||||
void reloadDraw();
|
||||
};
|
||||
|
||||
@@ -1234,7 +1253,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
channel.stopListening(".risk.sold_out");
|
||||
channel.stopListening(".risk.warning");
|
||||
};
|
||||
}, [clearAmountsForPlay, clearPlaceTraceId, drawNo, hasAmountsForPlay, reloadDraw, t]);
|
||||
}, [clearAmountsForPlay, drawNo, hasAmountsForPlay, invalidatePreview, reloadDraw, t]);
|
||||
|
||||
const collectDraftLineIssues = useCallback((): DraftLineIssue[] => {
|
||||
const issues: DraftLineIssue[] = [];
|
||||
@@ -1471,14 +1490,30 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
}
|
||||
|
||||
setPreviewLoading(true);
|
||||
const requestSeq = ++previewRequestSeqRef.current;
|
||||
const traceId = newPlaceTraceId();
|
||||
const drawId = display.draw_no;
|
||||
const previewCurrencyCode = currencyCode;
|
||||
const frozenLines = lines.map((line) => ({
|
||||
...line,
|
||||
provider_codes: line.provider_codes ? [...line.provider_codes] : undefined,
|
||||
}));
|
||||
placeTraceIdRef.current = traceId;
|
||||
try {
|
||||
placeTraceIdRef.current = newPlaceTraceId();
|
||||
const data = await postTicketPreview({
|
||||
draw_id: display.draw_no,
|
||||
currency_code: currencyCode,
|
||||
client_trace_id: placeTraceIdRef.current,
|
||||
lines,
|
||||
draw_id: drawId,
|
||||
currency_code: previewCurrencyCode,
|
||||
client_trace_id: traceId,
|
||||
lines: frozenLines,
|
||||
});
|
||||
if (requestSeq !== previewRequestSeqRef.current) return;
|
||||
previewSubmissionRef.current = {
|
||||
drawId: data.draw.draw_id.trim() || drawId,
|
||||
currencyCode: previewCurrencyCode,
|
||||
clientTraceId: traceId,
|
||||
lines: frozenLines,
|
||||
expectedConfigVersions: data.config_versions,
|
||||
};
|
||||
setPreviewData(data);
|
||||
setPreviewOpen(true);
|
||||
rows.forEach((row) => {
|
||||
@@ -1493,9 +1528,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
return;
|
||||
}
|
||||
if (e instanceof LotteryApiBizError && code === 2008) {
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
invalidatePreview();
|
||||
}
|
||||
if (e instanceof LotteryApiBizError && (code === 2001 || code === 2006)) {
|
||||
void reloadDraw();
|
||||
@@ -1507,7 +1540,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
};
|
||||
|
||||
const handlePlace = async () => {
|
||||
if (!display || !previewData) return;
|
||||
if (!previewData) return;
|
||||
if (placeLoading) {
|
||||
return;
|
||||
}
|
||||
@@ -1516,29 +1549,13 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
return;
|
||||
}
|
||||
|
||||
const lineIssues = collectDraftLineIssues();
|
||||
const missingProviderAt = providerMissingRow();
|
||||
if (missingProviderAt !== null) {
|
||||
toast.error(t("hall.providers.rowRequired", { row: missingProviderAt, defaultValue: `第 ${missingProviderAt} 行请选择至少一个开注商` }));
|
||||
return;
|
||||
}
|
||||
if (lineIssues.length > 0) {
|
||||
toast.error(formatDraftLineIssue(lineIssues[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = buildLines();
|
||||
if (lines.length === 0) {
|
||||
const snapshot = previewSubmissionRef.current;
|
||||
if (snapshot === null) {
|
||||
toast.error(t("hall.changedBeforeSubmit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const traceId = placeTraceIdRef.current ?? newPlaceTraceId();
|
||||
placeTraceIdRef.current = traceId;
|
||||
|
||||
const drawIdForBet =
|
||||
previewData.draw.draw_id.trim() || display.draw_no.trim();
|
||||
if (drawIdForBet === "") {
|
||||
if (snapshot.drawId === "") {
|
||||
toast.error(t("hall.notBettable"));
|
||||
return;
|
||||
}
|
||||
@@ -1546,12 +1563,13 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
setPlaceLoading(true);
|
||||
try {
|
||||
const data = await postTicketPlace({
|
||||
draw_id: drawIdForBet,
|
||||
currency_code: currencyCode,
|
||||
client_trace_id: traceId,
|
||||
lines,
|
||||
expected_config_versions: previewData.config_versions,
|
||||
draw_id: snapshot.drawId,
|
||||
currency_code: snapshot.currencyCode,
|
||||
client_trace_id: snapshot.clientTraceId,
|
||||
lines: snapshot.lines,
|
||||
expected_config_versions: snapshot.expectedConfigVersions,
|
||||
});
|
||||
previewSubmissionRef.current = null;
|
||||
clearPlaceTraceId();
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
@@ -1591,15 +1609,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
if (e instanceof LotteryApiBizError && code === 2002 && applyClosedPlayCleanup(e.data)) {
|
||||
const payload = e.data as ClosedPlayCleanupData;
|
||||
toast.error(payload.cleanup_hint ?? t("hall.ticketError.2002"));
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
invalidatePreview();
|
||||
return;
|
||||
}
|
||||
if (e instanceof LotteryApiBizError && (code === 2008 || code === 2009)) {
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
clearPlaceTraceId();
|
||||
invalidatePreview();
|
||||
}
|
||||
if (e instanceof LotteryApiBizError && (code === 2001 || code === 2006)) {
|
||||
void reloadDraw();
|
||||
@@ -2422,6 +2436,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
if (!open) {
|
||||
setPreviewData(null);
|
||||
if (!placeLoading) {
|
||||
previewRequestSeqRef.current += 1;
|
||||
previewSubmissionRef.current = null;
|
||||
clearPlaceTraceId();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user