cangku/backend/src/api/warehouseLogisticsException.js
yanhui db1ba79416 feat: 新增「仓储中心物流转运异常」页面(12 发货仓 · 每日 09:00 核验前一日已发货物流)
- 后端新增 warehouse-logistics-exception 模块(Registry / Service / Api),
  挂载 /api/warehouse-logistics-exception;前端新增独立页面并接入左侧栏
  「仓储中心物流转运异常」(排在「仓储中心 24 小时发货时效」下面)。
- 核验口径:核验日 09:00 看**前一日已发货**订单的物流记录;12 个发货仓按仓库名
  最长匹配归口(子仓并入主仓,「京仓-横林二仓京东云仓」排除)。
  只统计揽收前的卡点:无物流单 / 未揽收 / 已揽收未中转 / 快递报异常 / 退回;
  运输中、派送中、已签收一律正常(干线与中转轨迹本就不回传,不再按 24h 判停滞)。
  明细按包裹、汇总按内部单号去重;结果缓存 5 分钟,force=1 可绕过。
- 剔除口径(业务 2026-10-21 / 10-22 定版,剔掉的既不算异常也不算发货分母):
  · 承运商:顺丰系全部(唯品顺丰 / 顺丰干配 / 顺丰大件各账号)、武进安能、中铁、
    壹米滴答、京喜京东(快递·快运)、自提、线下、商家配送;
  · 有条件剔除:睢宁中通只在睢宁仓考核,其它仓异地打单的剔出;
  · 订单已终止:状态含取消/作废,或标签含「发货后取消」「作废」「拦截」;
  · 刷单 / 信誉单:订单标签含「刷单」「转空包」。
  「武进安能」「京喜京东」均按精确粒度匹配,不泛化成「安能」「京东」,避免误剔
  常州安能新、京东自营快递亿家达等正常账号。
- 时区:模块内统一用 Asia/Shanghai 取「今天」,避免生产容器 Etc/UTC 导致核验批次错位。
- 页面:分仓统计卡 + 异常类型筛选 + 明细(承运商 / 运单号 / 物流状态 / 异常标签 /
  最后轨迹 / 发货时间 / 待处理小时),页脚展示剔除口径与本批剔除量;
  物流快照停更按黄/红两级告警,并标注「历史批次按当前物流快照重算」。

验证(本地隔离环境,非生产库):
- node --test backend/test/warehouseLogisticsException.test.js → 24/24 通过
- npm --prefix frontend run check → 通过
- 本地真实数据接口实测:顺丰系、睢宁中通(异地仓)明细 0 条残留
2026-10-22 09:07:49 +08:00

57 lines
1.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 分钟结果缓存
*
* 挂载见 src/index.js:`/api/warehouse-logistics-exception` + authMiddleware
*/
const express = require('express');
const { getLogisticsExceptions } = require('../services/warehouseLogisticsExceptionService');
const {
CENTERS, EXCEPTION_TYPES, LOOKBACK_DAYS, VERIFY_HOUR,
} = require('../services/warehouseLogisticsExceptionRegistry');
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, '加载物流转运异常失败');
}
});
// 口径元数据(不查库,秒回):页面文案与图例用它,避免前后端各写一套
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,
},
});
});
module.exports = router;