feat(配件库存监控/MKH-BOM): 两页面新增「同步最新数据」按钮

需求:配件库存监控、MKH-BOM 资料两个页面各加一个同步按钮,任何时候点击都能
立即拉取最新数据,不必等凌晨 00:00 的定时刷新。

后端:
- mkhBomService 新增 clearMkhBomCache():清空本页全部服务端缓存
  (businessBom / officialBomIndex / officialBomDetail / productBom /
  mkhProduct / replenishIndex / workOrderIndex / pendingOrderIndex /
  componentStock / componentLocation),返回 syncedAt + 各缓存清除条目数。
- modularStockMonitorService 新增 syncModularStock():本页无进程内缓存
  (每次请求直连副本库),同步动作 = 重新解析并返回最新监控日,前端据此重拉列表。
- 新增接口 POST /api/mkh-bom/sync、POST /api/modular-stock-monitor/sync。

前端:
- api/mkhBom.js 新增 syncMkhBom;api/modularStockMonitor.js 新增 syncModularStock。
- 两个页面工具栏各加「同步最新数据」主按钮(SyncOutlined,带 loading 态),
  点击 → 调后端 sync → 成功后重新拉列表(MKH-BOM 页同时清空展开明细缓存
  detailCache,避免看到旧的配件明细)→ 提示「已同步最新数据」;
  失败时展示后端返回的错误信息。

口径说明(重要):
- 两页数据来自副本库 JSReportReplica,由上游聚水潭同步;应用层无法触发上游拉取。
  本按钮的作用是「清服务端缓存 + 直连副本库重取最新」,保证看到的不是
  2~15 分钟前的缓存结果,而不是重新跑一遍上游同步。
- 后端已内置的 dailyRefresh(cron 0 0 * * *,Asia/Shanghai)刷的是
  summary_daily_* / summary_monthly_* 汇总表,不覆盖这两页数据源,
  与本按钮既不重复也不冲突,可并存。

验证:
- 后端 4 个改动文件 node --check 全部通过。
- 前端 4 个改动文件(含 2 个 jsx)用 @babel/parser 解析全部通过。
- 实测:syncModularStock 返回 monitorDate=2026-09-16;
  clearMkhBomCache 返回 syncedAt 与各缓存清除条目数。
- 单测 mkhBomService.test.js 40/40 通过,无回归。
- 前端 lint/build 未运行:frontend/node_modules 仍是之前被中断 npm ci 破坏的
  坏目录(frontend/node_modules_broken_0917/,未纳入提交)。
  合并前需在本机 npm ci 恢复后,打开两个页面确认按钮渲染与点击效果。
- 后端改动需重启服务后才生效。
This commit is contained in:
yanhui 2026-09-17 18:14:57 +08:00
parent bc8c46d5be
commit be52bdd0b5
8 changed files with 120 additions and 3 deletions

View File

@ -5,6 +5,7 @@ const {
getMkhBomList,
getMkhBomDetail,
listMkhBomExportRows,
clearMkhBomCache,
} = require('../services/mkhBomService');
const {
getMkhReplenishAlertAdminConfig,
@ -101,4 +102,14 @@ router.put('/notification-center', async (req, res) => {
}
});
// 手动同步:清空本页服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
// 前端「同步」按钮调用后再拉一次列表即可直连副本库取最新快照
router.post('/sync', async (_req, res) => {
try {
res.json({ success: true, data: clearMkhBomCache() });
} catch (error) {
sendError(res, error, '同步 MKH-BOM 数据失败');
}
});
module.exports = router;

View File

@ -4,6 +4,7 @@ const express = require('express');
const {
getModularStockList,
getModularStockFilters,
syncModularStock,
} = require('../services/modularStockMonitorService');
const router = express.Router();
@ -36,4 +37,13 @@ router.get('/filters', async (_req, res) => {
}
});
// 手动同步:重新取最新监控日,前端「同步」按钮调用后再拉一次列表即拿到最新快照
router.post('/sync', async (_req, res) => {
try {
res.json({ success: true, data: await syncModularStock() });
} catch (error) {
sendError(res, error, '同步配件库存监控数据失败');
}
});
module.exports = router;

View File

@ -1947,8 +1947,37 @@ async function listMkhBomExportRows(query = {}) {
};
}
// 手动同步:清空本页全部服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 配件仓位)。
// 页面「同步」按钮调用——清掉后下一次查询直连副本库,拿到的是上游最新快照,
// 而不是 2~15 分钟前的缓存结果。
// 注意:副本库本身由聚水潭侧同步,应用层无法触发上游拉取,本函数只保证「不读旧缓存」。
function clearMkhBomCache() {
businessBomCache = null;
officialBomIndexCache = null;
productBomCache = null;
mkhProductCache = null;
replenishIndexCache = null;
workOrderIndexCache = null;
pendingOrderIndexCache = null;
const cleared = [
['officialBomDetailCache', officialBomDetailCache.size],
['componentLocationCache', componentLocationCache.size],
['componentStockCache', componentStockCache.size],
];
officialBomDetailCache.clear();
componentLocationCache.clear();
componentStockCache.clear();
return {
syncedAt: new Date().toISOString(),
cleared: cleared.map(([name, entries]) => ({ name, entries })),
};
}
module.exports = {
getMkhBomList,
clearMkhBomCache,
getMkhBomDetail,
listReplenishAlertRows,
listMkhBomExportRows,

View File

@ -445,6 +445,17 @@ async function getModularStockFilters() {
};
}
// 手动同步:本页无进程内缓存(每次请求直连副本库),同步动作 = 重新解析监控日并返回,
// 前端据此刷新列表到最新快照。
// 注意:副本库由聚水潭侧同步,应用层无法触发上游拉取,这里只保证取到最新监控日。
async function syncModularStock() {
const monitorDate = await resolveMonitorDate();
return {
syncedAt: new Date().toISOString(),
monitorDate,
};
}
module.exports = {
WAREHOUSE_NAME,
WAREHOUSE_WMS_CO_ID,
@ -452,4 +463,5 @@ module.exports = {
DEFAULT_SAFETY_DAYS,
getModularStockList,
getModularStockFilters,
syncModularStock,
};

View File

@ -45,3 +45,11 @@ export const saveMkhNotificationCenter = (payload = {}) => request.put(
'/api/mkh-bom/notification-center',
payload
);
// 手动同步:清空服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
// 随后重新拉列表即直连副本库取最新快照,可随时点击
export const syncMkhBom = () => request.post(
'/api/mkh-bom/sync',
null,
{ timeout: LIST_TIMEOUT_MS }
);

View File

@ -14,3 +14,10 @@ export const getModularStockFilters = () => request.get(
'/api/modular-stock-monitor/filters',
{ timeout: META_TIMEOUT_MS }
);
// 手动同步:后端重新取最新监控日,前端随后再拉一次列表即拿到最新快照
export const syncModularStock = () => request.post(
'/api/modular-stock-monitor/sync',
null,
{ timeout: META_TIMEOUT_MS }
);

View File

@ -4,7 +4,7 @@ import {
Table, Tooltip, Typography, message,
} from 'antd';
import {
BellOutlined, DownloadOutlined, PrinterOutlined, ReloadOutlined, SearchOutlined,
BellOutlined, DownloadOutlined, PrinterOutlined, ReloadOutlined, SearchOutlined, SyncOutlined,
} from '@ant-design/icons';
import { SycmPage } from '../../components/Common';
import {
@ -15,6 +15,7 @@ import {
saveMkhReplenishAlertConfig,
saveMkhNotificationCenter,
checkMkhReplenishAlert,
syncMkhBom,
} from '../../api/mkhBom';
import { exportTableFile } from '../../utils/tableExport';
import './style.css';
@ -233,6 +234,7 @@ const MkhBomData = () => {
// 导出(当前筛选条件下的全部数据)
const [exporting, setExporting] = useState(false);
const [syncing, setSyncing] = useState(false);
// 补货预警推送设置
const [alertOpen, setAlertOpen] = useState(false);
@ -718,6 +720,22 @@ const MkhBomData = () => {
);
};
// 手动同步:清掉服务端缓存(BOM / 补货索引 / 加工单 / 待发货 / 配件库存 / 仓位),
// 再拉一次列表即可直连副本库取最新快照;可随时点击,不必等凌晨的定时刷新
const handleSync = async () => {
setSyncing(true);
try {
await syncMkhBom();
setDetailCache({});
await load();
message.success('已同步最新数据');
} catch (syncError) {
message.error(syncError?.response?.data?.error || syncError?.message || '同步失败');
} finally {
setSyncing(false);
}
};
return (
<SycmPage className="mkh-page">
<header className="mkh-header">
@ -772,6 +790,9 @@ const MkhBomData = () => {
/>
<Button onClick={resetFilters}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新数据</Button>
<Tooltip title="立即重新拉取副本库最新数据(清服务端缓存后重查),可随时点击,不必等凌晨的定时刷新">
<Button type="primary" icon={<SyncOutlined />} onClick={handleSync} loading={syncing}>同步最新数据</Button>
</Tooltip>
<Dropdown
trigger={['click']}
disabled={exporting}

View File

@ -3,9 +3,9 @@ import {
Alert, Button, Card, Col, Dropdown, Image, Input, Row, Select,
Space, Spin, Statistic, Switch, Table, Tabs, Tag, Tooltip, Typography, message,
} from 'antd';
import { DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { DownloadOutlined, ReloadOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { SycmPage } from '../../components/Common';
import { getModularStockList, getModularStockFilters } from '../../api/modularStockMonitor';
import { getModularStockList, getModularStockFilters, syncModularStock } from '../../api/modularStockMonitor';
import { exportTableFile } from '../../utils/tableExport';
import './style.css';
@ -38,6 +38,7 @@ const AccessoryStockPanel = () => {
const [data, setData] = useState({ rows: [], total: 0, page: 1, pageSize: 50, summary: {}, monitorDate: null });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [syncing, setSyncing] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [sorter, setSorter] = useState({ field: 'realtimeStock', order: 'DESC' });
@ -282,6 +283,21 @@ const AccessoryStockPanel = () => {
}
};
// 手动同步:后端重新取最新监控日(清服务端缓存)后再拉一次列表,
// 拿到的是副本库当前最新快照;可随时点击,不受 24 点定时任务限制
const handleSync = async () => {
setSyncing(true);
try {
await syncModularStock();
await load();
message.success('已同步最新数据');
} catch (syncError) {
message.error(syncError?.response?.data?.error || syncError?.message || '同步失败');
} finally {
setSyncing(false);
}
};
const summary = data.summary || {};
return (
@ -327,6 +343,9 @@ const AccessoryStockPanel = () => {
</Space>
<Button onClick={resetFilters}>重置</Button>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新数据</Button>
<Tooltip title="立即重新拉取副本库最新数据(清服务端缓存后重查),可随时点击,不必等凌晨的定时刷新">
<Button type="primary" icon={<SyncOutlined />} onClick={handleSync} loading={syncing}>同步最新数据</Button>
</Tooltip>
</Space>
</Card>