'use strict'; /** * 仓储中心 24 小时发货时效 —— HTTP 路由(2026-10-20 新增) * ──────────────────────────────────────────────────────────────────────────── * GET /api/warehouse-ship-timeliness/list * query: * round 'NOON' | 'AFTERNOON' 手动指定核验轮次;不传按当前时刻自动判定 * limit 明细条数上限(默认 500,最大 2000) * * GET /api/warehouse-ship-timeliness/rounds * 只返回今日两轮的描述(页面顶部提示用,不打订单表,秒回) * * GET /api/warehouse-ship-timeliness/export (2026-10-27 新增) * 「超时未发」明细导出:不分页、不截断,返回本轮全部行(与 /list 同一份缓存) * query: round / force * * ── 两轮核验完成后推钉钉群(2026-10-27 新增)──────────────────────────────── * GET /api/warehouse-ship-timeliness/notify/settings 读配置(加签密钥只回掩码) * PUT /api/warehouse-ship-timeliness/notify/settings 保存配置(加签密钥留空 = 不变) * GET /api/warehouse-ship-timeliness/notify/state 上次推送结果 * POST /api/warehouse-ship-timeliness/notify/run 立即推送一次(dryRun / probe / force) * * 挂载见 src/index.js:`/api/warehouse-ship-timeliness` + authMiddleware */ const express = require('express'); const { getShipTimeliness, listShipTimelinessExportRows, } = require('../services/warehouseShipTimelinessService'); const { describeRounds, CENTERS } = require('../services/warehouseShipTimelinessRegistry'); const { getShipTimelinessNotifyAdminConfig, getShipTimelinessNotifyRuntimeConfig, loadShipTimelinessNotifyState, missingFields, saveShipTimelinessNotifyConfig, } = require('../services/warehouseShipTimelinessNotifySettings'); const { pushShipTimelinessNotify } = require('../jobs/warehouseShipTimelinessNotifyJob'); const router = express.Router(); function sendError(res, error, fallback) { const status = Number(error?.statusCode) || 500; console.error('[WarehouseShipTimeliness]', { message: error?.message || fallback, code: error?.code || null, status, }); res.status(status).json({ success: false, error: error.message || fallback }); } // 时效总览:每个主仓「超时未发」数量 + 超时订单明细 router.get('/list', async (req, res) => { try { const data = await getShipTimeliness(req.query || {}); res.json({ success: true, data }); } catch (error) { sendError(res, error, '加载仓储中心发货时效失败'); } }); // 仅轮次信息(不查订单) router.get('/rounds', async (_req, res) => { try { res.json({ success: true, data: { rounds: describeRounds(new Date()), centers: CENTERS.map((c) => ({ key: c.key, name: c.name, placeholder: Boolean(c.placeholder), })), }, }); } catch (error) { sendError(res, error, '加载核验轮次失败'); } }); /** * 导出「超时未发」明细:**不分页、不截断**,返回本轮考核口径内的全部行。 * * query: round(同 /list)、force=1(绕过缓存重查) * * 与 /list 共用 getShipTimeliness(同一份缓存),所以页面刚刷过时这里是秒回。 * 前端拿到 rows 后按选中的仓在前端过滤 —— 口径与页面完全一致,不需要第二套 filter。 */ router.get('/export', async (req, res) => { try { const data = await listShipTimelinessExportRows(req.query || {}); res.json({ success: true, data }); } catch (error) { sendError(res, error, '导出超时未发明细失败'); } }); // ── 两轮核验完成后推钉钉群 ────────────────────────────────────────────────── // 读配置:加签密钥只回掩码 + 是否已配置,明文永不出后端 router.get('/notify/settings', async (_req, res) => { try { const [config, state] = await Promise.all([ getShipTimelinessNotifyAdminConfig(), loadShipTimelinessNotifyState(), ]); res.json({ success: true, data: { ...config, lastState: state || {} } }); } catch (error) { sendError(res, error, '读取发货时效通知配置失败'); } }); // 保存配置;加签密钥留空表示「沿用已存的」,不必每次重填 router.put('/notify/settings', async (req, res) => { try { const data = await saveShipTimelinessNotifyConfig(req.body || {}); res.json({ success: true, data }); } catch (error) { const status = Number(error?.status) || 400; console.error('[WarehouseShipTimeliness] 保存通知配置失败:', error?.message); res.status(status).json({ success: false, error: error.message || '保存发货时效通知配置失败' }); } }); router.get('/notify/state', async (_req, res) => { try { const state = await loadShipTimelinessNotifyState(); res.json({ success: true, data: state || {} }); } catch (error) { sendError(res, error, '读取推送状态失败'); } }); /** * 立即推送一次(页面「立即推送」/「预览正文」/「测试通道」)。 * body: * round 'NOON' | 'AFTERNOON',不传按当前时刻自动判定 * dryRun true = 只组装消息、不发送(返回正文全文,业务先核对口径) * probe true = 只发一句「通道测试」文本,不查业务数据(验 webhook 通不通) * force true = 忽略「本轮已推过」(默认手动触发就是 true,除非显式给 false) */ 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 getShipTimelinessNotifyRuntimeConfig({ 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 pushShipTimelinessNotify({ dryRun, probe, force, round: body.round || undefined, }); res.json({ success: true, data: result }); } catch (error) { sendError(res, error, '推送发货时效通知失败'); } }); module.exports = router;