forked from lunanting/cangku
规则完全不动,只改名字,消除误导。 ## 为什么改 页面原名「仓储中心 24 小时发货时效」,但判定规则是「到 13:00 / 18:00 核验时刻仍未发货即超时」,**没有满 24 小时的门槛**(业务 2026-10-20 已三次纠偏定版)。 名字和规则打架,导致看的人以为「不满 24 小时不该算超时」。 实测例子:10-27 16:00 创建的单,今天 13:00 核验时只等了 **21 小时**就被判超时; 13:00 轮 599 单里有 416 单属于 10-27 16 点后创建。业务看到就问「创建时间都在 7 点后, 为什么算超时」—— 其实是名字里的「24 小时」在误导(那些是**昨天**晚上的单, 不是今天的)。 ## 改了什么 用户可见(2 处): - 侧栏菜单:`仓储中心24小时发货时效` → `仓储中心发货时效` (不加括号:侧栏现有最长 label 12 字,加了会到 18 字溢出) - 页面标题:`仓储中心 24 小时发货时效` → `仓储中心发货时效(到点未发即超时)` 其余为注释同步(8 个文件),代码逻辑与判定口径**零改动**: `warehouseShipTimelinessRegistry/Service/NotifyJob/NotifySettings`、 `warehouseLogisticsExceptionRegistry`、`frontend/api/warehouseShipTimeliness`、`backend/index`。 ## 不变的部分 - 核验时点、窗口上界(07:00 / 16:00)、仓级例外(山东 15:00、洛宁 13:00) - 数据源与起算点(订单两表 + `created`) - 钉钉推送抬头本来就是「【仓储中心发货时效】」,不用改 ## 验证 - 前端 eslint 通过、`npm run build` 通过(31.86s) - 浏览器真渲染(本地 5173,等足 75 秒数据返回): - 标题 = `仓储中心发货时效(到点未发即超时)` - 侧栏项 = `仓储中心发货时效`,全页 `body` 内已无「24小时」字样 - 数据正常渲染:超时未发 599 单 / 不计考核 849 / 涉及仓库 8 个 / 最久未发 13 天 - 明细「创建时间」列完整显示 `2026-10-27 19:xx` 这类日期,不存在时区错位 - 无 pageerror - 后端 `npm run check`:本次只动注释,不影响
175 lines
6.6 KiB
JavaScript
175 lines
6.6 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 仓储中心发货时效 —— 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;
|