fix: reject stale MKH backend responses
This commit is contained in:
parent
91e3d4040c
commit
ba6ef97a40
@ -18,6 +18,8 @@ npm run remote:dev
|
|||||||
|
|
||||||
首次启动时填写配置请求地址和部署 Key,系统自动写入被 Git 忽略的本机配置并重启后端。保存一次性显示的管理员账号和密码后直接登录,不需要在本地创建数据库或继续执行设置向导。前端默认访问 http://localhost:5173,API 默认代理到本地后端。
|
首次启动时填写配置请求地址和部署 Key,系统自动写入被 Git 忽略的本机配置并重启后端。保存一次性显示的管理员账号和密码后直接登录,不需要在本地创建数据库或继续执行设置向导。前端默认访问 http://localhost:5173,API 默认代理到本地后端。
|
||||||
|
|
||||||
|
拉取主仓库更新后必须停止旧的 `npm run remote:dev` 进程并重新启动,保证前后端使用同一版本。MKH 页的强制刷新带有版本协议校验;如果新前端仍代理到未重启的旧后端,页面会停止展示可能过期的库存并明确提示重启,不再将旧值冒充为“最新数据”。
|
||||||
|
|
||||||
## 验证与构建
|
## 验证与构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -2271,6 +2271,10 @@ async function getMkhBomList(query = {}) {
|
|||||||
saleWindowDays: SALE_WINDOW_DAYS,
|
saleWindowDays: SALE_WINDOW_DAYS,
|
||||||
dataMode: preferLiveData ? 'live' : 'snapshot',
|
dataMode: preferLiveData ? 'live' : 'snapshot',
|
||||||
refreshedAt: new Date().toISOString(),
|
refreshedAt: new Date().toISOString(),
|
||||||
|
// 前后端实时刷新协议。本地前端如果代理到未重启的旧后端,
|
||||||
|
// 将拒绝展示其返回的旧库存,而不是把缺失协议字段当成刷新成功。
|
||||||
|
freshDataContractVersion: 1,
|
||||||
|
freshDataApplied: forceFresh && operationalRefreshedAt > 0,
|
||||||
operationalRefreshedAt: operationalRefreshedAt
|
operationalRefreshedAt: operationalRefreshedAt
|
||||||
? new Date(operationalRefreshedAt).toISOString()
|
? new Date(operationalRefreshedAt).toISOString()
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
syncMkhBom,
|
syncMkhBom,
|
||||||
} from '../../api/mkhBom';
|
} from '../../api/mkhBom';
|
||||||
import { createRequestGate } from '../../utils/requestGate';
|
import { createRequestGate } from '../../utils/requestGate';
|
||||||
|
import { requireMkhFreshContract } from '../../utils/mkhFreshContract';
|
||||||
import { exportTableFile } from '../../utils/tableExport';
|
import { exportTableFile } from '../../utils/tableExport';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
@ -382,7 +383,14 @@ const MkhBomData = () => {
|
|||||||
setError(result?.error || '加载 MKH-BOM 资料失败');
|
setError(result?.error || '加载 MKH-BOM 资料失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData(result?.data || { rows: [], total: 0, page: 1, pageSize: 50 });
|
const snapshot = result?.data || null;
|
||||||
|
try {
|
||||||
|
requireMkhFreshContract(snapshot, { requireApplied: forceFresh });
|
||||||
|
} catch (contractError) {
|
||||||
|
setData({ rows: [], total: 0, page: 1, pageSize: 50, bomReady: false });
|
||||||
|
throw contractError;
|
||||||
|
}
|
||||||
|
setData(snapshot);
|
||||||
if (forceFresh) lastListRefreshAtRef.current = Date.now();
|
if (forceFresh) lastListRefreshAtRef.current = Date.now();
|
||||||
setError('');
|
setError('');
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
@ -908,7 +916,7 @@ const MkhBomData = () => {
|
|||||||
if (!result || result.success === false || !result?.data?.snapshot) {
|
if (!result || result.success === false || !result?.data?.snapshot) {
|
||||||
throw new Error(result?.error || '同步接口未返回数据库快照');
|
throw new Error(result?.error || '同步接口未返回数据库快照');
|
||||||
}
|
}
|
||||||
setData(result.data.snapshot);
|
setData(requireMkhFreshContract(result.data.snapshot));
|
||||||
setError('');
|
setError('');
|
||||||
message.success('已实时读取最新订单、库存、BOM 与仓位数据');
|
message.success('已实时读取最新订单、库存、BOM 与仓位数据');
|
||||||
} catch (syncError) {
|
} catch (syncError) {
|
||||||
|
|||||||
15
frontend/src/utils/mkhFreshContract.js
Normal file
15
frontend/src/utils/mkhFreshContract.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const MKH_FRESH_DATA_CONTRACT_VERSION = 1;
|
||||||
|
|
||||||
|
const OUTDATED_BACKEND_MESSAGE = '本地后端版本过旧或更新后未重启,已停止展示可能过期的库存。'
|
||||||
|
+ '请停止当前 npm run remote:dev,拉取最新 main 后重新启动。';
|
||||||
|
|
||||||
|
export function requireMkhFreshContract(snapshot, { requireApplied = false } = {}) {
|
||||||
|
const contractVersion = Number(snapshot?.freshDataContractVersion) || 0;
|
||||||
|
if (contractVersion < MKH_FRESH_DATA_CONTRACT_VERSION) {
|
||||||
|
throw new Error(OUTDATED_BACKEND_MESSAGE);
|
||||||
|
}
|
||||||
|
if (requireApplied && snapshot?.freshDataApplied !== true) {
|
||||||
|
throw new Error('后端未确认已重读最新运营数据,已停止展示可能过期的库存,请重试。');
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
23
frontend/test/mkhFreshContract.test.mjs
Normal file
23
frontend/test/mkhFreshContract.test.mjs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { requireMkhFreshContract } from '../src/utils/mkhFreshContract.js';
|
||||||
|
|
||||||
|
test('MKH fresh contract accepts a backend-confirmed live refresh', () => {
|
||||||
|
const snapshot = { freshDataContractVersion: 1, freshDataApplied: true };
|
||||||
|
assert.equal(requireMkhFreshContract(snapshot, { requireApplied: true }), snapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MKH fresh contract rejects an outdated local backend instead of showing stale stock', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => requireMkhFreshContract({ rows: [{ stock: 4 }] }, { requireApplied: true }),
|
||||||
|
/本地后端版本过旧/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MKH fresh contract requires explicit refresh completion for fresh requests', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => requireMkhFreshContract({ freshDataContractVersion: 1 }, { requireApplied: true }),
|
||||||
|
/未确认已重读/
|
||||||
|
);
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user