功能: - 退货库存预警:横林二仓/苏北/睢宁/山东青岗集/河南洛宁 五张看板, 按各仓客退上架仓对应的自定义库存位取数(各仓位不同:横林/苏北/睢宁 c1,山东 c2) - 支持导入聚水潭「箱及仓位库存」导出文件覆盖创建时间、件数、供应商 (副本只有仓库级数量,没有箱级维度,真实上架时间只能靠导入) - 退货入库核销与每日库存监控:状态落本地系统库,含定时任务与差异清单 - 服务端导出(xlsx/csv),不受列表分页与展示上限约束 稳定性: - 前端幂等请求在后端重启窗口内自动退避重连,不再直接抛 500 - 后端补齐统一 JSON 错误兜底与 /api JSON 404,异常不再返回 HTML - 隧道 ssh 改为绝对路径解析并隐藏窗口,避免非交互式启动 ENOENT 与黑窗弹窗 - 业务库 DATETIME 按北京时间解析(useUTC: false),修复时间显示整体 +8 小时 合并:与上游「隧道断线自愈 + 配置负缓存」为同一问题的两份实现, 本分支采用上游方案(带 TTL 的负缓存 + 统一失效函数 + 单测)。
133 lines
4.6 KiB
JavaScript
133 lines
4.6 KiB
JavaScript
'use strict';
|
||
|
||
const express = require('express');
|
||
const multer = require('multer');
|
||
const {
|
||
buildExportColumns,
|
||
getReturnStockWarningExportRows,
|
||
getReturnStockWarningList,
|
||
resolveWarehouse,
|
||
resolveWarehouseKey,
|
||
} = require('../services/returnStockWarningService');
|
||
const {
|
||
clearStockWarningImport,
|
||
importStockWarning,
|
||
} = require('../services/returnStockWarningImport');
|
||
const { sendWorkbookDownload } = require('../services/exportWorkbook');
|
||
|
||
const router = express.Router();
|
||
const upload = multer({
|
||
storage: multer.memoryStorage(),
|
||
limits: { fileSize: 20 * 1024 * 1024 },
|
||
});
|
||
|
||
const resolveScope = (value) => (['imported', 'replica'].includes(String(value)) ? String(value) : 'all');
|
||
|
||
/**
|
||
* GET /api/return-stock-warning/list —— 退货库存预警列表(客退数据监控,按仓库出数)
|
||
* warehouse: 仓库标识 henlin2 / subei / suining / shandong / henan(缺省 = 横林二仓)
|
||
*/
|
||
router.get('/list', async (req, res, next) => {
|
||
try {
|
||
const payload = await getReturnStockWarningList({
|
||
warehouse: req.query.warehouse,
|
||
page: req.query.page,
|
||
pageSize: req.query.pageSize,
|
||
keyword: req.query.keyword,
|
||
supplier: req.query.supplier,
|
||
sortBy: req.query.sortBy,
|
||
sortOrder: req.query.sortOrder,
|
||
qtyBasis: req.query.qtyBasis,
|
||
createdAtBasis: req.query.createdAtBasis,
|
||
scope: resolveScope(req.query.scope),
|
||
focusDays: req.query.focusDays,
|
||
alertDays: req.query.alertDays,
|
||
warningLevel: req.query.warningLevel,
|
||
priceFocusMin: req.query.priceFocusMin,
|
||
priceHighMin: req.query.priceHighMin,
|
||
priceLevel: req.query.priceLevel,
|
||
returnLimitDays: req.query.returnLimitDays,
|
||
refresh: req.query.refresh,
|
||
});
|
||
res.json({ success: true, data: payload });
|
||
} catch (error) {
|
||
next(error);
|
||
}
|
||
});
|
||
|
||
/** GET /api/return-stock-warning/export —— 服务端全量导出(xlsx / csv) */
|
||
router.get('/export', async (req, res, next) => {
|
||
try {
|
||
const format = String(req.query.format || 'xlsx').toLowerCase() === 'csv' ? 'csv' : 'xlsx';
|
||
const keyword = String(req.query.keyword || '').trim();
|
||
const warehouse = resolveWarehouse(req.query.warehouse);
|
||
const rows = await getReturnStockWarningExportRows({
|
||
warehouse: req.query.warehouse,
|
||
keyword,
|
||
supplier: req.query.supplier,
|
||
qtyBasis: req.query.qtyBasis,
|
||
createdAtBasis: req.query.createdAtBasis,
|
||
scope: resolveScope(req.query.scope),
|
||
focusDays: req.query.focusDays,
|
||
alertDays: req.query.alertDays,
|
||
warningLevel: req.query.warningLevel,
|
||
priceFocusMin: req.query.priceFocusMin,
|
||
priceHighMin: req.query.priceHighMin,
|
||
priceLevel: req.query.priceLevel,
|
||
returnLimitDays: req.query.returnLimitDays,
|
||
refresh: req.query.refresh,
|
||
});
|
||
|
||
const exportColumns = buildExportColumns(warehouse);
|
||
sendWorkbookDownload(res, {
|
||
fileName: `${warehouse.shortName}客退数据监控_${keyword || '全部'}`,
|
||
format,
|
||
sheets: [{
|
||
name: `${warehouse.shortName}客退数据监控`,
|
||
header: exportColumns.map((column) => column.title),
|
||
rows: rows.map((row) => exportColumns.map((column) => row[column.key])),
|
||
}],
|
||
});
|
||
} catch (error) {
|
||
next(error);
|
||
}
|
||
});
|
||
|
||
/** POST /api/return-stock-warning/import —— 导入聚水潭「箱及仓位库存」导出文件 */
|
||
router.post('/import', 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 importStockWarning({
|
||
buffer: req.file.buffer,
|
||
fileName,
|
||
importedBy: req.user?.username || req.user?.name || null,
|
||
// 导入落在哪个仓库下:优先取表单字段,其次 query,最后回落默认仓
|
||
warehouseKey: resolveWarehouseKey(req.body?.warehouse || req.query.warehouse),
|
||
});
|
||
res.json({ success: true, data: result });
|
||
} catch (error) {
|
||
next(error);
|
||
}
|
||
});
|
||
|
||
/** DELETE /api/return-stock-warning/import —— 清除当前仓库的导入,回到系统推算口径 */
|
||
router.delete('/import', async (req, res, next) => {
|
||
try {
|
||
const result = await clearStockWarningImport(resolveWarehouseKey(req.query.warehouse));
|
||
res.json({ success: true, data: result });
|
||
} catch (error) {
|
||
next(error);
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|