forked from lunanting/cangku
数据源:聚水潭「箱及仓位库存」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 卡、状态分段筛选、关键字搜索、目标天数、货位清单导入/清除、服务端导出。
135 lines
4.4 KiB
JavaScript
135 lines
4.4 KiB
JavaScript
'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;
|