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