Compare commits

..

No commits in common. "main" and "yanhui-wst-export-20261027" have entirely different histories.

13 changed files with 859 additions and 1803 deletions

View File

@ -1,7 +1,7 @@
'use strict';
/**
* 仓储中心发货时效 —— HTTP 路由(2026-10-20 新增)
* 仓储中心 24 小时发货时效 —— HTTP 路由(2026-10-20 新增)
* ────────────────────────────────────────────────────────────────────────────
* GET /api/warehouse-ship-timeliness/list
* query:

View File

@ -681,7 +681,7 @@ async function startServer(options = {}) {
cubejsApp.use('/api/product-monitoring', authMiddleware, productMonitoringRoutes);
cubejsApp.use('/api/modular-stock-monitor', authMiddleware, modularStockMonitorRoutes);
cubejsApp.use('/api/mkh-bom', authMiddleware, mkhBomRoutes);
// 仓储中心发货时效(2026-10-20 新增):列表左侧侧栏,11 个主仓超时未发
// 仓储中心 24 小时发货时效(2026-10-20 新增):列表左侧侧栏,11 个主仓超时未发
cubejsApp.use('/api/warehouse-ship-timeliness', authMiddleware, warehouseShipTimelinessRoutes);
// 仓储中心物流转运异常(2026-10-21 新增):每日 09:00 核验前一日已发货单的物流记录
cubejsApp.use('/api/warehouse-logistics-exception', authMiddleware, warehouseLogisticsExceptionRoutes);

View File

@ -1,7 +1,7 @@
'use strict';
/**
* 仓储中心发货时效 · 两轮核验完成后把超时未发单推到钉钉群
* 仓储中心 24 小时发货时效 · 两轮核验完成后把超时未发单推到钉钉群
* ────────────────────────────────────────────────────────────────────────────
* 业务口径(2026-10-27 原话):
* 「发货时效也需要钉钉通知:13 点核验 7 点前的超时单,核验完成后通知;
@ -9,18 +9,14 @@
* ⟹ **一天两推**,各绑一个核验轮次(轮次定义见 warehouseShipTimelinessRegistry 的 ROUNDS):
* NOON 13:00 核验「今天 07:00 前审核」→ 默认 13:05 推
* AFTERNOON 18:00 核验「截止 16:00 前审核」→ 默认 18:05 推
* ⚠️ 2026-10-28 起数据源换成订单两表(jst_Orders / jst_qmcust_orderlist),
* 起算点是**审核时间** `last_oa_confirm_time`(不是订单创建时间);
* 取不到审核时间的单回退创建时间,正文里在该时间后加 `*` 提示。
* 推送正文写「审核 HH:mm」(此前一版误写「创建」,已纠正)。
* ⚠️ 默认「核验时刻 + 5 分钟」:订单表 `updateTime` 得晚于核验时刻,
* ⚠️ 默认「核验时刻 + 5 分钟」:状态镜像 `query_time` 得晚于核验时刻,
* 整点推容易拿到 `freshness.coversDeadline=false`(数据没覆盖核验点,超时数偏大)。
* 时刻在页面上可改,改完立即生效(每分钟 tick 比对,不用重启后端)。
*
* 消息内容:
* 抬头(含「超时」字样,钉钉关键词安全设置要用)+ 汇总(超时单数 / 涉及仓数 / 最久已等)
* + 不计考核(待审核·订单异常,不算仓库责任)+ 按仓超时单数 + 前 N 条明细 + 页面链接。
* 明细一行 = 仓|内部单号|线上单号|快递|创建 HH:mm|已等 X 小时(业务对账用)。
* 明细一行 = 仓|内部单号|线上单号|快递|审核 HH:mm|已等 X 小时(业务对账用)。
* (「快递」是业务 2026-10-27 要求加的,见 buildDetailLine 的说明)
* 0 条也推 —— 群里看到「本轮 0 单超时」就知道任务在跑。
*
@ -122,18 +118,14 @@ function formatOverdue(minutes) {
}
/**
* 起算时间(= 审核时间 `last_oa_confirm_time`)的展示:
* 本轮的日期只给 HH:mm,跨天(历史未发单)必须带 MM-DD。
* 审核时间的展示:本轮的日期就只给 HH:mm,跨天(历史未发单)必须带 MM-DD。
* 🔥 为什么要带日期:13:00 轮窗口上界是 07:00,但口径「含历史未发」,
* 明细里会出现昨天 16:13 审核的单 —— 只写「审核 16:13」会被读成「今天 16:13」,
* 看着像「审核时间晚于窗口,不该算超时」,业务会对不上账。
*
* ⚠️ 传进来的 `auditTime` 是服务层的 benchmarkAt:审核时间,取不到时回退订单创建时间
* (此时 row.benchmarkSource === 'created',调用方会给时间加 `*`)。
*/
function auditWhen(auditTime, roundDate) {
const text = cleanCell(auditTime);
if (!text) return '';
if (!text) return '未审核';
const hit = text.match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/);
if (!hit) {
const clock = text.match(/(\d{2}:\d{2})/);
@ -151,21 +143,15 @@ function auditWhen(auditTime, roundDate) {
* 🔥 第四段「快递」是业务 2026-10-27 要求加的:群里看到超时单要能直接判断
* 是哪个快递、是不是得走站点(比如安能系、德邦、京东各区网点揽收节奏完全不同),
* 不用再回页面一单单点开确认。
* 🔥 2026-10-28 第五段是「审核」时间(起算点),不是订单创建时间;
* 审核时间缺失、回退订单创建时间的单,时间后面带 `*`。
* ⚠️ 跨天要带日期,见 auditWhen 的说明。
* ⚠️ 审核时间跨天要带日期,见 auditWhen 的说明。
*/
function buildDetailLine(row, index, roundDate) {
const when = auditWhen(row?.auditTime, roundDate);
const auditText = when
? `审核 ${when}${row?.benchmarkSource === 'created' ? '*' : ''}`
: '审核 —';
const parts = [
cleanCell(row?.warehouseName || row?.centerName, '未知仓'),
cleanCell(row?.oId, '无单号'),
cleanCell(row?.soId, '—'),
cleanCell(row?.logisticCompany, '—'),
auditText,
`审核 ${auditWhen(row?.auditTime, roundDate)}`,
`已等 ${formatOverdue(row?.overdueMinutes)}`,
];
return `${index}. ${parts.join('|')}`;
@ -289,7 +275,7 @@ function buildNotifyText(data = {}, options = {}) {
head.push('本轮没有超时未发货的订单,全部按时发出。');
}
if (heldCount > 0) {
head.push(`另有 ${heldCount} 单不计考核(待审核 / 订单异常)`);
head.push(`另有 ${heldCount} 单不计考核(已支付待审核 / 订单异常)`);
}
const carrierSkipped = Number(summary.carrierSkipped) || 0;
if (carrierSkipped > 0) {

View File

@ -4,7 +4,7 @@
* 仓储中心物流转运异常(2026-10-21 新增)
* ────────────────────────────────────────────────────────────────────────────
* 需求(yanhui 2026-10-21):
* 在左侧栏加一栏「仓储中心物流转运异常」(放在「仓储中心发货时效」下面),
* 在左侧栏加一栏「仓储中心物流转运异常」(放在「仓储中心 24 小时发货时效」下面),
* 下属 12 个发货仓:
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 /

View File

@ -1,7 +1,7 @@
'use strict';
/**
* 「仓储中心发货时效」两轮核验完成后推文本到钉钉群 —— 配置与状态存取
* 「仓储中心 24 小时发货时效」两轮核验完成后推文本到钉钉群 —— 配置与状态存取
* ────────────────────────────────────────────────────────────────────────────
* 业务口径(2026-10-27):
* 「13 点核验 7 点前的超时单,核验完成后通知;18:00 核验的订单,核验完成后也通知。」

View File

@ -1,10 +1,10 @@
'use strict';
/**
* 仓储中心发货时效(2026-10-20 新增)
* 仓储中心 24 小时发货时效(2026-10-20 新增)
* ────────────────────────────────────────────────────────────────────────────
* 需求(yanhui 2026-10-20):
* MKH-BOM 列表左侧加一栏「仓储中心发货时效」,下列 11 个仓:
* MKH-BOM 列表左侧加一栏「仓储中心24小时发货时效」,下列 11 个仓:
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 / 河南洛宁仓
* 从系统库存抓取「已发货订单」与「未发货订单」,按**订单审核时间**分轮核算。
@ -32,10 +32,7 @@
* rpt.jst_Orders —— 京东 / 拼多多 / 抖音 / 得物 / 唯品会
* rpt.jst_qmcust_orderlist —— 天猫 / 淘宝 / 1688
* 字段口径(2026-10-20 四次修正,全部以聚水潭「销售出库单」页面为准):
* 🔴 2026-10-28 现状:候选池取订单两表的 `send_date`,起算点取状态表的
* `last_oa_confirm_time`(审核时间),**下面这段「创建时间 = 出库单 created」
* 是老口径,仅作历史记录,不要再照着实现**。
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)(已废弃)
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)
* ⚠️ 不是 `order_date`(平台下单时间);
* ⚠️ 更不是 `modified` —— 实测数仓每天凌晨把整批订单的 modified 刷成
* 02:2x / 04:3x(样本 9765107 等 7 单全是 10-20 04:39~04:40,
@ -265,48 +262,6 @@ const OUTBOUND_TABLES = Object.freeze({
// ⚠️ 本表**没有金额**,金额要回订单表按 `o_id = inner_order_id` 取。
const ORDER_STATE_TABLE = 'jst_order_express_details';
// ── 🔥 2026-10-28 数据源切换:主数据源改成「订单 + 订单明细」────────────────
// 业务 yanhui 2026-10-28 明确指出:「核验抓取的销售订单表是错误的,应该抓
// `jst_Orders`(订单)+ `jst_OrderItem`(订单明细),o_id → o_id;审核时间就是
// jst_Orders 里的创建时间」。
//
// 为什么原来那张状态镜像表不能用(实测证据):
// ① `send_time` 会**漏回写**:近 7 天「状态表说发货中+未发」的 4157 单里,
// 订单表已有 `send_date` 的 **563 单**、status 已是 Sent 的 492 单 ——
// 明明发了却还算超时;2026-10-28 06h/07h 那批大批量刷新后集中爆发。
// ② 状态表对**被合并 / 拆单父单**不回写(已有 89 + 755 单),必须回订单表看 status。
// ③ 状态表会**整批停更**:2026-10-27 11:54:59 之后 6.8 小时零写入,
// 18:00 轮推出去 34 单里混着已发货的(业务当场发现)。
// ④ 两张表发货时间**其实同源**:状态表 send_time 与订单表 send_date
// 27387 单逐条比对**零偏差** ⟹ 订单表才是权威,状态表只是它的滞后副本。
//
// ⟹ 主查询直接打订单两表(店铺互斥必须都读),**不再依赖状态表**。
// 仓库归属用订单表的 `wms_co_id`(不是状态表的仓名文本),更准。
// 状态表只剩「售后已作废」一个判据,实测在「发货中+未发」里只影响 4 单,可放弃。
const ORDER_SOURCE_TABLES = Object.freeze(['jst_Orders', 'jst_qmcust_orderlist']);
// 订单表 `status` 的取值(聚水潭实时状态,不是下单快照 —— 这一点
// `jst_Orders.status` 与代码历史上「status 不可信」的结论不同,见下面 CREATED 段说明):
// Delivering 发货中 —— **唯一该考核仓库的状态**
// WaitConfirm 待审核 —— 仓库还没接到单,单列「待审核」不计超时
// Question 疑问/异常 —— 单列「异常待处理」不计超时
// Sent 已发货 → 剔除
// Cancelled 已取消(业务叫「作废」)→ 剔除
// Split / Merged 拆单父单 / 被合并单 → 剔除(自己永不发货)
// WaitOuterSent 等供销商|外仓发货 → 剔除(不是本仓发)
// 近 30 天分布:Sent 132341 / Cancelled 66643 / Split 8251 / Question 2057 /
// Merged 1854 / Delivering 1446 / WaitConfirm 190 / WaitOuterSent 45
const ORDER_PENDING_STATUS = 'Delivering';
const ORDER_UNAUDITED_STATUS = 'WaitConfirm';
const ORDER_QUESTION_STATUS = 'Question';
const ORDER_SHIPPED_STATUS = 'Sent';
// 主查询只捞这三种(其余状态一律不进候选,SQL 侧就过滤掉,不传回 Node)
const ORDER_CANDIDATE_STATUSES = Object.freeze([
ORDER_PENDING_STATUS,
ORDER_UNAUDITED_STATUS,
ORDER_QUESTION_STATUS,
]);
// 「发货中」= 已审核、仓库还没发货 —— **唯一该考核仓库的状态**
const SHIPPING_STATUS = '发货中';
// 「已支付待审核」= 还没审单,仓库根本没接到 → 不计超时(单列为「待审核」)
@ -321,26 +276,24 @@ const ABNORMAL_STATUS = '异常';
// 漏掉这个排除,整个页面会被作废单淹没。
const IGNORED_STATES = Object.freeze(['已发货', '已取消', '等供销商|外仓发货']);
// ── 起算点:聚水潭「审核时间」`last_oa_confirm_time` ────────────────────────
// 🔥 2026-10-28 **下午二次纠偏**(业务 yanhui 拿 9837824 打回来):
// 起算点是**审核时间**,不是订单创建时间。证据:
// 9837824 created 10-19 23:44:22 → last_oa_confirm_time 10-28 09:58:42,
// 积压 8 天半;仓库 09:58 审核、10:09:55 快递员就上门取件。
// 按 created 算「超时 205 小时」= 把客服审核积压算到仓库头上。
// 量化(10-28 13:00 轮 599 单):376 单是当天 08:10~10:30 才批量审核的,
// 按审核时间分轮后 13:00 轮只剩 247(再剔除漏回写的已发单 → 138)。
// ── 起算点:聚水潭「创建时间」──────────────────────────────────────────────
// 业务 2026-10-20 最终定版:「起算点是聚水潭的创建时间,不是审核时间」。
//
// ⚠️ 上午那版注释里「created 100% 早于 last_oa_confirm_time,正常差 7~40 分钟,
// 业务要求按创建时间算」已被推翻 —— 那句话只统计了当期正常单,
// 长尾积压单能差 8~11 天。**别再把起算点改回 created。**
// ⚠️ 状态表 `jst_order_express_details` **没有**聚水潭创建时间,必须回订单表补:
// · `createdAt`(本表那个 datetime2)—— ❌ **不是**订单创建时间。
// 实测比付款晚 1.5~3.5 小时(261019-… pay 21:05:16 / createdAt 次日 00:23:51),
// 是**同步落库时间**,用了会把起算点整体推后几小时。
// · `pay_time` —— ❌ 付款时间,业务明确说「这一单的创建还是按照付款时间来的」,
// 要换掉。
// · ✅ `jst_Orders.created` / `jst_qmcust_orderlist.created` —— **聚水潭创建时间**,
// 稳定比下单/付款晚 15~30 秒(抽样 00:07:23 → 00:07:41),
// 这才是聚水潭订单列表里「创建时间」那一列。
//
// 审核时间只存在于状态镜像表 `jst_order_express_details`(订单两表没有这个字段),
// 由 service 层按 `inner_order_id IN (...)` 批量取;取不到时回退 created 并标 `*`。
// 用 last 而不是 first:业务认为改单重审后重新进作业流,从最后一次审核计时
// (代价是重复审核会冲掉已累计超时,业务已知悉)。
// 性能(2026-10-20 实测 9,676 个候选):
// 只投影 `o_id, created` 两列、分批 200 → jst_Orders **14.5s** + qmcust **2.9s**。
// 🔥 千万别顺手把 pay_amount / labels 一起取出来:同样 IN 400 个只要 **38.7s**。
const CREATED_SOURCE_TABLES = Object.freeze(['jst_Orders', 'jst_qmcust_orderlist']);
// 分批大小:200 实测最快(再大收益递减,再小批数太多)
// ⚠️ 2026-10-28 起主链路已不用(created 直接从主查询取),保留给历史脚本/测试引用。
const CREATED_BACKFILL_BATCH = 200;
// ── 订单表里的「作废」状态:这些单不会由本单发货,必须剔除 ─────────────────
@ -378,64 +331,6 @@ const VOID_ORDER_LABEL_KEYWORDS = Object.freeze(['发货前售后']);
// 万一哪天出现「发货中 + 已作废 + 订单表也不是 Sent」,不至于再被算成超时。
const AFTERSALE_VOID_STATUS = '已作废';
/**
* 状态镜像表 `jst_order_express_details.order_status_name` 的**终态**。
*
* 🔥 2026-10-29 血的教训(业务第三次报「作废单/已发单还在考核里」):
* 订单两表**会各自停更**(当天 qmcust 停在 07:25、jst_Orders 停在 12:45),
* 停更期间在聚水潭做的「取消 / 发货」动作,订单表里根本看不到 ——
* 订单 status 仍是 `Delivering`、`send_date` 仍是空,于是被当成真超时。
* 状态镜像表虽然也会停(当天 11:52),但**比订单表新鲜 4 小时**,
* 而且 `order_status_name` 是聚水潭订单状态的直接映射。
*
* 实测(2026-10-29 13:00 轮 159 单):
* · `order_status_name='已取消'` 11 单 —— 订单表的 `labels` 里**没有**「发货前售后」
* 这个关键词(`presaleVoid` 判据完全失效),状态表的 `order_labels` 才有,且带
* 「已打印被取消」。业务就是拿这 11 单说「作废的也在考核之内」。
* · `order_status_name='已发货'` 0 单 —— 机制上必须有,否则停更期发的货永远判不出。
*
* ⚠️ **`aftersale_status_name='已作废'` 不是作废单**!那是「售后单被撤销」,
* 订单照常发货、**仍然要考核**(近 2 天 1387 单都是这种)。别把两个混为一谈,
* 能判「这单不用发了」的只有 `order_status_name='已取消'`。
*/
const STATE_ORDER_VOID_STATUS = '已取消';
const STATE_ORDER_SHIPPED_STATUS = '已发货';
/**
* 销售出库单 `status` 里**等于单据作废**的值。
*
* 🔥 2026-10-29 新增(业务点名 9992097 等 9 单:「在聚水潭的销售出库单里找一下这个作废单据」)。
*
* 聚水潭的作废会落在**出库单**上:`jst_salesOutboundInfo.status` / `jst_qmcust_salesoutbound.status`
* 取值只有四种(近 2 天实测):`Confirmed` / `WaitConfirm` / `Delete` / `OuterConfirming`,
* 其中 `Delete` 就是单据作废。
*
* 为什么必须加这一条判据(状态表 `order_status_name='已取消'` 不够):
* · 出库单 `Delete` 的近 7 天样本里,**55 单订单表仍是 `Delivering`、36 单是 `Question`**,
* 这些单状态镜像表未必抓得到(它一天只抓几次),只能靠出库单判;
* · 出库单是仓库侧最贴身的单据,作废动作一定先落在这里。
*
* ⚠️ **不能只看「有 Delete 就算作废」**:一个订单可能有多张出库单(拆包 / 重开),
* 删掉旧的那张、新的还在 `WaitConfirm` —— 这种单照常要发。
* ⟹ 判据是「**该订单没有任何非 Delete 的出库单**」,见 service 里 ioAllDeleted 的说明。
*
* ⚠️ 也**不能**用「没有出库单」当判据:近 3 天未发单里 **82%(1467/1790)本来就没有出库单**
* (订单还没推下游),拿它判作废会把绝大多数正常待发单全放跑。
*/
const IO_VOID_STATUS = 'Delete';
/**
* 状态镜像表 `logistic_status_name` 里**等于已发**的值。
*
* `拒收` 也算已发 —— 快递拿走过了,客户拒收是后续环节,仓库的发货动作已完成。
* ⚠️ **`无物流单` 不算**(39/159 单是这种:单子还没录快递单,可能自提/待定),
* `未揽收` 也不算(只是打了单,快递还没来取)。
* ⚠️ 别拿 `logistic_time` 当发货时间:它多数是「快递员将上门取件」的预告。
*/
const STATE_LOGISTIC_SHIPPED_STATUSES = Object.freeze([
'已揽收', '运输中', '派送中', '已签收', '拒收',
]);
// 24 小时发货时效 —— ⚠️ 仅用于**页面命名/路由**,**不参与判定**。
// 2026-10-20 三次调整后判定改成「到 13:00 / 18:00 核验时刻仍未发即超时」,
// 没有滚动 24 小时窗口(见 SHIP_TIMELINESS_HOURS 的历史误用记录)。
@ -544,29 +439,12 @@ async function loadCenterWarehouseMap(force = false) {
warehouseCache = {
at: Date.now(),
rows: list,
// 全量「wms_co_id → 仓名」映射(**含归不到主仓的仓**)。
// 发货时效主查询不再 LEFT JOIN 仓表(实测 JOIN 会让执行计划从 28s 恶化到 61~105s),
// 改为在主查询里取回 wms_co_id、在 Node 侧用这份缓存换名字。
allNames: rows.map((r) => ({ wmsCoId: Number(r.wms_co_id), name: String(r.name || '') })),
unmatched,
unmatchedCount: unmatched.length,
excluded,
};
return list;
}
/** 全量「wms_co_id → 仓名」映射(含归不到主仓的仓),供 service 侧做仓名还原 */
async function loadWarehouseNameMap() {
if (Date.now() - warehouseCache.at >= WAREHOUSE_CACHE_TTL_MS || !warehouseCache.rows.length) {
await loadCenterWarehouseMap();
}
const map = new Map();
for (const r of warehouseCache.allNames || []) {
if (Number.isFinite(r.wmsCoId)) map.set(r.wmsCoId, r.name);
}
return map;
}
function clearWarehouseCache() {
warehouseCache = { at: 0, rows: [] };
}
@ -679,7 +557,6 @@ module.exports = {
centerWindowEnd,
nextRoundDeadline,
loadCenterWarehouseMap,
loadWarehouseNameMap,
clearWarehouseCache,
getWarehouseCache: () => warehouseCache,
resolveActiveRound,
@ -692,13 +569,6 @@ module.exports = {
NOT_SPLIT_MERGED_SQL,
OUTBOUND_TABLES,
ORDER_STATE_TABLE,
// 2026-10-28 起主数据源 = 订单两表(状态表退居兜底)
ORDER_SOURCE_TABLES,
ORDER_PENDING_STATUS,
ORDER_UNAUDITED_STATUS,
ORDER_QUESTION_STATUS,
ORDER_SHIPPED_STATUS,
ORDER_CANDIDATE_STATUSES,
SHIPPING_STATUS,
UNAUDITED_STATUS,
ABNORMAL_STATUS,
@ -709,10 +579,6 @@ module.exports = {
VOID_QUESTION_STATUS,
VOID_ORDER_LABEL_KEYWORDS,
AFTERSALE_VOID_STATUS,
STATE_ORDER_VOID_STATUS,
STATE_ORDER_SHIPPED_STATUS,
STATE_LOGISTIC_SHIPPED_STATUSES,
IO_VOID_STATUS,
SHIP_TIMELINESS_HOURS,
toNumber,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -141,8 +141,8 @@ test('按仓块只列超时 > 0 的仓,并按单数降序', () => {
test('明细一行 = 仓|内部单号|线上单号|快递|审核 时间|已等', () => {
const built = job.buildNotifyText(sampleData(), { maxDetailRows: 30 });
assert.match(built.text, /1\. 简尚-崔桥工厂仓|9855123|261021-301318827670255|武进韵达|审核 06:32|已等 6 小时 28 分/);
// 无线上单号 → 「—」;无快递名 → 「—」;无创建时间 → 「无创建时间」;都不能出现 undefined
assert.match(built.text, /3\. 简尚-横林二仓|9855300|—|—|审核 —|已等 2 小时/);
// 无线上单号 → 「—」;无快递名 → 「—」;无审核时间 → 「未审核」;都不能出现 undefined
assert.match(built.text, /3\. 简尚-横林二仓|9855300|—|—|审核 未审核|已等 2 小时/);
assert.doesNotMatch(built.text, /undefined|NaN/);
});
@ -164,7 +164,7 @@ test('历史未发单的审核时间必须带 MM-DD,否则会被读成「今
// 直接测函数本身的跨天判定
assert.equal(job.auditWhen('2026-10-27 06:32:11', '2026-10-27'), '06:32');
assert.equal(job.auditWhen('2026-10-26 16:13:00', '2026-10-27'), '10-26 16:13');
assert.equal(job.auditWhen('', '2026-10-27'), '');
assert.equal(job.auditWhen('', '2026-10-27'), '未审核');
});
test('不计考核 / 不考核快递 / 数据未覆盖核验点都要在正文里说明', () => {

View File

@ -4,7 +4,7 @@ import request from './request';
// 超时必须放大到 3 分钟,否则前端会先断开、后端还在跑。
const TIMELINESS_TIMEOUT_MS = 3 * 60 * 1000;
// 仓储中心发货时效:每个主仓「超时未发」数量 + 超时订单明细
// 仓储中心 24 小时发货时效:每个主仓「超时未发」数量 + 超时订单明细
// params.round 'NOON' | 'AFTERNOON'(不传按当前时刻自动判定)
// params.limit 明细条数上限
export const getWarehouseShipTimeliness = (params = {}) => request.get(

View File

@ -37,7 +37,7 @@ export const DEFAULT_SIDEBAR_CONFIG = {
{ key: 'mkh-bom-data', label: 'MKH-BOM资料', icon: 'table', routeKey: 'mkh-bom-data', visible: true, order: 20 },
],
},
{ key: 'warehouse-ship-timeliness', type: 'item', label: '仓储中心发货时效', icon: 'inbox', routeKey: 'warehouse-ship-timeliness', visible: true, order: 110 },
{ key: 'warehouse-ship-timeliness', type: 'item', label: '仓储中心24小时发货时效', icon: 'inbox', routeKey: 'warehouse-ship-timeliness', visible: true, order: 110 },
{ key: 'warehouse-logistics-exception', type: 'item', label: '仓储中心物流转运异常', icon: 'warning', routeKey: 'warehouse-logistics-exception', visible: true, order: 111 },
{
key: 'large-stock-monitor', type: 'group', label: '大货库存监控', icon: 'appstore', visible: true, order: 200, defaultExpanded: true,

View File

@ -83,7 +83,7 @@ const NOTIFY_HELP_ITEMS = [
业务口径:<b>13:00 核验 7 点前审核的超时单</b>、<b>18:00 核验 16 点前审核的单</b>,
各自<b>核验完成后</b>推一次,所以是两个独立时刻,互不影响。
<br />
默认给到 <b>13:05 / 18:05</b>(核验点 + 5 分钟):订单表的 updateTime 要晚几分钟才覆盖到核验时刻,
默认给到 <b>13:05 / 18:05</b>(核验点 + 5 分钟):聚水潭状态镜像要晚几分钟才覆盖到核验时刻,
整点推容易报「数据没覆盖核验点、超时数偏大」。想改成整点也可以,消息里会带这句提醒。
<br />
同一轮次同一天只推一次(服务重启不会重复轰炸);要重发点「立即推送」。
@ -165,46 +165,19 @@ const HELD_COLUMNS = [
},
},
{
title: (
<Tooltip title="聚水潭「审核时间」= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者(状态表会滞后,9978643 旧值比真实审核早一天)。两者都没有才回退订单创建时间,标 *">
<span>审核时间</span>
</Tooltip>
),
title: '审核时间',
dataIndex: 'auditTime',
width: 152,
ellipsis: true,
render: (v, row) => {
if (!v) return <span className="wst-detail__fallback">无审核时间</span>;
return row?.benchmarkSource === 'created' ? `${v} *` : v;
},
render: (v) => (v || <span className="wst-detail__fallback">未审核</span>),
},
{ title: '下单时间', dataIndex: 'createdAt', width: 152, ellipsis: true, render: (v) => v || '—' },
{ title: '付款时间', dataIndex: 'payTime', width: 152, ellipsis: true, render: (v) => v || '—' },
{ title: '已挂起', dataIndex: 'heldDays', width: 88, render: (v) => (v > 0 ? `${v} 天` : '—') },
];
const DETAIL_COLUMNS = [
// 内部单号 = 聚水潭 o_id(系统内部编号,仓管在系统里按它查单)
{
title: '内部单号',
dataIndex: 'oId',
width: 158,
// 🔥 来自停更订单表的单必须打标(2026-10-28):
// 该表停更之后发出的单,send_date 仍是空 → 被判「未发」,实际很可能已经发了。
// 18:00 轮实测 546 单里 540 单是这种,不标出来业务会以为判定写错。
render: (v, row) => (
<span className="wst-detail__oid">
{v || '—'}
{row?.sourceStale ? (
<Tooltip
title={`这单来自 ${row?.srcLabel || '订单表'},该表已停止同步:停更之后发出的单在这里仍是「未发货」,很可能已经发了。请先在聚水潭核对。`}
>
<Tag bordered={false} color="error" className="wst-detail__stale">未同步</Tag>
</Tooltip>
) : null}
</span>
),
},
{ title: '内部单号', dataIndex: 'oId', width: 110 },
// 线上订单号 = 平台 so_id(京东/抖音/天猫等平台单号,客服按它对接客户)
{
title: '线上订单号',
@ -230,7 +203,7 @@ const DETAIL_COLUMNS = [
{
// 发货时间 = 聚水潭回传的发货时间(业务 2026-10-20:明细里不要创建时间,要发货时间)
title: (
<Tooltip title="订单表 jst_Orders.send_date(聚水潭回传的发货时间)。为空但有运单号的,说明仓库已打单、还没揽收/回传发货时间,会显示成「已打单」">
<Tooltip title="聚水潭回传的发货时间(send_time)。为空但有运单号的,说明仓库已打单、还没揽收/回传发货时间,会显示成「已打单」">
<span>发货时间</span>
</Tooltip>
),
@ -251,111 +224,47 @@ const DETAIL_COLUMNS = [
},
},
{
// 审核时间 = 本页的**考核起算点**(业务 2026-10-28 下午三次定版)
// = max(状态表 last_oa_confirm_time, 订单表 modified),都没有才回退创建时间并打 `*`
// 审核时间 = **考核基准**,取聚水潭「审核时间」列(最后一次审核,重审会刷新)
title: (
<Tooltip title="聚水潭「审核时间」= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者。状态表会滞后(9978643 状态表是 10-27 17:08,真实审核 10-28 14:52),所以取较晚的那个。今天 7:00 前审核的 13:00 核验;截止 16:00 前审核的(含历史未发单)18:00 核验。带 * 表示两者都没有,回退订单创建时间">
<Tooltip title="= 聚水潭订单列表「审核时间」那一列(最后一次审核,重新审核会刷新)。今天 7:00 前审核的 13:00 核验;截止 16:00 前审核的(含今天之前未发的历史单)18:00 核验,已等时长也从它算">
<span>审核时间</span>
</Tooltip>
),
dataIndex: 'auditTime',
width: 152,
ellipsis: true,
render: (v, row) => {
if (!v) return <span className="wst-detail__fallback">—</span>;
if (row?.benchmarkSource === 'created') {
return (
<Tooltip title="状态表审核时间和订单表 modified 都取不到,已回退订单创建时间参与分轮与计时(`*` 为标记)">
<span>{v} *</span>
</Tooltip>
);
}
return v;
},
},
// 下单时间 = 客户在平台下单那一刻(只做参考对账,不参与计时)
{
title: (
<Tooltip title="聚水潭订单创建时间(jst_Orders.created),即客户下单时刻。只做参考,不参与分轮与计时">
<span>下单时间</span>
</Tooltip>
),
dataIndex: 'createdAt',
width: 152,
ellipsis: true,
render: (v) => v || '—',
render: (v, row) => (v
? v
: (
<Tooltip title="没取到审核时间,已回退创建/付款时间参与分轮与计时">
<span className="wst-detail__fallback">{row?.benchmarkTime || '—'} *</span>
</Tooltip>
)),
},
// 付款时间 = 客户付款那一刻(并列展示,方便对账)
{ title: '付款时间', dataIndex: 'payTime', width: 152, ellipsis: true, render: (v) => v || '—' },
{
// 发货状态:**优先显示聚水潭终态**(状态镜像表),订单表状态只作对照
// 🔥 2026-10-29:订单两表会各自停更,停更期间聚水潭里已经「已取消 / 已发货」的单,
// 订单表里仍是「发货中」——业务看到的「作废的也在考核里」就是这么来的。
// 状态镜像表比订单表新鲜,`order_status_name` 才是聚水潭的真状态。
// 发货状态:聚水潭实时状态(发货中 = 已审核未发货,即考核对象)
title: (
<Tooltip title="优先显示聚水潭订单终态(状态镜像表,订单表停更时只有它靠得住)。⚠ = 与订单表 jst_Orders.status 不一致">
<Tooltip title="聚水潭订单实时状态。发货中 = 已审核、仓库还没发货">
<span>发货状态</span>
</Tooltip>
),
dataIndex: 'stateOrderStatus',
width: 128,
render: (v, row) => {
const lake = v || '';
const ord = row?.state || '';
const diff = Boolean(lake && ord && lake !== ord);
const text = lake || ord || '—';
const color = text === '已取消' ? 'red' : (text === '发货中' ? 'orange' : 'default');
return (
<Tooltip title={`聚水潭:${lake || '—'}|订单表:${ord || '—'}${diff ? '(不一致,订单表没同步到这个状态)' : ''}`}>
<Tag bordered={false} color={color}>{text}{diff ? ' ⚠' : ''}</Tag>
</Tooltip>
);
},
},
{
// 物流状态:优先用状态表的真实物流节点(未揽收 / 已揽收 / 运输中 / 已签收…),
// 订单表没有物流轨迹,只能靠「有没有运单号」猜「已打单 · 未揽收」
title: (
<Tooltip title="聚水潭物流节点(状态镜像表)。已揽收及之后 = 货已经被快递拿走">
<span>物流状态</span>
</Tooltip>
dataIndex: 'state',
width: 104,
render: (v) => (
<Tag bordered={false} color={v === '发货中' ? 'orange' : 'default'}>{v || '—'}</Tag>
),
dataIndex: 'stateLogisticStatus',
width: 108,
render: (v, row) => {
const text = v || row?.logisticStatus || '';
if (!text) return '—';
const shipped = ['已揽收', '运输中', '派送中', '已签收', '拒收'].includes(v);
return <Tag bordered={false} color={shipped ? 'green' : 'default'}>{text}</Tag>;
},
},
{
// 销售出库单状态(2026-10-29 新增)
// 🔥 业务点名「这些订单都是作废订单」后挖出来的:聚水潭的作废**落在出库单上**
// (`status='Delete'`),订单表的 labels 判据在那批单上 0 命中。
// 这一列让业务能直接对照聚水潭「销售出库单」页面,不用再问「系统里为什么没有」。
title: (
<Tooltip title="聚水潭「销售出库单」的单据状态。已作废=单据被删(不考核);待出库=已生成单据但没出库;空白=还没建出库单(正常,近八成未发单本来就没有)">
<span>销售出库单</span>
</Tooltip>
),
dataIndex: 'ioStatus',
width: 112,
render: (v) => {
const map = {
Delete: ['已作废', 'red'],
WaitConfirm: ['待出库', 'blue'],
Confirmed: ['已出库', 'green'],
OuterConfirming: ['外部确认中', 'default'],
};
const hit = map[v];
if (!hit) return '—';
return <Tag bordered={false} color={hit[1]}>{hit[0]}</Tag>;
},
title: '物流状态',
dataIndex: 'logisticStatus',
width: 100,
render: (v) => (v ? <Tag bordered={false}>{v}</Tag> : '—'),
},
{
title: (
<Tooltip title="从审核时间(仓库接到单的时刻)到本轮核验时刻(13:00 / 18:00)等了多久">
<Tooltip title="从审核时间到本轮核验时刻(13:00 / 18:00)等了多久">
<span>已等时长</span>
</Tooltip>
),
@ -382,43 +291,15 @@ const EXPORT_COLUMNS = [
{ title: '发货仓', dataIndex: 'warehouseName' },
{ title: '快递公司', dataIndex: 'logisticCompany', exportValue: (v) => v || '未指定' },
{ title: '运单号', dataIndex: 'logisticId', exportValue: (v) => v || '未打单' },
{ title: '物流状态', dataIndex: 'stateLogisticStatus', exportValue: (v, row) => v || row?.logisticStatus || '' },
{ title: '物流状态', dataIndex: 'logisticStatus' },
{ title: '发货状态', dataIndex: 'state' },
// 聚水潭终态与订单表状态分开导出:两者不一致的单,就是「订单表没同步」的证据
{ title: '聚水潭状态', dataIndex: 'stateOrderStatus', exportValue: (v) => v || '—' },
// 销售出库单状态:聚水潭的作废落在这一层(Delete=单据已作废),
// 业务对账时要拿这一列跟聚水潭「销售出库单」页面对
{
title: '销售出库单',
dataIndex: 'ioStatus',
exportValue: (v) => ({
Delete: '已作废',
WaitConfirm: '待出库',
Confirmed: '已出库',
OuterConfirming: '外部确认中',
}[v] || '未建单'),
},
{ title: '状态表抓取时间', dataIndex: 'stateQueryTime', exportValue: (v) => v || '—' },
// 导出也要带上「这单的来源表 + 该表是否停更」,否则 Excel 脱离页面就没有上下文,
// 拿着 540 单「未发」去追仓库,实际人家早就发了。
{ title: '数据源', dataIndex: 'srcLabel', exportValue: (v) => v || '—' },
{
title: '数据源是否停更',
dataIndex: 'sourceStale',
exportValue: (v) => (v ? '是(该单可能已发)' : '否'),
},
{ title: '发货时间', dataIndex: 'sendTime', exportValue: (v) => v || '未发货' },
{
title: '审核时间',
dataIndex: 'auditTime',
// 起算点 = 审核时间;没取到时回退订单创建时间,导出用 `*` 标出来,否则对账对不上
exportValue: (v, row) => {
const t = v || row?.benchmarkTime || '';
if (!t) return '无审核时间';
return row?.benchmarkSource === 'created' ? `${t} *` : t;
},
// 审核时间缺失时页面会回退创建/付款并打 `*`,导出也保持一致,否则对账对不上
exportValue: (v, row) => v || (row?.benchmarkTime ? `${row.benchmarkTime} *` : '未审核'),
},
{ title: '下单时间', dataIndex: 'createdAt', exportValue: (v) => v || '—' },
{ title: '付款时间', dataIndex: 'payTime' },
// 分钟数给一列原始值,业务在 Excel 里能直接排序/筛选;人读的那列再格式化
{ title: '已等(分钟)', dataIndex: 'overdueMinutes' },
@ -439,22 +320,17 @@ const roundFileText = (roundInfo) => (
);
/**
* 仓储中心发货时效(独立页面)
* 仓储中心 24 小时发货时效(独立页面)
* ────────────────────────────────────────────────────────────────
* 业务口径(yanhui 2026-10-20 定轮次;2026-10-28 定起算点):
* 业务口径(yanhui 2026-10-20):
* · 11 个主仓,子仓并入主仓一起算;
* · 数据源 = 订单表 jst_Orders + jst_qmcust_orderlist(两张表店铺互斥,都要读);
* · 今天 07:00 前**审核**的订单 13:00 核验,截止今天 16:00 前审核的单 18:00 核验
* (含 7 点前审核未发出的、含今天之前审核至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
* 今天之前仍未发的(历史未发)两个轮次都算;
* (含 7 点前未发出的、含今天之前审核至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
* 今天之前审核仍未发的(历史未发)两个轮次都算;
* **到核验时刻未发即超时**,没有「满 24 小时」门槛(业务纠偏点);
* 轮次归属和「已等时长」都按**审核时间**算 ——
* 业务 2026-10-28 下午三次定版:审核时间 = max(状态表 last_oa_confirm_time, 订单表 modified)。
* 只取状态表会漏 —— 状态表会滞后(9978643 状态表写的是 10-27 17:08,聚水潭真实审核 10-28 14:52,
* 整整早一天),导致「今天下午才审核的单」被算进 13:00 轮,冤枉仓库;
* 两者都没有才回退订单创建时间,页面上带 `*` 标记;
* 轮次归属和「已等时长」都按**审核时间**算(聚水潭「审核时间」列,最后一次审核);
* · **13:00 之前不核验**,直接显示「未到核验时间」,不拿昨天的结果冒充今天;
* · 已出库 / 待出库以订单表 **send_date** 为准(SQL 直接 `send_date IS NULL` 才进候选);
* · 已出库 / 待出库以**聚水潭发货时间**为准(有 send_time = 已发);
* · 只显示超时未出库(发完的仓显示「—」),点仓库可筛右侧明细。
*
* 数据来自 /api/warehouse-ship-timeliness/list(service 层见
@ -516,11 +392,6 @@ function WarehouseShipTimeliness() {
const summary = data?.summary || {};
const round = data?.round || null;
const freshness = data?.freshness || null;
// 🔥 超时单里「所在订单表已停更」的数量(2026-10-28 定型):
// 停更⟹停更之后发出的单 send_date 仍为空⟹被判「未发」,但实际很可能已发。
// 18:00 轮实测 546 单里 540 单属于这种,必须显著提示,否则业务会以为判定写错。
const staleUnshipped = Number(summary.sourceStaleUnshipped || 0);
const staleSources = Array.isArray(data?.dataSources) ? data.dataSources.filter((s) => s && s.stale) : [];
// 13:00 之前后端返回 round=null + notStarted=true:此时**不展示任何超时数字**
const notStarted = Boolean(data?.notStarted);
const hasRound = Boolean(round);
@ -748,11 +619,9 @@ function WarehouseShipTimeliness() {
return (
<SycmPage className="wst-page">
<header className="wst-header">
<Title level={3}>仓储中心发货时效(到点未发即超时)</Title>
<Title level={3}>仓储中心 24 小时发货时效</Title>
<Text type="secondary">
按聚水潭订单表实时状态核验 11 个主仓是否在承诺时效内完成发货,只列超时未发货的单量。
{/* 2026-10-28 业务定:名字里不再出现「24 小时」—— 规则本身没有 24h 门槛,
原名会让 10-27 16:00 创建(只等了 21 小时)的单看着像「没到时限也被判超时」。*/}
按聚水潭订单实时状态核验 11 个主仓是否在承诺时效内完成发货,只列超时未发货的单量。
</Text>
<Text type="secondary" className="wst-note">
每天 <b>13:00</b> 核验「<b>今天 07:00 前审核</b>」、<b>18:00</b> 核验「<b>截止 16:00 前审核</b>」、
@ -762,17 +631,15 @@ function WarehouseShipTimeliness() {
<b>到点还没发就算超时 —— 不设「满 24 小时」门槛</b>(早上 7 点前那份单,13:00 还没走就是超时)。
<b>仓级例外</b>:18:00 轮里 <b>山东青岗集仓 / 山东品牌仓</b> 只看 <b>15:00 前审核</b>的单、
<b>河南洛宁仓</b> 只看 <b>13:00 前审核</b>的单(核验时刻仍是 18:00)。
<b>轮次与「已等时长」都按审核时间算</b>(= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者:
审核才是仓库接到单的时刻,按下单时间算会把客服审核积压算到仓库头上;
状态表的审核时间会滞后,所以要和订单 modified 取较晚的那个);
明细并列「审核时间 / 下单时间 / 发货时间」,方便直接和聚水潭对账。
<b>已发 / 未发以订单表 send_date 为准</b>(有值即已发,SQL 侧直接过滤不进候选);
订单表 send_date 没回写、但状态表 send_time 有值的,也按已发剔除(不漏冤仓库)。
<b>轮次与「已等时长」都按审核时间算</b>(= 聚水潭「审核时间」列,最后一次审核);
明细并列「审核时间 / 发货时间」,方便直接和聚水潭对账。
<b>已发 / 未发以聚水潭发货时间为准</b>:有发货时间就是已发,不再用订单表的发货字段反推
—— 该字段经常不回写,会把早就发货的单误判成超时。
<b>已打单但还没揽收的</b>(聚水潭已生成运单号、发货时间还空着)会标成「已打单 · 未揽收」,
鼠标悬停能看到运单号 —— 这类单还留在列表里,等聚水潭回传发货时间才消失。
<b>{excludedCarrierText} 不考核</b>(客户自提、专线大件或加盟快运网点,揽收与在途仓库控制不了),
已按物流公司自动剔除。
<b>「待审核」和「异常」的单不计考核</b>(仓库还没接到或订单本身有问题),单独归入「不计考核」并给出明细;
<b>「已支付待审核」和「异常」的单不计考核</b>(仓库还没接到或订单本身有问题),单独归入「不计考核」并给出明细;
<b>作废单(已取消,含发货前售后)不考核也不展示</b> —— 客户付款后取消的单仓库本就不该发。
</Text>
</header>
@ -824,44 +691,14 @@ function WarehouseShipTimeliness() {
/>
) : null}
{(freshness && (freshness.stale || freshness.coversDeadline === false))
|| staleUnshipped > 0
|| staleSources.length > 0 ? (
{freshness && (freshness.stale || freshness.coversDeadline === false) ? (
<Alert
// 快照没覆盖到核验时刻 / 有单来自停更的表 → 数字必然偏大,用红色;只是滞后用黄色
type={staleUnshipped > 0 || freshness?.coversDeadline === false ? 'error' : 'warning'}
// 快照没覆盖到核验时刻 → 数字必然偏大,用红色;只是滞后用黄色
type={freshness.coversDeadline === false ? 'error' : 'warning'}
showIcon
className="wst-alert"
message={
staleUnshipped > 0
? `${number(staleUnshipped)} 单所在订单表已停止同步,这些单很可能已经发货`
: (freshness?.voidJudgeReliable === false
// 🔥 2026-10-29:出库单/状态表停更 ⟹ 作废判据失效。
// 业务当天报「洛宁仓 16 单都是作废单」,但我们三个来源全停在作废动作之前,
// 判据再准也判不出来 —— 必须明说,别让业务以为是系统认定「没作废」。
? '出库单数据源已停止同步,作废单可能判不出来'
: (staleSources.length > 0
// 状态镜像表也算数据源:它停了,判「已取消 / 已发货」的依据就过期了
? '数据源已停止同步(订单表 / 状态镜像表)'
: (freshness?.coversDeadline === false
? '数据未覆盖本轮核验时刻,超时单量偏大'
: '发货数据可能滞后')))
}
description={(
<>
{freshness?.hint}
{staleSources.length ? (
<div className="wst-alert__sources">
{staleSources.map((s) => (
<span key={s.key} className="wst-alert__source">
{s.label}:最后同步 {String(s.lastUpdate || '').slice(11, 19) || '—'}
(已 {Math.round((s.lagMinutes || 0) / 6) / 10} 小时)
</span>
))}
</div>
) : null}
</>
)}
message={freshness.coversDeadline === false ? '数据未覆盖本轮核验时刻,超时单量偏大' : '发货数据可能滞后'}
description={freshness.hint}
/>
) : null}
@ -878,13 +715,6 @@ function WarehouseShipTimeliness() {
formatter={(v) => (notStarted ? '—' : number(v))}
valueStyle={{ color: (summary.overdueUnshipped || 0) > 0 ? '#cf1322' : undefined }}
/>
{staleUnshipped > 0 ? (
<Tooltip title="这批单来自已停止同步的订单表:停更之后发出的单,在库里 send_date 还是空,会被判成「未发」。先在聚水潭核对,别直接拿去追仓库。">
<div className="wst-stat__warn">
其中 {number(staleUnshipped)} 单数据未同步
</div>
</Tooltip>
) : null}
</Card>
<Card
bordered={false}
@ -893,7 +723,7 @@ function WarehouseShipTimeliness() {
>
<Statistic
title={(
<Tooltip title="待审核(订单状态 WaitConfirm,还没审单,仓库没接到)和订单异常(Question)的单。这类单不进仓库考核,点开看明细。">
<Tooltip title="已支付待审核(还没审单,仓库没接到)和订单异常的单。这类单不进仓库考核,点开看明细。">
<span>不计考核 <span className="wst-stat__more">明细</span></span>
</Tooltip>
)}
@ -1047,7 +877,7 @@ function WarehouseShipTimeliness() {
type="info"
showIcon
className="wst-alert"
message="「待审核」= 订单状态还没审单(WaitConfirm),仓库根本没接到;「订单异常」= 聚水潭标记 Question(缺货/地址/风控等)。两类都不该算仓库超时,故单列于此。"
message="「已支付待审核」= 还没审单,仓库根本没接到;「订单异常」= 聚水潭标记异常(缺货/地址/风控等)。两类都不该算仓库超时,故单列于此。"
/>
{heldOrders.length ? (
<Table
@ -1241,25 +1071,16 @@ function WarehouseShipTimeliness() {
</Modal>
<Text type="secondary" className="wst-foot">
说明:数据源 = 聚水潭<b>订单表 jst_Orders + jst_qmcust_orderlist</b>(两张表店铺互斥,按 o_id 去重)。
<b>今天 7:00 前审核</b>的订单 13:00 核验;<b>截止 16:00 前审核</b>的单 18:00 核验
说明:<b>今天 7:00 前审核</b>的订单 13:00 核验;<b>截止 16:00 前审核</b>的单 18:00 核验
(含 7 点前审核、13:00 还没发出的,也含今天之前审核至今未发的历史单)。
<b>到点仍未发货即超时</b>(不设「满 24 小时」门槛);
轮次与「已等时长」都按<b>审核时间</b>算(= 状态表 last_oa_confirm_time 与订单表 modified 的较晚者,
审核才是仓库接到单的时刻;状态表会滞后,故取较晚者)。两者都取不到才回退订单创建时间,列上带 <b>*</b>。
发货 = 订单表 send_date(为空才算未发)。<b>已发货不考核</b>:状态表 send_time 有值、
聚水潭状态「已发货」、或物流已揽收及之后,任一成立即判已发
{summary.stateShippedSkipped > 0 ? ` —— 本轮已剔除 ${summary.stateShippedSkipped} 单` : ''}。
状态只取「发货中」。
<b>作废单不考核</b>:聚水潭状态「已取消」,或订单表 labels 含「发货前售后」
{summary.voidCount > 0
? ` —— 本轮已自动剔除 ${summary.voidCount} 单`
+ `${summary.stateVoidSkipped > 0 ? `(其中 ${summary.stateVoidSkipped} 单靠聚水潭状态判出,订单表当时没同步)` : ''}`
: ''};
⚠️ 售后状态「已作废」<b>不算</b>作废单 —— 那是售后单被撤销、订单照常发货,仍要考核。
轮次与「已等时长」都按审核时间(聚水潭显示值)算。
发货 = 聚水潭回传的发货时间。
<b>作废单不考核</b>(被合并 / 拆单父单 / 已取消,或订单表已有发货时间)
{summary.voidCount > 0 ? ` —— 本轮已自动剔除 ${summary.voidCount} 单` : ''};
<b>{excludedCarrierText}不考核</b>
{summary.carrierSkipped > 0 ? ` —— 本轮已剔除 ${summary.carrierSkipped} 单` : ''};
待审核、订单异常的单归入「不计考核」,避免冤枉仓库。
已支付待审核、订单异常的单归入「不计考核」,避免冤枉仓库。
</Text>
</SycmPage>
);

View File

@ -111,45 +111,6 @@
color: #1677ff;
}
/* 超时卡下面的「其中 N 单数据未同步」—— 订单表停更时的警示小字(2026-10-28) */
.wst-stat__warn {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: #cf1322;
cursor: help;
}
/* 停更告警里逐个源列出「最后同步时间 / 滞后多久」 */
.wst-alert__sources {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
}
.wst-alert__source {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
font-variant-numeric: tabular-nums;
}
/* 明细里的「内部单号 + 未同步标记」 */
.wst-detail__oid {
display: inline-flex;
align-items: center;
gap: 4px;
font-variant-numeric: tabular-nums;
}
.wst-detail__stale {
margin-inline-end: 0;
font-size: 11px;
line-height: 16px;
padding-inline: 5px;
cursor: help;
}
/* ============ 主从两栏 ============ */
.wst-body {
display: flex;