修复财务冻结记录请求乱序与列表离页回写
This commit is contained in:
1 parent
1fe68cc8dd
commit
30415593a6
2 files changed
+184
-4
No files matched your search
@@ -13,6 +13,7 @@ definePageMeta({
|
|||||||
});
|
});
|
||||||
import { toDisplayString } from "vue";
|
import { toDisplayString } from "vue";
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
|
import { onScopeDispose } from "vue";
|
||||||
import { watch } from "vue";
|
import { watch } from "vue";
|
||||||
import { createBlock } from "vue";
|
import { createBlock } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
@@ -315,9 +316,11 @@ var je = {
|
|||||||
pt = ref(!1),
|
pt = ref(!1),
|
||||||
G = ref(``),
|
G = ref(``),
|
||||||
K = ref(`0.00`),
|
K = ref(`0.00`),
|
||||||
q = 0;
|
q = 0,
|
||||||
|
disposed = !1;
|
||||||
// 交易流水使用独立请求序号,快速筛选时忽略更早的结果。
|
// 交易流水使用独立请求序号,快速筛选时忽略更早的结果。
|
||||||
async function J() {
|
async function J() {
|
||||||
|
if (disposed) return;
|
||||||
let e = ++q;
|
let e = ++q;
|
||||||
((ut.value = !0), (U.value = ``));
|
((ut.value = !0), (U.value = ``));
|
||||||
try {
|
try {
|
||||||
@@ -341,6 +344,7 @@ var je = {
|
|||||||
let Y = 0;
|
let Y = 0;
|
||||||
// 余额流水与累计金额同步更新;失败时不保留旧金额冒充新结果。
|
// 余额流水与累计金额同步更新;失败时不保留旧金额冒充新结果。
|
||||||
async function X() {
|
async function X() {
|
||||||
|
if (disposed) return;
|
||||||
let e = ++Y;
|
let e = ++Y;
|
||||||
((pt.value = !0), (G.value = ``));
|
((pt.value = !0), (G.value = ``));
|
||||||
try {
|
try {
|
||||||
@@ -412,6 +416,7 @@ var je = {
|
|||||||
Ct = 0;
|
Ct = 0;
|
||||||
// 按服务端状态分页查询提现记录;本地关键词筛选仅作用于当前返回页。
|
// 按服务端状态分页查询提现记录;本地关键词筛选仅作用于当前返回页。
|
||||||
async function Q() {
|
async function Q() {
|
||||||
|
if (disposed) return;
|
||||||
let e = ++Ct;
|
let e = ++Ct;
|
||||||
((bt.value = !0), (xt.value = ``));
|
((bt.value = !0), (xt.value = ``));
|
||||||
try {
|
try {
|
||||||
@@ -442,28 +447,37 @@ var je = {
|
|||||||
Dt = ref(0),
|
Dt = ref(0),
|
||||||
Ot = ref(1),
|
Ot = ref(1),
|
||||||
kt = ref(!1),
|
kt = ref(!1),
|
||||||
$ = ref(``);
|
$ = ref(``),
|
||||||
// 冻结余额独立分页读取;异常时清空旧列表并显示错误。
|
freezeRequestVersion = 0;
|
||||||
|
// 冻结记录与其他流水遵守同一代次规则,旧请求不能改写结果、错误或加载状态。
|
||||||
async function At() {
|
async function At() {
|
||||||
|
if (disposed) return;
|
||||||
|
let requestVersion = ++freezeRequestVersion;
|
||||||
((kt.value = !0), ($.value = ``));
|
((kt.value = !0), ($.value = ``));
|
||||||
try {
|
try {
|
||||||
let e = await _.get(`/account/credit/freeze`, {
|
let e = await _.get(`/account/credit/freeze`, {
|
||||||
page: Ot.value,
|
page: Ot.value,
|
||||||
limit: T.value,
|
limit: T.value,
|
||||||
});
|
});
|
||||||
|
if (requestVersion !== freezeRequestVersion) return;
|
||||||
((Et.value = e.list ?? []), (Dt.value = e.count ?? 0));
|
((Et.value = e.list ?? []), (Dt.value = e.count ?? 0));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (requestVersion !== freezeRequestVersion) return;
|
||||||
(($.value = e instanceof Error ? e.message : `加载冻结记录失败`),
|
(($.value = e instanceof Error ? e.message : `加载冻结记录失败`),
|
||||||
(Et.value = []),
|
(Et.value = []),
|
||||||
(Dt.value = 0));
|
(Dt.value = 0));
|
||||||
} finally {
|
} finally {
|
||||||
kt.value = !1;
|
requestVersion === freezeRequestVersion && (kt.value = !1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function jt(e) {
|
function jt(e) {
|
||||||
((Ot.value = e.page + 1), (T.value = e.rows), At());
|
((Ot.value = e.page + 1), (T.value = e.rows), At());
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
// 与共用列表加载器一致:销毁后拒绝新读取,并作废各列表全部在途响应。
|
||||||
|
onScopeDispose(() => {
|
||||||
|
((disposed = !0), q++, Y++, Ct++, freezeRequestVersion++);
|
||||||
|
}),
|
||||||
watch(
|
watch(
|
||||||
() => x.query.tab,
|
() => x.query.tab,
|
||||||
(e) => {
|
(e) => {
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/** 冻结记录的响应乱序回归:只控制本地 HTTP 边界,页面与请求层使用生产实现。 */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
appOrigin,
|
||||||
|
backendOrigin,
|
||||||
|
eventually,
|
||||||
|
login,
|
||||||
|
scenario,
|
||||||
|
} from "./helpers/console-browser.mjs";
|
||||||
|
|
||||||
|
for (const outcome of ["late success", "late failure", "newer read pending"]) {
|
||||||
|
test(
|
||||||
|
`finance freeze records ignore an older response with ${outcome}`,
|
||||||
|
{ timeout: 35000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, { loginMode: "password-only" }, async (page) => {
|
||||||
|
let freezeReads = 0;
|
||||||
|
let releaseOlder;
|
||||||
|
let releaseNewer;
|
||||||
|
const olderHeld = new Promise((resolve) => {
|
||||||
|
releaseOlder = resolve;
|
||||||
|
});
|
||||||
|
const newerHeld = new Promise((resolve) => {
|
||||||
|
releaseNewer = resolve;
|
||||||
|
});
|
||||||
|
const freezePath = "/console/v1/account/credit/freeze";
|
||||||
|
const emptyPaths = [
|
||||||
|
"/console/v1/transaction",
|
||||||
|
"/console/v1/credit",
|
||||||
|
"/console/v1/withdraw",
|
||||||
|
"/console/v1/order",
|
||||||
|
"/console/v1/refund/pending/amount",
|
||||||
|
];
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
(url.pathname === freezePath || emptyPaths.includes(url.pathname)),
|
||||||
|
async (route) => {
|
||||||
|
const path = new URL(route.request().url()).pathname;
|
||||||
|
const fulfill = (data, status = 200) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: { "Access-Control-Allow-Origin": appOrigin },
|
||||||
|
body: JSON.stringify({
|
||||||
|
status,
|
||||||
|
msg: status === 200 ? "" : "旧冻结请求失败",
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (path === "/console/v1/refund/pending/amount")
|
||||||
|
return fulfill({ amount: "0.00" });
|
||||||
|
if (path !== freezePath)
|
||||||
|
return fulfill({ list: [], count: 0, total_amount: "0.00" });
|
||||||
|
const sequence = ++freezeReads;
|
||||||
|
if (sequence === 2) await olderHeld;
|
||||||
|
if (sequence === 3 && outcome === "newer read pending")
|
||||||
|
await newerHeld;
|
||||||
|
if (sequence === 2 && outcome === "late failure")
|
||||||
|
return fulfill({}, 400);
|
||||||
|
return fulfill({
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
amount: sequence === 2 ? "110.00" : "220.00",
|
||||||
|
client_notes:
|
||||||
|
sequence === 1
|
||||||
|
? "初始冻结快照"
|
||||||
|
: sequence === 2
|
||||||
|
? "较早冻结快照"
|
||||||
|
: "最新冻结快照",
|
||||||
|
nickname: "audit-user",
|
||||||
|
create_time: 1784619935,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await login(page, "/finance?tab=freeze");
|
||||||
|
await page.getByText("初始冻结快照", { exact: true }).waitFor();
|
||||||
|
const transaction = page.getByRole("tab", {
|
||||||
|
name: "交易记录",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const freeze = page.getByRole("tab", {
|
||||||
|
name: "冻结记录",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
await transaction.click();
|
||||||
|
await freeze.click();
|
||||||
|
await eventually(
|
||||||
|
() => freezeReads,
|
||||||
|
(value) => value === 2,
|
||||||
|
"Older freeze read is held at the HTTP boundary",
|
||||||
|
);
|
||||||
|
await transaction.click();
|
||||||
|
await freeze.click();
|
||||||
|
await eventually(
|
||||||
|
() => freezeReads,
|
||||||
|
(value) => value === 3,
|
||||||
|
"Newer freeze read has started",
|
||||||
|
);
|
||||||
|
const panel = page.getByRole("tabpanel").filter({ visible: true });
|
||||||
|
const loading = panel.locator(".p-datatable-mask");
|
||||||
|
if (outcome === "newer read pending")
|
||||||
|
await loading.waitFor({ state: "visible" });
|
||||||
|
else await page.getByText("最新冻结快照", { exact: true }).waitFor();
|
||||||
|
const olderResponse = page.waitForResponse(
|
||||||
|
(response) => new URL(response.url()).pathname === freezePath,
|
||||||
|
);
|
||||||
|
releaseOlder();
|
||||||
|
await (await olderResponse).finished();
|
||||||
|
// 等浏览器完成响应处理与绘制后再核对当前列表,不直接读取组件内部状态。
|
||||||
|
await page.evaluate(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (outcome === "newer read pending") {
|
||||||
|
// 已开始淡出的遮罩仍暂时可见;等待它自身的过渡结束再判断是否仍在加载。
|
||||||
|
await loading.evaluateAll((nodes) =>
|
||||||
|
Promise.all(
|
||||||
|
nodes.flatMap((node) =>
|
||||||
|
node
|
||||||
|
.getAnimations()
|
||||||
|
.map((animation) => animation.finished.catch(() => {})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await loading.isVisible(),
|
||||||
|
true,
|
||||||
|
"An older request must not finish the newer request's loading state",
|
||||||
|
);
|
||||||
|
releaseNewer();
|
||||||
|
await page.getByText("最新冻结快照", { exact: true }).waitFor();
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
await page.getByText("最新冻结快照", { exact: true }).isVisible(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await page.getByText("较早冻结快照", { exact: true }).isVisible(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(await panel.locator(".p-message-error").count(), 0);
|
||||||
|
await loading.waitFor({ state: "hidden" });
|
||||||
|
assert.equal(new URL(page.url()).searchParams.get("tab"), "freeze");
|
||||||
|
return {
|
||||||
|
olderResponse: outcome,
|
||||||
|
freezeReads,
|
||||||
|
finalSnapshot: "latest",
|
||||||
|
staleErrorVisible: false,
|
||||||
|
loadingBelongsToLatestRead: true,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
releaseOlder();
|
||||||
|
releaseNewer();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user