cangku/backend/src/api/binReplenish.js
lunanting 55b755c1d9 feat: 新增「缺货补货监控」看板(拣货位缺货 → 从存货位补货)
数据源:聚水潭「箱及仓位库存」rpt.jst_packInventoryInfo(副本里是视图,
按 SKU+货位聚合单次 35~45s,因此服务端缓存 10 分钟 + 启动预热)。

口径:
- 拣货位:按业务导入的货位清单判定(本地表 bin_replenish_pick_bin,按仓库隔离);
  副本没有货位类型主数据,未导入时回退为 pack_type='Bin' 的记录,页面明确标注。
- 拣货位库存 / 存货位库存 / 现有库存合计;
- 目标库存 = max(10, 近15天日销 × 目标天数(默认7));
- 建议补货量 = min(存货位库存, 目标 − 拣货位库存);
- 状态:拣货位缺货 / 无货可补 / 拣货位偏低 / 未设拣货位 / 正常。
  「未设拣货位」单列,避免把整箱直发商品误报成缺货。

后端:binReplenishService + binReplenishSchema + /api/bin-replenish
(list / export / pick-bins GET-POST-DELETE / ping),建表与预热挂在 localSchema。
前端:/planning/operation/bin-replenish-monitor 页面、侧栏入口、页面权限注册,
含 KPI 卡、状态分段筛选、关键字搜索、目标天数、货位清单导入/清除、服务端导出。
2026-10-22 11:08:20 +08:00

135 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const express = require('express');
const multer = require('multer');
const {
clearPickBins,
getPickBinMeta,
getReplenishExportRows,
getReplenishList,
importPickBins,
resolveWarehouseKey,
} = require('../services/binReplenishService');
const { sendWorkbookDownload } = require('../services/exportWorkbook');
const router = express.Router();
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 20 * 1024 * 1024 },
});
const EXPORT_COLUMNS = [
{ key: 'skuId', title: '商品编码' },
{ key: 'productName', title: '品名' },
{ key: 'supplier', title: '供应商' },
{ key: 'statusLabel', title: '状态' },
{ key: 'pickQty', title: '拣货位库存' },
{ key: 'storageQty', title: '存货位库存' },
{ key: 'totalQty', title: '现有库存合计' },
{ key: 'targetQty', title: '目标拣货位库存' },
{ key: 'suggestQty', title: '建议补货量' },
{ key: 'avgSale', title: '近15天日销' },
{ key: 'pickBinText', title: '拣货位(货位号/数量)' },
{ key: 'storageBinText', title: '存货位(前5个/数量)' },
];
/** GET /api/bin-replenish/list —— 缺货补货监控列表 */
router.get('/list', async (req, res, next) => {
try {
const payload = await getReplenishList({
warehouse: req.query.warehouse,
keyword: req.query.keyword,
status: req.query.status,
page: req.query.page,
pageSize: req.query.pageSize,
targetDays: req.query.targetDays,
refresh: req.query.refresh,
});
res.json({ success: true, data: payload });
} catch (error) {
next(error);
}
});
/** GET /api/bin-replenish/export —— 服务端全量导出 */
router.get('/export', async (req, res, next) => {
try {
const format = String(req.query.format || 'xlsx').toLowerCase() === 'csv' ? 'csv' : 'xlsx';
const payload = await getReplenishExportRows({
warehouse: req.query.warehouse,
keyword: req.query.keyword,
status: req.query.status,
targetDays: req.query.targetDays,
});
const rows = payload.rows.map((row) => ({
...row,
pickBinText: (row.pickBins || []).map((item) => `${item.bin}(${item.qty})`).join('、'),
storageBinText: (row.storageBins || []).map((item) => `${item.bin}(${item.qty})`).join('、'),
}));
sendWorkbookDownload(res, {
fileName: `${payload.warehouse.shortName}缺货补货监控_${new Date().toISOString().slice(0, 10)}`,
format,
sheets: [{
name: '缺货补货监控',
header: EXPORT_COLUMNS.map((column) => column.title),
rows: rows.map((row) => EXPORT_COLUMNS.map((column) => row[column.key])),
}],
});
} catch (error) {
next(error);
}
});
/** GET /api/bin-replenish/pick-bins —— 当前仓库的拣货位清单概况 */
router.get('/pick-bins', async (req, res, next) => {
try {
const meta = await getPickBinMeta(resolveWarehouseKey(req.query.warehouse));
res.json({ success: true, data: meta });
} catch (error) {
next(error);
}
});
/** POST /api/bin-replenish/pick-bins —— 导入拣货位清单(xlsx / xls / csv / txt) */
router.post('/pick-bins', upload.single('file'), async (req, res, next) => {
try {
if (!req.file || !req.file.buffer) {
res.status(400).json({ success: false, message: '请选择要导入的货位清单文件(xlsx / xls / csv)' });
return;
}
// multer 按 latin1 解析 multipart 文件名,中文会乱码,这里转回 UTF-8
const rawName = req.file.originalname || '';
const fileName = /[\u00c0-\u00ff]/.test(rawName)
? Buffer.from(rawName, 'latin1').toString('utf8')
: rawName;
const result = await importPickBins({
buffer: req.file.buffer,
fileName,
warehouseKey: req.body?.warehouse || req.query.warehouse,
importedBy: req.user?.username || req.user?.name || null,
});
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
});
/** DELETE /api/bin-replenish/pick-bins —— 清除货位清单,回到回退口径 */
router.delete('/pick-bins', async (req, res, next) => {
try {
const result = await clearPickBins(req.query.warehouse);
res.json({ success: true, data: result });
} catch (error) {
next(error);
}
});
/** GET /api/bin-replenish/ping —— 路由存活探针(401 也代表已注册) */
router.get('/ping', (req, res) => {
res.json({ success: true, data: { ok: true } });
});
module.exports = router;