diff --git a/README.md b/README.md index c51db3c..c36d8af 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ npm run remote:dev 首次启动时填写配置请求地址和部署 Key,系统自动写入被 Git 忽略的本机配置并重启后端。保存一次性显示的管理员账号和密码后直接登录,不需要在本地创建数据库或继续执行设置向导。前端默认访问 http://localhost:5173,API 默认代理到本地后端。 +拉取主仓库更新后必须停止旧的 `npm run remote:dev` 进程并重新启动,保证前后端使用同一版本。MKH 页的强制刷新带有版本协议校验;如果新前端仍代理到未重启的旧后端,页面会停止展示可能过期的库存并明确提示重启,不再将旧值冒充为“最新数据”。 + ## 验证与构建 ```bash diff --git a/backend/src/services/mkhBomService.js b/backend/src/services/mkhBomService.js index 3ea8996..f585725 100644 --- a/backend/src/services/mkhBomService.js +++ b/backend/src/services/mkhBomService.js @@ -2271,6 +2271,10 @@ async function getMkhBomList(query = {}) { saleWindowDays: SALE_WINDOW_DAYS, dataMode: preferLiveData ? 'live' : 'snapshot', refreshedAt: new Date().toISOString(), + // 前后端实时刷新协议。本地前端如果代理到未重启的旧后端, + // 将拒绝展示其返回的旧库存,而不是把缺失协议字段当成刷新成功。 + freshDataContractVersion: 1, + freshDataApplied: forceFresh && operationalRefreshedAt > 0, operationalRefreshedAt: operationalRefreshedAt ? new Date(operationalRefreshedAt).toISOString() : null, diff --git a/frontend/src/pages/MkhBomData/index.jsx b/frontend/src/pages/MkhBomData/index.jsx index 46ebc7a..56e7043 100644 --- a/frontend/src/pages/MkhBomData/index.jsx +++ b/frontend/src/pages/MkhBomData/index.jsx @@ -18,6 +18,7 @@ import { syncMkhBom, } from '../../api/mkhBom'; import { createRequestGate } from '../../utils/requestGate'; +import { requireMkhFreshContract } from '../../utils/mkhFreshContract'; import { exportTableFile } from '../../utils/tableExport'; import './style.css'; @@ -382,7 +383,14 @@ const MkhBomData = () => { setError(result?.error || '加载 MKH-BOM 资料失败'); 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(); setError(''); } catch (requestError) { @@ -908,7 +916,7 @@ const MkhBomData = () => { if (!result || result.success === false || !result?.data?.snapshot) { throw new Error(result?.error || '同步接口未返回数据库快照'); } - setData(result.data.snapshot); + setData(requireMkhFreshContract(result.data.snapshot)); setError(''); message.success('已实时读取最新订单、库存、BOM 与仓位数据'); } catch (syncError) { diff --git a/frontend/src/utils/mkhFreshContract.js b/frontend/src/utils/mkhFreshContract.js new file mode 100644 index 0000000..2ba828d --- /dev/null +++ b/frontend/src/utils/mkhFreshContract.js @@ -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; +} diff --git a/frontend/test/mkhFreshContract.test.mjs b/frontend/test/mkhFreshContract.test.mjs new file mode 100644 index 0000000..261f79d --- /dev/null +++ b/frontend/test/mkhFreshContract.test.mjs @@ -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 }), + /未确认已重读/ + ); +});