fix: 根治MKH同步后旧请求覆盖最新数据
This commit is contained in:
parent
97a93766e8
commit
fa91b9242c
@ -32,6 +32,7 @@ function sendError(res, error, fallback) {
|
||||
// MKH-BOM 资料列表(模块化组合商品 + 配件汇总)
|
||||
router.get('/list', async (req, res) => {
|
||||
try {
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.json({ success: true, data: await getMkhBomList(req.query || {}) });
|
||||
} catch (error) {
|
||||
sendError(res, error, '加载 MKH-BOM 资料列表失败');
|
||||
@ -42,6 +43,7 @@ router.get('/list', async (req, res) => {
|
||||
router.get('/detail', async (req, res) => {
|
||||
try {
|
||||
const skuId = req.query?.skuId || req.query?.comboSkuId;
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.json({ success: true, data: await getMkhBomDetail(skuId) });
|
||||
} catch (error) {
|
||||
sendError(res, error, '加载 MKH-BOM 明细失败');
|
||||
@ -102,11 +104,14 @@ router.put('/notification-center', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 手动同步:清空本页服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
|
||||
// 前端「同步」按钮调用后再拉一次列表即可直连副本库取最新快照
|
||||
router.post('/sync', async (_req, res) => {
|
||||
// 手动同步:先清空本页服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
|
||||
// 再在同一次请求中直连副本库读取新快照。这样不会被同步前尚未完成的旧列表请求覆盖。
|
||||
router.post('/sync', async (req, res) => {
|
||||
try {
|
||||
res.json({ success: true, data: clearMkhBomCache() });
|
||||
const cache = clearMkhBomCache();
|
||||
const snapshot = await getMkhBomList(req.body?.query || {});
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.json({ success: true, data: { cache, snapshot } });
|
||||
} catch (error) {
|
||||
sendError(res, error, '同步 MKH-BOM 数据失败');
|
||||
}
|
||||
|
||||
@ -7,7 +7,11 @@ const EXPORT_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
// MKH-BOM 资料列表(模块化组合商品 + 配件汇总)
|
||||
export const getMkhBomList = (params = {}) => request.get(
|
||||
'/api/mkh-bom/list',
|
||||
{ params, timeout: LIST_TIMEOUT_MS }
|
||||
{
|
||||
params,
|
||||
timeout: LIST_TIMEOUT_MS,
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
}
|
||||
);
|
||||
|
||||
// 单个 MKH 组合商品的 BOM 配件明细(列表展开 / 打印预览共用)
|
||||
@ -46,10 +50,9 @@ export const saveMkhNotificationCenter = (payload = {}) => request.put(
|
||||
payload
|
||||
);
|
||||
|
||||
// 手动同步:清空服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
|
||||
// 随后重新拉列表即直连副本库取最新快照,可随时点击
|
||||
export const syncMkhBom = () => request.post(
|
||||
// 手动同步:服务端清空全部 MKH 缓存,并在同一次请求中返回当前筛选条件的数据库新快照
|
||||
export const syncMkhBom = (query = {}) => request.post(
|
||||
'/api/mkh-bom/sync',
|
||||
null,
|
||||
{ query },
|
||||
{ timeout: LIST_TIMEOUT_MS }
|
||||
);
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
checkMkhReplenishAlert,
|
||||
syncMkhBom,
|
||||
} from '../../api/mkhBom';
|
||||
import { createRequestGate } from '../../utils/requestGate';
|
||||
import { exportTableFile } from '../../utils/tableExport';
|
||||
import './style.css';
|
||||
|
||||
@ -273,6 +274,8 @@ const MkhBomData = () => {
|
||||
const [centerSaving, setCenterSaving] = useState(false);
|
||||
|
||||
const tableWrapRef = useRef(null);
|
||||
const listRequestGateRef = useRef(createRequestGate());
|
||||
const detailGenerationRef = useRef(0);
|
||||
const [bodyHeight, setBodyHeight] = useState(420);
|
||||
|
||||
useEffect(() => {
|
||||
@ -310,19 +313,22 @@ const MkhBomData = () => {
|
||||
}, [page, pageSize, sorter, filters]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const requestGeneration = listRequestGateRef.current.begin();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = await getMkhBomList(query);
|
||||
const result = await getMkhBomList({ ...query, _refresh: Date.now() });
|
||||
if (!listRequestGateRef.current.isCurrent(requestGeneration)) return;
|
||||
if (!result || result.success === false) {
|
||||
setError(result?.error || '加载 MKH-BOM 资料失败');
|
||||
return;
|
||||
}
|
||||
setData(result?.data || { rows: [], total: 0, page: 1, pageSize: 50 });
|
||||
} catch (requestError) {
|
||||
if (!listRequestGateRef.current.isCurrent(requestGeneration)) return;
|
||||
setError(requestError?.response?.data?.error || requestError?.message || '加载 MKH-BOM 资料失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (listRequestGateRef.current.isCurrent(requestGeneration)) setLoading(false);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
@ -482,18 +488,23 @@ const MkhBomData = () => {
|
||||
|
||||
const loadDetail = useCallback(async (skuId) => {
|
||||
if (detailCache[skuId] || detailLoading[skuId]) return;
|
||||
const detailGeneration = detailGenerationRef.current;
|
||||
setDetailLoading((old) => ({ ...old, [skuId]: true }));
|
||||
try {
|
||||
const result = await getMkhBomDetail(skuId);
|
||||
if (detailGeneration !== detailGenerationRef.current) return;
|
||||
if (!result || result.success === false) {
|
||||
message.error(result?.error || '加载配件明细失败');
|
||||
return;
|
||||
}
|
||||
setDetailCache((old) => ({ ...old, [skuId]: result?.data || { items: [] } }));
|
||||
} catch (e) {
|
||||
if (detailGeneration !== detailGenerationRef.current) return;
|
||||
message.error(e?.response?.data?.error || e?.message || '加载配件明细失败');
|
||||
} finally {
|
||||
setDetailLoading((old) => ({ ...old, [skuId]: false }));
|
||||
if (detailGeneration === detailGenerationRef.current) {
|
||||
setDetailLoading((old) => ({ ...old, [skuId]: false }));
|
||||
}
|
||||
}
|
||||
}, [detailCache, detailLoading]);
|
||||
|
||||
@ -791,18 +802,35 @@ const MkhBomData = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 手动同步:清掉服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
|
||||
// 再拉一次列表即可直连副本库取最新快照;可随时点击,不必等凌晨的定时刷新
|
||||
// 手动同步:服务端清缓存后在同一次请求中直连副本库返回新快照。
|
||||
// 请求代际会阻止同步前尚未完成的旧请求覆盖这个新快照。
|
||||
const handleSync = async () => {
|
||||
setSyncing(true);
|
||||
setLoading(true);
|
||||
// 立即废弃同步前发出的列表/明细请求;它们即使稍后返回,也不能覆盖新快照。
|
||||
listRequestGateRef.current.invalidate();
|
||||
detailGenerationRef.current += 1;
|
||||
setDetailCache({});
|
||||
setDetailLoading({});
|
||||
setExpandedKeys({});
|
||||
const syncGeneration = listRequestGateRef.current.begin();
|
||||
try {
|
||||
await syncMkhBom();
|
||||
setDetailCache({});
|
||||
await load();
|
||||
message.success('已同步最新数据');
|
||||
const result = await syncMkhBom(query);
|
||||
if (!listRequestGateRef.current.isCurrent(syncGeneration)) return;
|
||||
if (!result || result.success === false || !result?.data?.snapshot) {
|
||||
throw new Error(result?.error || '同步接口未返回数据库快照');
|
||||
}
|
||||
setData(result.data.snapshot);
|
||||
setError('');
|
||||
const monitorDate = result.data.snapshot.monitorDate;
|
||||
message.success(monitorDate ? `已读取数据库最新快照(监控日 ${monitorDate})` : '已读取数据库最新快照');
|
||||
} catch (syncError) {
|
||||
if (listRequestGateRef.current.isCurrent(syncGeneration)) {
|
||||
setError(syncError?.response?.data?.error || syncError?.message || '同步失败');
|
||||
}
|
||||
message.error(syncError?.response?.data?.error || syncError?.message || '同步失败');
|
||||
} finally {
|
||||
if (listRequestGateRef.current.isCurrent(syncGeneration)) setLoading(false);
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
17
frontend/src/utils/requestGate.js
Normal file
17
frontend/src/utils/requestGate.js
Normal file
@ -0,0 +1,17 @@
|
||||
export function createRequestGate() {
|
||||
let generation = 0;
|
||||
|
||||
return {
|
||||
begin() {
|
||||
generation += 1;
|
||||
return generation;
|
||||
},
|
||||
invalidate() {
|
||||
generation += 1;
|
||||
return generation;
|
||||
},
|
||||
isCurrent(requestGeneration) {
|
||||
return requestGeneration === generation;
|
||||
},
|
||||
};
|
||||
}
|
||||
24
frontend/test/requestGate.test.mjs
Normal file
24
frontend/test/requestGate.test.mjs
Normal file
@ -0,0 +1,24 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createRequestGate } from '../src/utils/requestGate.js';
|
||||
|
||||
test('only the newest request may update the page', () => {
|
||||
const gate = createRequestGate();
|
||||
const oldRequest = gate.begin();
|
||||
const newRequest = gate.begin();
|
||||
|
||||
assert.equal(gate.isCurrent(oldRequest), false);
|
||||
assert.equal(gate.isCurrent(newRequest), true);
|
||||
});
|
||||
|
||||
test('sync invalidates every request that started before cache clearing', () => {
|
||||
const gate = createRequestGate();
|
||||
const beforeSync = gate.begin();
|
||||
|
||||
gate.invalidate();
|
||||
|
||||
assert.equal(gate.isCurrent(beforeSync), false);
|
||||
const freshSnapshot = gate.begin();
|
||||
assert.equal(gate.isCurrent(freshSnapshot), true);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user