fix: reject stale MKH backend responses

This commit is contained in:
134 2026-09-24 14:02:24 +08:00
parent 91e3d4040c
commit ba6ef97a40
5 changed files with 54 additions and 2 deletions

View File

@ -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

View File

@ -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,

View File

@ -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) {

View 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;
}

View 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 }),
/未确认已重读/
);
});