'use strict'; /** * 仓储中心物流转运异常 —— HTTP 路由(2026-10-21 新增) * ──────────────────────────────────────────────────────────────────────────── * GET /api/warehouse-logistics-exception/list * query: * date 核验日 'YYYY-MM-DD'(默认:已过 09:00 用今天,否则用昨天) * limit 明细条数上限(默认 500,最大 2000) * force 1 = 绕过 5 分钟结果缓存 * GET /api/warehouse-logistics-exception/export * 同 /list 口径的不分页明细(导出用,上限 5000) * * 每天 09:10 推 Excel 到钉钉群(2026-10-22 新增): * GET /api/warehouse-logistics-exception/notify/settings 读配置(AppSecret 只回掩码) * PUT /api/warehouse-logistics-exception/notify/settings 保存配置(AppSecret 留空=不变) * POST /api/warehouse-logistics-exception/notify/run 立即推送一次(force / dryRun) * GET /api/warehouse-logistics-exception/notify/state 上次推送结果 * * 挂载见 src/index.js:`/api/warehouse-logistics-exception` + authMiddleware */ const express = require('express'); const { getLogisticsExceptions, getLogisticsExceptionExportRows, } = require('../services/warehouseLogisticsExceptionService'); const { CENTERS, EXCEPTION_TYPES, LOOKBACK_DAYS, VERIFY_HOUR, } = require('../services/warehouseLogisticsExceptionRegistry'); const { getWarehouseLogisticsExceptionNotifyAdminConfig, loadWarehouseLogisticsExceptionNotifyState, missingFields, saveWarehouseLogisticsExceptionNotifyConfig, getWarehouseLogisticsExceptionNotifyRuntimeConfig, } = require('../services/warehouseLogisticsExceptionNotifySettings'); const { pushWarehouseLogisticsExceptionNotify, } = require('../jobs/warehouseLogisticsExceptionNotifyJob'); const router = express.Router(); function sendError(res, error, fallback) { const status = Number(error?.statusCode) || 500; console.error('[WarehouseLogisticsException]', { message: error?.message || fallback, code: error?.code || null, status, }); res.status(status).json({ success: false, error: error.message || fallback }); } // 转运异常总览:12 个发货仓异常单量 + 明细 router.get('/list', async (req, res) => { try { const data = await getLogisticsExceptions(req.query || {}); res.json({ success: true, data }); } catch (error) { sendError(res, error, '加载物流转运异常失败'); } }); // 导出用:与 /list 同一套口径,只是把明细上限放开(不分页) // 页面筛选(仓 / 异常类型)由前端用同一份过滤函数处理,保证导出的就是页面看到的 router.get('/export', async (req, res) => { try { const data = await getLogisticsExceptionExportRows(req.query || {}); res.json({ success: true, data }); } catch (error) { sendError(res, error, '导出物流转运异常失败'); } }); // 口径元数据(不查库,秒回):页面文案与图例用它,避免前后端各写一套 router.get('/meta', (_req, res) => { res.json({ success: true, data: { verifyHour: VERIFY_HOUR, lookbackDays: LOOKBACK_DAYS, centers: CENTERS.map((c) => ({ key: c.key, name: c.name })), exceptionTypes: EXCEPTION_TYPES, }, }); }); // ── 每天推送到钉钉群 ──────────────────────────────────────────────────────── // 读配置:AppSecret 只回掩码 + 是否已配置,明文永不出后端 router.get('/notify/settings', async (_req, res) => { try { const [config, state] = await Promise.all([ getWarehouseLogisticsExceptionNotifyAdminConfig(), loadWarehouseLogisticsExceptionNotifyState(), ]); res.json({ success: true, data: { ...config, lastState: state || {} } }); } catch (error) { sendError(res, error, '读取异常通知配置失败'); } }); // 保存配置;AppSecret 留空表示「沿用已存的」,不必每次重填 router.put('/notify/settings', async (req, res) => { try { const data = await saveWarehouseLogisticsExceptionNotifyConfig(req.body || {}); res.json({ success: true, data }); } catch (error) { const status = Number(error?.status) || 400; console.error('[WarehouseLogisticsException] 保存通知配置失败:', error?.message); res.status(status).json({ success: false, error: error.message || '保存异常通知配置失败' }); } }); router.get('/notify/state', async (_req, res) => { try { const state = await loadWarehouseLogisticsExceptionNotifyState(); res.json({ success: true, data: state || {} }); } catch (error) { sendError(res, error, '读取推送状态失败'); } }); /** * 立即推送一次(页面「立即推送」/「预览」/「测试通道」)。 * body: * dryRun true = 只组装消息、不发送(返回正文全文,业务先核对口径) * probe true = 只发一句「通道测试」文本,不查业务数据(验 webhook 通不通) * force true = 忽略「今天已经推过」(默认手动触发就是 true,除非显式给 false) * date 指定核验日 */ router.post('/notify/run', async (req, res) => { try { const body = req.body || {}; const dryRun = body.dryRun === true || body.dryRun === 'true'; const probe = body.probe === true || body.probe === 'true'; const force = body.force === undefined ? true : (body.force === true || body.force === 'true'); if (!dryRun) { const config = await getWarehouseLogisticsExceptionNotifyRuntimeConfig({ forceRefresh: true }); const missing = missingFields(config); if (missing.length) { res.status(400).json({ success: false, error: `请先补全并保存:${missing.join(' / ')}`, data: { missingFields: missing }, }); return; } } const result = await pushWarehouseLogisticsExceptionNotify({ dryRun, probe, force, date: body.date || undefined, }); // 查完这一轮记得让页面上的状态同步(lastState 由前端重新拉 settings 获取) res.json({ success: true, data: result }); } catch (error) { const status = Number(error?.status) || 500; console.error('[WarehouseLogisticsException] 推送失败:', { message: error?.message, code: error?.code, dingtalk: error?.dingtalk, }); res.status(status).json({ success: false, error: error.message || '推送到钉钉群失败', data: { code: error?.code ?? null }, }); } }); module.exports = router;