fix(wst): 起算点改回「最后审核时间」,并剔除订单表漏回写的已发单 #90
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 仓储中心 24 小时发货时效 —— HTTP 路由(2026-10-20 新增)
|
||||
* 仓储中心发货时效 —— HTTP 路由(2026-10-20 新增)
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* GET /api/warehouse-ship-timeliness/list
|
||||
* query:
|
||||
|
||||
@ -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);
|
||||
// 仓储中心 24 小时发货时效(2026-10-20 新增):列表左侧侧栏,11 个主仓超时未发
|
||||
// 仓储中心发货时效(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);
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 仓储中心 24 小时发货时效 · 两轮核验完成后把超时未发单推到钉钉群
|
||||
* 仓储中心发货时效 · 两轮核验完成后把超时未发单推到钉钉群
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* 业务口径(2026-10-27 原话):
|
||||
* 「发货时效也需要钉钉通知:13 点核验 7 点前的超时单,核验完成后通知;
|
||||
* 18:00 核验的订单,核验完成后也通知。」
|
||||
* ⟹ **一天两推**,各绑一个核验轮次(轮次定义见 warehouseShipTimelinessRegistry 的 ROUNDS):
|
||||
* NOON 13:00 核验「今天 07:00 前创建」→ 默认 13:05 推
|
||||
* AFTERNOON 18:00 核验「截止 16:00 前创建」→ 默认 18:05 推
|
||||
* ⚠️ 2026-10-28 起数据源换成订单两表、起算点换成 `jst_Orders.created`
|
||||
* (业务:「审核时间 jst_Orders 是这个表里面的创建时间」),
|
||||
* 所以推送正文写的是「创建 HH:mm」,不再叫「审核」。
|
||||
* 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` 得晚于核验时刻,
|
||||
* 整点推容易拿到 `freshness.coversDeadline=false`(数据没覆盖核验点,超时数偏大)。
|
||||
* 时刻在页面上可改,改完立即生效(每分钟 tick 比对,不用重启后端)。
|
||||
@ -121,18 +122,18 @@ function formatOverdue(minutes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 起算时间(= 订单创建时间 `jst_Orders.created`)的展示:
|
||||
* 起算时间(= 审核时间 `last_oa_confirm_time`)的展示:
|
||||
* 本轮的日期只给 HH:mm,跨天(历史未发单)必须带 MM-DD。
|
||||
* 🔥 为什么要带日期:13:00 轮窗口上界是 07:00,但口径「含历史未发」,
|
||||
* 明细里会出现昨天 16:13 创建的单 —— 只写「创建 16:13」会被读成「今天 16:13」,
|
||||
* 看着像「创建时间晚于窗口,不该算超时」,业务会对不上账。
|
||||
* 明细里会出现昨天 16:13 审核的单 —— 只写「审核 16:13」会被读成「今天 16:13」,
|
||||
* 看着像「审核时间晚于窗口,不该算超时」,业务会对不上账。
|
||||
*
|
||||
* ⚠️ 2026-10-28 数据源切换后,本函数的入参 `auditTime` 实际是**订单创建时间**
|
||||
* (服务层把 created 同时写进 auditTime / createdAt,业务口径两者等同)。
|
||||
* ⚠️ 传进来的 `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})/);
|
||||
@ -144,22 +145,27 @@ function auditWhen(auditTime, roundDate) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 明细一行:仓|内部单号|线上单号|快递|创建 时间|已等 X 小时
|
||||
* 明细一行:仓|内部单号|线上单号|快递|审核 时间|已等 X 小时
|
||||
* ⚠️ 第二段是**内部单号**(聚水潭 o_id,仓管在系统里按它查单),第三段是线上单号(客服对客户)。
|
||||
* 两者业务都要,别只留一个。
|
||||
* 🔥 第四段「快递」是业务 2026-10-27 要求加的:群里看到超时单要能直接判断
|
||||
* 是哪个快递、是不是得走站点(比如安能系、德邦、京东各区网点揽收节奏完全不同),
|
||||
* 不用再回页面一单单点开确认。
|
||||
* 🔥 2026-10-28 第五段从「审核」改「创建」:起算点已换成订单创建时间。
|
||||
* 🔥 2026-10-28 第五段是「审核」时间(起算点),不是订单创建时间;
|
||||
* 审核时间缺失、回退订单创建时间的单,时间后面带 `*`。
|
||||
* ⚠️ 跨天要带日期,见 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, '—'),
|
||||
`创建 ${auditWhen(row?.auditTime, roundDate)}`,
|
||||
auditText,
|
||||
`已等 ${formatOverdue(row?.overdueMinutes)}`,
|
||||
];
|
||||
return `${index}. ${parts.join('|')}`;
|
||||
@ -274,7 +280,7 @@ function buildNotifyText(data = {}, options = {}) {
|
||||
const windowEnd = String(window.end || '').slice(11, 16);
|
||||
|
||||
const head = [
|
||||
`【仓储中心发货时效】${roundLabel}${windowEnd ? ` · 核验 ${windowEnd} 前创建` : ''}`,
|
||||
`【仓储中心发货时效】${roundLabel}${windowEnd ? ` · 核验 ${windowEnd} 前审核` : ''}`,
|
||||
`超时未发货 ${overdue} 单,涉及 ${centerCount} 个仓`
|
||||
+ (oldestDays > 0 ? `,最久已等 ${oldestDays} 天` : ''),
|
||||
];
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* 仓储中心物流转运异常(2026-10-21 新增)
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* 需求(yanhui 2026-10-21):
|
||||
* 在左侧栏加一栏「仓储中心物流转运异常」(放在「仓储中心 24 小时发货时效」下面),
|
||||
* 在左侧栏加一栏「仓储中心物流转运异常」(放在「仓储中心发货时效」下面),
|
||||
* 下属 12 个发货仓:
|
||||
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
|
||||
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 /
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 「仓储中心 24 小时发货时效」两轮核验完成后推文本到钉钉群 —— 配置与状态存取
|
||||
* 「仓储中心发货时效」两轮核验完成后推文本到钉钉群 —— 配置与状态存取
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* 业务口径(2026-10-27):
|
||||
* 「13 点核验 7 点前的超时单,核验完成后通知;18:00 核验的订单,核验完成后也通知。」
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 仓储中心 24 小时发货时效(2026-10-20 新增)
|
||||
* 仓储中心发货时效(2026-10-20 新增)
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* 需求(yanhui 2026-10-20):
|
||||
* MKH-BOM 列表左侧加一栏「仓储中心24小时发货时效」,下列 11 个仓:
|
||||
* MKH-BOM 列表左侧加一栏「仓储中心发货时效」,下列 11 个仓:
|
||||
* 横林二仓 / 横林三仓 / 横林二仓品牌仓 / 崔桥工厂仓 / 崔桥工厂品牌仓 /
|
||||
* 崔桥工厂半成品包装项目仓 / 新苏北仓 / 睢宁仓 / 山东青岗集仓 / 山东品牌仓 / 河南洛宁仓
|
||||
* 从系统库存抓取「已发货订单」与「未发货订单」,按**订单审核时间**分轮核算。
|
||||
@ -32,7 +32,10 @@
|
||||
* rpt.jst_Orders —— 京东 / 拼多多 / 抖音 / 得物 / 唯品会
|
||||
* rpt.jst_qmcust_orderlist —— 天猫 / 淘宝 / 1688
|
||||
* 字段口径(2026-10-20 四次修正,全部以聚水潭「销售出库单」页面为准):
|
||||
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)
|
||||
* 🔴 2026-10-28 现状:候选池取订单两表的 `send_date`,起算点取状态表的
|
||||
* `last_oa_confirm_time`(审核时间),**下面这段「创建时间 = 出库单 created」
|
||||
* 是老口径,仅作历史记录,不要再照着实现**。
|
||||
* · 创建时间:**销售出库单的 `created`**(出库单创建时间)(已废弃)
|
||||
* ⚠️ 不是 `order_date`(平台下单时间);
|
||||
* ⚠️ 更不是 `modified` —— 实测数仓每天凌晨把整批订单的 modified 刷成
|
||||
* 02:2x / 04:3x(样本 9765107 等 7 单全是 10-20 04:39~04:40,
|
||||
@ -318,18 +321,23 @@ const ABNORMAL_STATUS = '异常';
|
||||
// 漏掉这个排除,整个页面会被作废单淹没。
|
||||
const IGNORED_STATES = Object.freeze(['已发货', '已取消', '等供销商|外仓发货']);
|
||||
|
||||
// ── 起算点:聚水潭「创建时间」──────────────────────────────────────────────
|
||||
// 🔥 2026-10-28 **业务再次确认**:「审核时间是 jst_Orders 这个表里面的创建时间」。
|
||||
// 这与 2026-10-20 的最初定版一致(本段下面就是那次的原话),中间一段
|
||||
// 因「9818476 创建 10-18、审核 10-20」被临时改成按审核时间,现已改回创建时间。
|
||||
// ── 起算点:聚水潭「审核时间」`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)。
|
||||
//
|
||||
// 实测差异(近 7 天 26086 单):`created` **100% 早于** `last_oa_confirm_time`,
|
||||
// 正常单只差 7~40 分钟;夜间下单、次日上班才审的单差 6~9 小时(极端的差 2 天)。
|
||||
// 影响量化(2026-10-27 18:00 轮):按审核 147 单 / 平均已等 4.5h;
|
||||
// 按创建 272 单 / 平均已等 8.5h —— 业务已知悉并要求按创建时间算。
|
||||
// ⚠️ 上午那版注释里「created 100% 早于 last_oa_confirm_time,正常差 7~40 分钟,
|
||||
// 业务要求按创建时间算」已被推翻 —— 那句话只统计了当期正常单,
|
||||
// 长尾积压单能差 8~11 天。**别再把起算点改回 created。**
|
||||
//
|
||||
// 主数据源已经切成订单表(见上面 ORDER_SOURCE_TABLES),`created` 直接从主查询拿,
|
||||
// **不再需要 backfill**。
|
||||
// 审核时间只存在于状态镜像表 `jst_order_express_details`(订单两表没有这个字段),
|
||||
// 由 service 层按 `inner_order_id IN (...)` 批量取;取不到时回退 created 并标 `*`。
|
||||
// 用 last 而不是 first:业务认为改单重审后重新进作业流,从最后一次审核计时
|
||||
// (代价是重复审核会冲掉已累计超时,业务已知悉)。
|
||||
const CREATED_SOURCE_TABLES = Object.freeze(['jst_Orders', 'jst_qmcust_orderlist']);
|
||||
// 分批大小:200 实测最快(再大收益递减,再小批数太多)
|
||||
// ⚠️ 2026-10-28 起主链路已不用(created 直接从主查询取),保留给历史脚本/测试引用。
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 仓储中心 24 小时发货时效 —— 服务层
|
||||
* 仓储中心发货时效 —— 服务层
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════════════════
|
||||
* 🔥 2026-10-28 数据源与起算点「双切换」(业务 yanhui 定版)
|
||||
* 🔥 2026-10-28 数据源切换 + 起算点二次纠偏(业务 yanhui 定版)
|
||||
* ═══════════════════════════════════════════════════════════════════════════
|
||||
* 业务原话:
|
||||
* ① 「核验抓取的销售订单表是错误的,应该在数据库抓取:订单 jst_Orders、
|
||||
* 订单明细 jst_OrderItem,o_id → o_id」
|
||||
* ② 「审核时间是 jst_Orders 这个表里面的创建时间」
|
||||
* 数据源(已定版,不要动):
|
||||
* rpt.jst_order_express_details(状态镜像)→
|
||||
* rpt.jst_Orders + rpt.jst_qmcust_orderlist(**店铺互斥,必须都读**)
|
||||
*
|
||||
* 于是本版做两件事:
|
||||
* · 主数据源:rpt.jst_order_express_details(状态镜像)→
|
||||
* rpt.jst_Orders + rpt.jst_qmcust_orderlist(**店铺互斥,必须都读**)
|
||||
* · 起算点:last_oa_confirm_time(审核时间)→ created(订单创建时间)
|
||||
*
|
||||
* 为什么状态镜像表不能用(实测证据,2026-10-27/28):
|
||||
* 为什么状态镜像表不能当**候选池**(实测证据,2026-10-27/28):
|
||||
* ① send_time **漏回写**:近 7 天「状态表说发货中+未发」的 4157 单里,
|
||||
* 订单表已有 send_date 的 **563 单**、status 已是 Sent 的 492 单;
|
||||
* ② 被合并 / 拆单父单**不回写**:另有 Merged 89 + Split 755 单被冤枉;
|
||||
@ -25,27 +19,48 @@
|
||||
* ④ 两表发货时间**同源**:send_time vs send_date 27387 单逐条比对**零偏差**
|
||||
* ⟹ 订单表才是权威,状态表只是它的滞后副本。
|
||||
*
|
||||
* 起算点换 created 的影响(业务已确认):
|
||||
* created 100% 早于 last_oa_confirm_time(近 7 天 26086 单无一例外),
|
||||
* 正常单差 7~40 分钟;夜间下单、次日上班才审的单差 6~9 小时。
|
||||
* 2026-10-27 18:00 轮实测:按审核 147 单 / 均等 4.5h → 按创建 272 单 / 均等 8.5h。
|
||||
* 🔥 起算点:**审核时间 `last_oa_confirm_time`**(2026-10-28 下午二次纠偏)
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
* 下午那一版曾把起算点改成 `created`(订单创建时间),理由是「审核时间就是
|
||||
* 订单创建时间」。**这个判断是错的**,业务当场拿 9837824 打回来:
|
||||
*
|
||||
* ⚠️ 2026-10-20 那次「按审核时间算」的纠偏已被本版覆盖,别再把基准时间
|
||||
* 改回 auditAt —— 那是上一轮的定版,业务 2026-10-28 明确要求按创建时间。
|
||||
* 9837824(崔桥工厂仓,京喜-橡野里家具工厂店)逐字段:
|
||||
* order_date 10-19 23:43:35 / created 10-19 23:44:22 / pay_date 10-19 23:44:05
|
||||
* last_oa_confirm_time **10-28 09:58:42**(= first_oa_confirm_time)
|
||||
* 运单 JDVB71664383839,10:09:55 快递员上门取件
|
||||
* ⟹ 10-19 下的单压了 9 天才审核,仓库审核后 11 分钟就打单,
|
||||
* 按 created 算它「超时 205 小时」,纯属把审核积压算到仓库头上。
|
||||
*
|
||||
* 量化(10-28 13:00 轮 599 单):376 单是**今天 07:00 之后**才审核的
|
||||
* (08:10~10:30 集中批量审核,多为 10-17~10-27 积压单),491 单其实已有运单号。
|
||||
* 改口径后实测:599 → 144,再剔除 6 单「状态表 send_time 有值、订单表 send_date 空」
|
||||
* 的漏回写单 → **138**。
|
||||
*
|
||||
* ⚠️ 别再被「created 100% 早于 last_oa_confirm_time,正常差 7~40 分钟」误导:
|
||||
* 那只统计了当期正常单,长尾积压单能差 8~11 天。判断起算点必须看全量分布。
|
||||
*
|
||||
* 为什么用 `last_oa_confirm_time` 而不是 `first_oa_confirm_time`(业务 2026-10-28 选定):
|
||||
* 客服改单重审会刷新 last,业务认为重审后订单重新进仓库作业流,应从最后一次
|
||||
* 审核重新计时。实测 599 单里 first==last 366 单,first<last 24 单(最长推后 5.8 天)。
|
||||
* ⚠️ 这意味着**重复审核会把已累计的超时冲掉**,业务已知悉并接受。
|
||||
*
|
||||
* 审核时间为空(实测 599 单里 209 单,多为天猫/抖音/拼多多,136 单其实已打单)
|
||||
* → **回退 order created** 参与分轮与计时,`benchmarkSource='created'`,
|
||||
* 页面该列打 `*` 标注,钉钉/导出同步标注。别默默丢掉这些单。
|
||||
*
|
||||
* ── 三条铁律(不变)──────────────────────────────────────────────────────
|
||||
* ① 轮次归属看起算点:今天 07:00 前创建 → 13:00 轮;
|
||||
* 截止今天 16:00 前创建 → 18:00 轮(山东两仓 15:00、河南洛宁 13:00 为仓级例外)。
|
||||
* **历史未发单一律算**(昨天及更早创建、至今没发的,两个轮次都要看到)。
|
||||
* ① 轮次归属看起算点:今天 07:00 前**审核** → 13:00 轮;
|
||||
* 截止今天 16:00 前**审核** → 18:00 轮(山东两仓 15:00、河南洛宁 13:00 为仓级例外)。
|
||||
* **历史未发单一律算**(昨天及更早审核、至今没发的,两个轮次都要看到)。
|
||||
* ② 没有「满 24 小时」门槛:到核验时刻仍未发就是超时。
|
||||
* ③ 自提 / 中铁 / 壹米滴答 / 武进安能 四个快递不考核(按快递名包含匹配,
|
||||
* 武进安能只剔这一个网点,**别泛化成「安能」**,常州安能新等仍考核),
|
||||
* 剔除量记 summary.carrierSkipped。
|
||||
*
|
||||
* 已等时长 = 创建时间 → 本轮核验时刻(13:00 / 18:00)。
|
||||
* 已等时长 = 审校起算点 → 本轮核验时刻(13:00 / 18:00)。
|
||||
*
|
||||
* ── 订单表字段口径(已逐一实测)───────────────────────────────────────────
|
||||
* · created —— ✅ **本页起算点**(聚水潭订单创建时间)
|
||||
* · created —— 聚水潭订单创建时间(**只做兜底与展示**,不是起算点)
|
||||
* · order_date —— 平台下单时间(客户侧,比 created 早 15~30 秒)
|
||||
* · pay_date —— 付款时间(并列展示)
|
||||
* · send_date —— 发货时间(**未发单必为空**,已发 100% 有值)
|
||||
@ -60,7 +75,7 @@
|
||||
* · modified —— ⚠️ 每天凌晨被批量刷新,**不可用**
|
||||
*
|
||||
* ── 三态分流(互斥,全部由订单表 status 决定)─────────────────────────────
|
||||
* 超时未发货 = status='Delivering' + send_date 空 + created 已过窗口上界 → orders
|
||||
* 超时未发货 = status='Delivering' + send_date 空 + 审核时间已过窗口上界 → orders
|
||||
* 不计考核 = status='WaitConfirm'(待审核)/ 'Question'(异常) → heldOrders
|
||||
* 不展示 = Sent / Cancelled / Split / Merged / WaitOuterSent → SQL 侧已排除
|
||||
* ⚠️ 作废(Cancelled,业务叫「作废单」)近 30 天 66643 单,是 Delivering 1446 单的 46 倍,
|
||||
@ -236,6 +251,11 @@ function diffMinutes(fromText, toText) {
|
||||
* Sent/Cancelled/Split/Merged/WaitOuterSent 直接不进候选
|
||||
* · `created >= windowStart` —— 30 天护栏(`HISTORY_LOOKBACK_DAYS`)
|
||||
*
|
||||
* ⚠️ 这里的 SQL 只是**粗筛**:起算点是审核时间(在状态表里),SQL 拿不到,
|
||||
* 所以「哪一轮」由 Node 侧按起算点再判一次。护栏按 `created` 卡,
|
||||
* 理论上「创建超过 30 天、今天才审核」的单会被漏掉 —— 实测极罕见
|
||||
* (最老积压单创建 10-17,审核 10-28,差 11 天),暂不放宽,别顺手改成去掉护栏。
|
||||
*
|
||||
* 仓库:订单表给的是 `wms_co_id`(不是仓名文本),主查询**只回这个 id**,
|
||||
* 由 Node 侧用 `registry.loadWarehouseNameMap()` 的缓存换仓名,再走
|
||||
* `resolveCenter()` 关键词归属 —— 比状态表的仓名更准(同一仓在状态表里有
|
||||
@ -304,6 +324,58 @@ async function loadPendingOrders(round) {
|
||||
return rows || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量取**审核时间**(起算点)—— 唯一来源是状态镜像表 `jst_order_express_details`。
|
||||
*
|
||||
* 为什么必须回状态表拿:
|
||||
* · `jst_Orders` / `jst_qmcust_orderlist` **都没有审核时间字段**(85 列逐一看过,
|
||||
* 只有 order_date / pay_date / created / modified / updateTime,前者是下单、
|
||||
* 后者是灌库时间,`modified` 还会被每天凌晨批量刷新);
|
||||
* · 状态表 `first_oa_confirm_time` / `last_oa_confirm_time` 才是聚水潭的审核时间。
|
||||
* ⚠️ 只从它拿**这一个字段**,候选取舍(哪些单未发货)仍然只看订单两表 ——
|
||||
* 状态表的 send_time 漏回写、会整批停更,那些坑一个都不能重新踩回来。
|
||||
*
|
||||
* 业务口径(2026-10-28):起算点用 `last_oa_confirm_time`(业务选「最后审核时间」,
|
||||
* 理由见文件头)。空值不在这里兜,交给调用方回退 `created`。
|
||||
*
|
||||
* 性能:`inner_order_id IN (...)` 一次 600 个约 **15s**(全表扫一次,402 万行视图)。
|
||||
* 🔥 千万别改成逐单查 —— 实测单条 **14.5s**,599 条要 2.4 小时。
|
||||
* 分批 600 是因为 T-SQL 单条语句 IN 列表太长会解析超时;两批各 600 / 剩余。
|
||||
*
|
||||
* 失败降级:抛错由调用方 catch → 全部回退 created,并在 payload 里标 degraded,
|
||||
* **不要因为状态表挂了就整页 500**(候选池在订单表,本来就有数据)。
|
||||
*/
|
||||
async function loadOaConfirmInfo(stateRows) {
|
||||
const ids = [...new Set(
|
||||
(stateRows || []).map((r) => Number(r.o_id)).filter((n) => Number.isFinite(n))
|
||||
)];
|
||||
const map = new Map();
|
||||
if (!ids.length) return map;
|
||||
|
||||
const BATCH = 600;
|
||||
for (let i = 0; i < ids.length; i += BATCH) {
|
||||
// ids 已用 Number.isFinite 过滤过,拼进 IN 是纯数字,无注入面
|
||||
const chunk = ids.slice(i, i + BATCH);
|
||||
const rows = await cloudDatabase.querySlow(
|
||||
`SELECT inner_order_id, first_oa_confirm_time, last_oa_confirm_time, send_time
|
||||
FROM rpt.jst_order_express_details WITH (NOLOCK)
|
||||
WHERE inner_order_id IN (${chunk.join(',')})`,
|
||||
{},
|
||||
{ connectRequestTimeoutMs: 300000 }
|
||||
);
|
||||
for (const r of rows || []) {
|
||||
map.set(Number(r.inner_order_id), {
|
||||
first: formatDateTime(r.first_oa_confirm_time),
|
||||
last: formatDateTime(r.last_oa_confirm_time),
|
||||
// 顺带带回来做「已发」二次兜底:订单表 send_date 也有漏回写的情况
|
||||
// (2026-10-28 实测 144 单里 6 单状态表 send_time 有值、订单表却是空)
|
||||
sendTime: formatDateTime(r.send_time),
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
function emptySummary() {
|
||||
return {
|
||||
@ -317,6 +389,8 @@ function emptySummary() {
|
||||
voidCount: 0,
|
||||
// 已剔除的「不考核快递」单量(自提 / 中铁 / 壹米滴答 / 武进安能)
|
||||
carrierSkipped: 0,
|
||||
// 已剔除的「其实已发」单量:订单表 send_date 空、状态表 send_time 有值(漏回写)
|
||||
stateShippedSkipped: 0,
|
||||
// 不属于 11 主仓的单量(代发仓 / POP 仓 / 仓库表查不到)
|
||||
unknownWarehouse: 0,
|
||||
};
|
||||
@ -428,10 +502,23 @@ async function getShipTimeliness(options = {}) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// ③.5 起算点:批量取审核时间(最后审核时间优先,缺失回退订单创建时间)
|
||||
// ⚠️ 状态表挂了不阻断页面:全量回退 created,并在 payload 里标降级
|
||||
let oaConfirmMap = new Map();
|
||||
let oaConfirmDegraded = false;
|
||||
try {
|
||||
oaConfirmMap = await loadOaConfirmInfo(stateRows);
|
||||
} catch (e) {
|
||||
oaConfirmDegraded = true;
|
||||
console.warn('[WST] 审核时间查询失败,全量回退订单创建时间:', e.message);
|
||||
}
|
||||
|
||||
// 被剔除的作废 / 已发单计数(只用于页面说明,不进考核)
|
||||
let voidCount = 0;
|
||||
// 因「不考核快递」被跳过的单量(自提 / 中铁 / 壹米滴答 / 武进安能),写进 summary 便于对账
|
||||
let carrierSkipped = 0;
|
||||
// 订单表 send_date 为空、但状态表 send_time 有值(漏回写)→ 已发,剔除
|
||||
let stateShippedSkipped = 0;
|
||||
// 仓库表里查不到 / 不属于 11 主仓的单量(代发仓、POP 仓、wms_co_id 为空…)
|
||||
let unknownWarehouse = 0;
|
||||
|
||||
@ -467,11 +554,30 @@ async function getShipTimeliness(options = {}) {
|
||||
const bucket = acc.get(center.key);
|
||||
if (!bucket) continue;
|
||||
|
||||
// ── 起算点 = **订单创建时间**(业务 2026-10-28 定版)────────────────────
|
||||
// 业务原话:「审核时间是 jst_Orders 这个表里面的创建时间」。
|
||||
// 实测 created 100% 早于状态表的 last_oa_confirm_time,正常差 7~40 分钟,
|
||||
// 夜间下单次日才审的单差 6~9 小时(详见 registry 起算点段的量化)。
|
||||
// ── 起算点 = **审核时间**(业务 2026-10-28 定版)────────────────────────
|
||||
// 业务原话:「9837824 这个单的创建时间是 2026-10-28 09:58:42」—— 那是
|
||||
// 状态表的 last_oa_confirm_time,不是订单表的 created(10-19 23:44:22)。
|
||||
// 积压单 created → 审核能差 8~11 天,按 created 算等于把审核积压
|
||||
// 算到仓库头上(详见文件头 9837824 的证据)。
|
||||
// 审核时间取不到(实测 599 单里 209 单)→ 回退 created,并标注来源。
|
||||
const createdAt = formatDateTime(row.created);
|
||||
const oaInfo = oaConfirmMap.get(Number(row.o_id));
|
||||
|
||||
// ── ② 已发兜底:状态表说发了,就不再算未发 ─────────────────────────────
|
||||
// PR #88 换源时记录的是「状态表 send_time 漏回写」这个方向,但**反方向也会漏**:
|
||||
// 2026-10-28 实测 13:00 轮 144 单里 **6 单**状态表 send_time 有值、且有
|
||||
// 物流揽收轨迹,而订单表 send_date 仍是 NULL —— 只信订单表就会冤枉仓库。
|
||||
// ⟹ 口径:**任一方有发货时间即视为已发**,剔除量记 summary.stateShippedSkipped。
|
||||
// ⚠️ 只认 send_time,**不要**拿 `logistic_time` 当发货时间:它多数是
|
||||
// 「快递员将上门取件」的预告(实测 9983162 有轨迹但并未收件),会提前放行。
|
||||
if (oaInfo && oaInfo.sendTime) {
|
||||
stateShippedSkipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const oaConfirmAt = oaInfo ? (oaInfo.last || '') : '';
|
||||
const benchmarkAt = oaConfirmAt || createdAt;
|
||||
const benchmarkSource = oaConfirmAt ? 'oa_confirm' : 'created';
|
||||
const payAt = formatDateTime(row.pay_date);
|
||||
const statusRaw = String(row.status || '');
|
||||
|
||||
@ -480,14 +586,14 @@ async function getShipTimeliness(options = {}) {
|
||||
if (seenSo.has(dedupeKey)) continue;
|
||||
seenSo.add(dedupeKey);
|
||||
|
||||
// ── 本轮核验范围(窗口上界按**创建时间**卡)────────────────────────────
|
||||
// 13:00 轮:今天 07:00 前创建的单 + 历史未发
|
||||
// 18:00 轮:今天 16:00 前创建的单 + 历史未发(山东两仓 15:00、河南洛宁 13:00)
|
||||
// ── 本轮核验范围(窗口上界按**起算点**卡)──────────────────────────────
|
||||
// 13:00 轮:今天 07:00 前审核的单 + 历史未发
|
||||
// 18:00 轮:今天 16:00 前审核的单 + 历史未发(山东两仓 15:00、河南洛宁 13:00)
|
||||
// ⚠️ 历史未发单一律算 —— 昨天、前天乃至更早挂着没发的都要进超时列表。
|
||||
const windowEndAt = registry.centerWindowEnd(center.key, round);
|
||||
const todayStart = `${round.date} 00:00:00`;
|
||||
if (createdAt && createdAt < round.windowStart) continue; // 超出 30 天护栏
|
||||
if (createdAt && createdAt >= todayStart && createdAt > windowEndAt) continue;
|
||||
if (benchmarkAt && benchmarkAt < round.windowStart) continue; // 超出 30 天护栏
|
||||
if (benchmarkAt && benchmarkAt >= todayStart && benchmarkAt > windowEndAt) continue;
|
||||
|
||||
const base = {
|
||||
row,
|
||||
@ -496,12 +602,14 @@ async function getShipTimeliness(options = {}) {
|
||||
statusRaw,
|
||||
src: String(row.src || ''),
|
||||
createdAt,
|
||||
oaConfirmAt,
|
||||
payAt,
|
||||
// 「审核时间」= 创建时间(业务口径),前端那一列显示的就是它
|
||||
auditAt: createdAt,
|
||||
windowAt: createdAt,
|
||||
benchmarkAt: createdAt,
|
||||
benchmarkSource: 'created',
|
||||
// 起算点(审核时间;缺失回退创建时间)—— 「已等时长」从它算
|
||||
benchmarkAt,
|
||||
benchmarkSource,
|
||||
// 页面「审核时间」列显示的就是起算点(回退时会打 `*`)
|
||||
auditAt: benchmarkAt,
|
||||
windowAt: benchmarkAt,
|
||||
warehouseName,
|
||||
logisticCompany: String(row.logistics_company || '').trim(),
|
||||
logisticId: String(row.l_id || '').trim(),
|
||||
@ -540,13 +648,13 @@ async function getShipTimeliness(options = {}) {
|
||||
}
|
||||
|
||||
// ── ④ 超时判定:到核验时刻仍未发货即超时(没有「满 24 小时」门槛)────────
|
||||
if (!createdAt) {
|
||||
if (!benchmarkAt) {
|
||||
bucket.heldCount += 1;
|
||||
heldRows.push({ ...base, holdReason: '缺创建时间' });
|
||||
heldRows.push({ ...base, holdReason: '缺审核时间与创建时间' });
|
||||
continue;
|
||||
}
|
||||
// waited = 创建时间 → 本轮核验时刻(13:00 / 18:00)
|
||||
const waited = Math.max(0, diffMinutes(createdAt, round.deadline));
|
||||
// waited = 起算点(审核时间)→ 本轮核验时刻(13:00 / 18:00)
|
||||
const waited = Math.max(0, diffMinutes(benchmarkAt, round.deadline));
|
||||
|
||||
bucket.pendingCount += 1;
|
||||
const days = Math.floor(waited / 1440);
|
||||
@ -579,13 +687,14 @@ async function getShipTimeliness(options = {}) {
|
||||
oId: String(r.o_id || ''),
|
||||
soId: String(r.so_id || ''),
|
||||
shopName: String(r.shop_name || ''),
|
||||
// 创建时间 = 订单表 `created`(**本页的考核起算点**,业务 2026-10-28 定版)
|
||||
// 订单创建时间(聚水潭建单)—— 只做展示与兜底,不是考核起算点
|
||||
createdAt: x.createdAt,
|
||||
// 发货时间:候选本身就是 send_date IS NULL,所以恒为空 → 前端显示「未发货」
|
||||
sendTime: formatDateTime(r.send_date),
|
||||
// 付款时间(并列展示,方便对账)
|
||||
payTime: x.payAt,
|
||||
// 「审核时间」= 创建时间(业务口径:审核时间就是订单创建时间)
|
||||
// 「审核时间」= 起算点:状态表 last_oa_confirm_time;缺失回退 createdAt
|
||||
// benchmarkSource='created' 时前端该列会打 `*` 提示是回退值
|
||||
auditTime: x.auditAt,
|
||||
benchmarkTime: x.benchmarkAt,
|
||||
benchmarkSource: x.benchmarkSource,
|
||||
@ -663,6 +772,7 @@ async function getShipTimeliness(options = {}) {
|
||||
}, emptySummary());
|
||||
summary.voidCount = voidCount;
|
||||
summary.carrierSkipped = carrierSkipped;
|
||||
summary.stateShippedSkipped = stateShippedSkipped;
|
||||
summary.unknownWarehouse = unknownWarehouse;
|
||||
|
||||
// 新鲜度:快照时间取自上面主查询的 query_time(不再额外扫一遍 402 万行表)
|
||||
@ -706,14 +816,18 @@ async function getShipTimeliness(options = {}) {
|
||||
isPreviousDay: false,
|
||||
rounds,
|
||||
deadline: round.deadline,
|
||||
// boundary='created':轮次归属、已等时长都按**订单创建时间**(jst_Orders.created)算
|
||||
// 业务 2026-10-28 定版;此前一段用的是状态表的 last_oa_confirm_time,已废弃。
|
||||
// boundary='oa_confirm':轮次归属、已等时长都按**审核时间**算
|
||||
// (状态表 last_oa_confirm_time;取不到时回退订单 created,见 benchmarkSource)
|
||||
// 业务 2026-10-28 下午二次定版;上午那版按 created 已被 9837824 推翻。
|
||||
window: {
|
||||
start: round.windowStart,
|
||||
end: round.windowEnd,
|
||||
includeHistory: true,
|
||||
boundary: 'created',
|
||||
measuredBy: 'created',
|
||||
boundary: 'oa_confirm',
|
||||
measuredBy: 'oa_confirm',
|
||||
// 审核时间缺失、回退订单创建时间的单量(页面/钉钉需要标出来)
|
||||
fallbackToCreated: orders.filter((o) => o.benchmarkSource === 'created').length,
|
||||
fallbackDegraded: oaConfirmDegraded,
|
||||
// 仓级例外窗口(山东青岗集/山东品牌 15:00、河南洛宁 13:00,仅 18:00 轮)
|
||||
overrides: windowOverrides,
|
||||
},
|
||||
|
||||
@ -184,7 +184,7 @@ test('起算点是 orders.created,且只从订单两表取', () => {
|
||||
});
|
||||
|
||||
// ── ④ 服务层分桶(注入假查询,不连库)──
|
||||
function stubDb(dbPath, scenario = 'default') {
|
||||
function stubDb(dbPath, scenario = 'default', oaRows = []) {
|
||||
const WMS = {
|
||||
CQ: 15038426,
|
||||
SD_QGJ: 15137523,
|
||||
@ -283,7 +283,7 @@ function stubDb(dbPath, scenario = 'default') {
|
||||
|
||||
const rows = scenario === 'noon' ? noonRows : defaultRows;
|
||||
|
||||
const calls = { orders: 0, warehouse: 0, sql: [], params: [] };
|
||||
const calls = { orders: 0, oaConfirm: 0, warehouse: 0, sql: [], params: [] };
|
||||
|
||||
const handler = async (sql, params) => {
|
||||
const text = String(sql);
|
||||
@ -291,6 +291,12 @@ function stubDb(dbPath, scenario = 'default') {
|
||||
calls.warehouse += 1;
|
||||
return warehouseRows;
|
||||
}
|
||||
// 审核时间查询(状态表):默认返回空 → 服务层全部回退订单创建时间,
|
||||
// 这样老断言(按创建时间算的分钟数)继续成立;要测审核时间口径就传 oaRows
|
||||
if (/jst_order_express_details/u.test(text)) {
|
||||
calls.oaConfirm += 1;
|
||||
return oaRows;
|
||||
}
|
||||
calls.orders += 1;
|
||||
calls.sql.push(text);
|
||||
calls.params.push(params || {});
|
||||
@ -308,11 +314,11 @@ function stubDb(dbPath, scenario = 'default') {
|
||||
return { calls, WMS };
|
||||
}
|
||||
|
||||
function withStubbedService(fn, scenario = 'default') {
|
||||
function withStubbedService(fn, scenario = 'default', oaRows = []) {
|
||||
return async () => {
|
||||
const dbPath = require.resolve('../src/config/cloudDatabase');
|
||||
const realDb = require.cache[dbPath];
|
||||
const ctx = stubDb(dbPath, scenario);
|
||||
const ctx = stubDb(dbPath, scenario, oaRows);
|
||||
delete require.cache[require.resolve('../src/services/warehouseShipTimelinessService')];
|
||||
delete require.cache[require.resolve('../src/services/warehouseShipTimelinessRegistry')];
|
||||
try {
|
||||
@ -374,8 +380,8 @@ test('服务层三态分流:超时未发货 / 不计考核 / 丢弃', withStub
|
||||
|
||||
// 3 号单没创建时间 → 归「不计考核」,原因写清楚
|
||||
const noCreated = data.heldOrders.find((o) => String(o.oId) === '3');
|
||||
assert.ok(noCreated, '缺创建时间的单不能被静默丢掉');
|
||||
assert.equal(noCreated.holdReason, '缺创建时间');
|
||||
assert.ok(noCreated, '缺起算点(审核时间与创建时间都没有)的单不能被静默丢掉');
|
||||
assert.equal(noCreated.holdReason, '缺审核时间与创建时间');
|
||||
|
||||
// 创建时间晚于窗口上界 → 本轮不核验(2 号:16:20、6 号:17:00)
|
||||
assert.ok(!data.orders.some((o) => String(o.oId) === '2'), '创建 16:20 晚于 16:00 本轮不核验');
|
||||
@ -431,8 +437,8 @@ test('服务层三态分流:超时未发货 / 不计考核 / 丢弃', withStub
|
||||
const reasons = data.heldOrders.map((h) => h.holdReason).sort();
|
||||
assert.deepEqual(
|
||||
reasons,
|
||||
['缺创建时间', '待审核', '订单异常', '订单异常'].sort(),
|
||||
'4 单:缺创建时间 1 + 待审核 1 + 异常 2'
|
||||
['缺审核时间与创建时间', '待审核', '订单异常', '订单异常'].sort(),
|
||||
'4 单:缺起算点 1 + 待审核 1 + 异常 2'
|
||||
);
|
||||
|
||||
// 新鲜度:取自主查询的 MAX(updated_at)
|
||||
@ -482,11 +488,11 @@ test('服务层三态分流:超时未发货 / 不计考核 / 丢弃', withStub
|
||||
// round 必须带出 windowEnd,否则页面渲染成「核验 前」
|
||||
assert.equal(data.round.key, 'AFTERNOON');
|
||||
assert.match(String(data.round.windowEnd), /16:00:00$/u);
|
||||
assert.equal(data.window.boundary, 'created', '窗口按创建时间判,要透出给前端');
|
||||
assert.equal(data.window.measuredBy, 'created', '已等时长按创建时间算');
|
||||
assert.equal(data.window.boundary, 'oa_confirm', '窗口按审核时间判,要透出给前端');
|
||||
assert.equal(data.window.measuredBy, 'oa_confirm', '已等时长按审核时间算');
|
||||
}));
|
||||
|
||||
test('13:00 轮:只核验「今天 07:00 前创建」的单 + 历史未发', withStubbedService(async (svc) => {
|
||||
test('13:00 轮:只核验「今天 07:00 前审核」的单 + 历史未发', withStubbedService(async (svc) => {
|
||||
const data = await svc.getShipTimeliness({ now: at(13, 5), limit: 10 });
|
||||
assert.equal(data.round.key, 'NOON');
|
||||
assert.equal(data.round.windowEnd, '2026-10-20 07:00:00');
|
||||
@ -514,7 +520,7 @@ test('13:00 轮:只核验「今天 07:00 前创建」的单 + 历史未发', w
|
||||
// 13:00 轮 = 21 + 22 + 23 + 25 + 26 = 5 单
|
||||
// (25/26 都是「今天之前创建、至今未发」,历史未发池两个轮次都算)
|
||||
assert.equal(data.summary.overdueUnshipped, 5);
|
||||
assert.equal(data.window.boundary, 'created', '窗口按创建时间判');
|
||||
assert.equal(data.window.boundary, 'oa_confirm', '窗口按审核时间判(取不到回退创建时间)');
|
||||
// 仓级例外只在 18:00 轮生效,13:00 轮没有例外
|
||||
assert.equal(data.window.overrides.length, 0, '13:00 轮统一 07:00,无仓级例外');
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ test('补推窗口:错过时刻 30 分钟内启动仍会补,超窗口不补'
|
||||
test('正文包含轮次、超时单数、涉及仓数、最久天数', () => {
|
||||
const built = job.buildNotifyText(sampleData(), { maxDetailRows: 30 });
|
||||
assert.match(built.text, /【仓储中心发货时效】13:00 轮/);
|
||||
assert.match(built.text, /核验 07:00 前创建/);
|
||||
assert.match(built.text, /核验 07:00 前审核/);
|
||||
assert.match(built.text, /超时未发货 3 单,涉及 2 个仓,最久已等 4 天/);
|
||||
assert.equal(built.rowCount, 3);
|
||||
});
|
||||
@ -138,11 +138,11 @@ test('按仓块只列超时 > 0 的仓,并按单数降序', () => {
|
||||
assert.doesNotMatch(built.text, /睢宁仓 0 单/);
|
||||
});
|
||||
|
||||
test('明细一行 = 仓|内部单号|线上单号|快递|创建 时间|已等', () => {
|
||||
test('明细一行 = 仓|内部单号|线上单号|快递|审核 时间|已等', () => {
|
||||
const built = job.buildNotifyText(sampleData(), { maxDetailRows: 30 });
|
||||
assert.match(built.text, /1\. 简尚-崔桥工厂仓|9855123|261021-301318827670255|武进韵达|创建 06:32|已等 6 小时 28 分/);
|
||||
assert.match(built.text, /1\. 简尚-崔桥工厂仓|9855123|261021-301318827670255|武进韵达|审核 06:32|已等 6 小时 28 分/);
|
||||
// 无线上单号 → 「—」;无快递名 → 「—」;无创建时间 → 「无创建时间」;都不能出现 undefined
|
||||
assert.match(built.text, /3\. 简尚-横林二仓|9855300|—|—|创建 无创建时间|已等 2 小时/);
|
||||
assert.match(built.text, /3\. 简尚-横林二仓|9855300|—|—|审核 —|已等 2 小时/);
|
||||
assert.doesNotMatch(built.text, /undefined|NaN/);
|
||||
});
|
||||
|
||||
@ -151,20 +151,20 @@ test('快递列紧跟线上单号(业务 2026-10-27 要求:群里直接看
|
||||
const line = built.text.split('\n').find((l) => l.startsWith('2. 简尚-崔桥工厂仓'));
|
||||
assert.ok(line, '明细第 2 行不见了');
|
||||
const parts = line.replace(/^\d+\.\s*/, '').split('|');
|
||||
assert.deepEqual(parts, ['简尚-崔桥工厂仓', '9855200', 'SO20261027001', '常州安能新', '创建 10-26 09:05', '已等 1 天 23 小时']);
|
||||
assert.deepEqual(parts, ['简尚-崔桥工厂仓', '9855200', 'SO20261027001', '常州安能新', '审核 10-26 09:05', '已等 1 天 23 小时']);
|
||||
});
|
||||
|
||||
test('历史未发单的创建时间必须带 MM-DD,否则会被读成「今天 16:13」', () => {
|
||||
test('历史未发单的审核时间必须带 MM-DD,否则会被读成「今天 16:13」', () => {
|
||||
// 13:00 轮窗口上界 07:00,但口径「含历史未发」→ 明细里会出现昨天的单
|
||||
const built = job.buildNotifyText(sampleData(), { maxDetailRows: 30 });
|
||||
assert.match(built.text, /2\. 简尚-崔桥工厂仓|9855200|SO20261027001|常州安能新|创建 10-26 09:05|已等 1 天 23 小时/);
|
||||
assert.match(built.text, /2\. 简尚-崔桥工厂仓|9855200|SO20261027001|常州安能新|审核 10-26 09:05|已等 1 天 23 小时/);
|
||||
// 同一天的就只给 HH:mm,别啰嗦
|
||||
assert.match(built.text, /创建 06:32/);
|
||||
assert.doesNotMatch(built.text, /创建 10-27 06:32/);
|
||||
assert.match(built.text, /审核 06:32/);
|
||||
assert.doesNotMatch(built.text, /审核 10-27 06:32/);
|
||||
// 直接测函数本身的跨天判定
|
||||
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('不计考核 / 不考核快递 / 数据未覆盖核验点都要在正文里说明', () => {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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: '仓储中心24小时发货时效', icon: 'inbox', routeKey: 'warehouse-ship-timeliness', visible: true, order: 110 },
|
||||
{ key: 'warehouse-ship-timeliness', type: 'item', label: '仓储中心发货时效', 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,
|
||||
|
||||
@ -80,7 +80,7 @@ const NOTIFY_HELP_ITEMS = [
|
||||
label: '③ 为什么一天推两次、时间怎么定',
|
||||
children: (
|
||||
<div className="wst-notify__help">
|
||||
业务口径:<b>13:00 核验 7 点前创建的超时单</b>、<b>18:00 核验 16 点前创建的单</b>,
|
||||
业务口径:<b>13:00 核验 7 点前审核的超时单</b>、<b>18:00 核验 16 点前审核的单</b>,
|
||||
各自<b>核验完成后</b>推一次,所以是两个独立时刻,互不影响。
|
||||
<br />
|
||||
默认给到 <b>13:05 / 18:05</b>(核验点 + 5 分钟):订单表的 updateTime 要晚几分钟才覆盖到核验时刻,
|
||||
@ -165,12 +165,20 @@ const HELD_COLUMNS = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: (
|
||||
<Tooltip title="聚水潭「审核时间」(last_oa_confirm_time)。带 * 的表示没取到审核时间,已回退订单创建时间">
|
||||
<span>审核时间</span>
|
||||
</Tooltip>
|
||||
),
|
||||
dataIndex: 'auditTime',
|
||||
width: 152,
|
||||
ellipsis: true,
|
||||
render: (v) => (v || <span className="wst-detail__fallback">无创建时间</span>),
|
||||
render: (v, row) => {
|
||||
if (!v) return <span className="wst-detail__fallback">无审核时间</span>;
|
||||
return row?.benchmarkSource === 'created' ? `${v} *` : v;
|
||||
},
|
||||
},
|
||||
{ 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} 天` : '—') },
|
||||
];
|
||||
@ -224,22 +232,39 @@ const DETAIL_COLUMNS = [
|
||||
},
|
||||
},
|
||||
{
|
||||
// 创建时间 = 本页的**考核起算点**(业务 2026-10-28 定版:审核时间就是订单创建时间)
|
||||
// 审核时间 = 本页的**考核起算点**(业务 2026-10-28 下午二次定版)
|
||||
// 取状态表 last_oa_confirm_time;取不到回退订单创建时间并打 `*`
|
||||
title: (
|
||||
<Tooltip title="取订单表 jst_Orders 的创建时间(业务口径:审核时间即订单创建时间)。今天 7:00 前创建的 13:00 核验;截止 16:00 前创建的(含今天之前未发的历史单)18:00 核验,已等时长也从它算">
|
||||
<span>创建时间</span>
|
||||
<Tooltip title="聚水潭「审核时间」(last_oa_confirm_time),这才是仓库真正接到单的时刻。今天 7:00 前审核的 13:00 核验;截止 16:00 前审核的(含历史未发单)18:00 核验,已等时长也从它算。带 * 的表示没取到审核时间,已回退订单创建时间">
|
||||
<span>审核时间</span>
|
||||
</Tooltip>
|
||||
),
|
||||
dataIndex: 'auditTime',
|
||||
width: 152,
|
||||
ellipsis: true,
|
||||
render: (v, row) => (v
|
||||
? v
|
||||
: (
|
||||
<Tooltip title="没取到创建时间,已回退付款时间参与分轮与计时">
|
||||
<span className="wst-detail__fallback">{row?.benchmarkTime || '—'} *</span>
|
||||
</Tooltip>
|
||||
)),
|
||||
render: (v, row) => {
|
||||
if (!v) return <span className="wst-detail__fallback">—</span>;
|
||||
if (row?.benchmarkSource === 'created') {
|
||||
return (
|
||||
<Tooltip title="状态表里取不到这个单的审核时间,已回退订单创建时间参与分轮与计时(`*` 为标记)">
|
||||
<span>{v} *</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return v;
|
||||
},
|
||||
},
|
||||
// 下单时间 = 客户在平台下单那一刻(只做参考对账,不参与计时)
|
||||
{
|
||||
title: (
|
||||
<Tooltip title="聚水潭订单创建时间(jst_Orders.created),即客户下单时刻。只做参考,不参与分轮与计时">
|
||||
<span>下单时间</span>
|
||||
</Tooltip>
|
||||
),
|
||||
dataIndex: 'createdAt',
|
||||
width: 152,
|
||||
ellipsis: true,
|
||||
render: (v) => v || '—',
|
||||
},
|
||||
// 付款时间 = 客户付款那一刻(并列展示,方便对账)
|
||||
{ title: '付款时间', dataIndex: 'payTime', width: 152, ellipsis: true, render: (v) => v || '—' },
|
||||
@ -264,7 +289,7 @@ const DETAIL_COLUMNS = [
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Tooltip title="从订单创建时间到本轮核验时刻(13:00 / 18:00)等了多久">
|
||||
<Tooltip title="从审核时间(仓库接到单的时刻)到本轮核验时刻(13:00 / 18:00)等了多久">
|
||||
<span>已等时长</span>
|
||||
</Tooltip>
|
||||
),
|
||||
@ -295,11 +320,16 @@ const EXPORT_COLUMNS = [
|
||||
{ title: '发货状态', dataIndex: 'state' },
|
||||
{ title: '发货时间', dataIndex: 'sendTime', exportValue: (v) => v || '未发货' },
|
||||
{
|
||||
title: '创建时间',
|
||||
title: '审核时间',
|
||||
dataIndex: 'auditTime',
|
||||
// 创建时间缺失时页面会回退付款时间并打 `*`,导出也保持一致,否则对账对不上
|
||||
exportValue: (v, row) => v || (row?.benchmarkTime ? `${row.benchmarkTime} *` : '无创建时间'),
|
||||
// 起算点 = 审核时间;没取到时回退订单创建时间,导出用 `*` 标出来,否则对账对不上
|
||||
exportValue: (v, row) => {
|
||||
const t = v || row?.benchmarkTime || '';
|
||||
if (!t) return '无审核时间';
|
||||
return row?.benchmarkSource === 'created' ? `${t} *` : t;
|
||||
},
|
||||
},
|
||||
{ title: '下单时间', dataIndex: 'createdAt', exportValue: (v) => v || '—' },
|
||||
{ title: '付款时间', dataIndex: 'payTime' },
|
||||
// 分钟数给一列原始值,业务在 Excel 里能直接排序/筛选;人读的那列再格式化
|
||||
{ title: '已等(分钟)', dataIndex: 'overdueMinutes' },
|
||||
@ -320,17 +350,18 @@ const roundFileText = (roundInfo) => (
|
||||
);
|
||||
|
||||
/**
|
||||
* 仓储中心 24 小时发货时效(独立页面)
|
||||
* 仓储中心发货时效(独立页面)
|
||||
* ────────────────────────────────────────────────────────────────
|
||||
* 业务口径(yanhui 2026-10-20 定轮次;2026-10-28 定起算点):
|
||||
* · 11 个主仓,子仓并入主仓一起算;
|
||||
* · 数据源 = 订单表 jst_Orders + jst_qmcust_orderlist(两张表店铺互斥,都要读);
|
||||
* · 今天 07:00 前**创建**的订单 13:00 核验,截止今天 16:00 前创建的单 18:00 核验
|
||||
* (含 7 点前未发出的、含今天之前创建至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
|
||||
* 今天之前创建仍未发的(历史未发)两个轮次都算;
|
||||
* · 今天 07:00 前**审核**的订单 13:00 核验,截止今天 16:00 前审核的单 18:00 核验
|
||||
* (含 7 点前审核未发出的、含今天之前审核至今未发的历史单;山东两仓 15:00、河南洛宁 13:00);
|
||||
* 今天之前仍未发的(历史未发)两个轮次都算;
|
||||
* **到核验时刻未发即超时**,没有「满 24 小时」门槛(业务纠偏点);
|
||||
* 轮次归属和「已等时长」都按**创建时间**(jst_Orders.created)算 ——
|
||||
* 业务口径:聚水潭「审核时间」指的就是订单创建时间;
|
||||
* 轮次归属和「已等时长」都按**审核时间**(状态表 last_oa_confirm_time)算 ——
|
||||
* 业务 2026-10-28 下午二次定版:仓库只有审核后才会接到单,按下单时间算会把审核积压算到仓库头上;
|
||||
* 审核时间取不到的单回退订单创建时间,页面上带 `*` 标记;
|
||||
* · **13:00 之前不核验**,直接显示「未到核验时间」,不拿昨天的结果冒充今天;
|
||||
* · 已出库 / 待出库以订单表 **send_date** 为准(SQL 直接 `send_date IS NULL` 才进候选);
|
||||
* · 只显示超时未出库(发完的仓显示「—」),点仓库可筛右侧明细。
|
||||
@ -621,23 +652,25 @@ function WarehouseShipTimeliness() {
|
||||
return (
|
||||
<SycmPage className="wst-page">
|
||||
<header className="wst-header">
|
||||
<Title level={3}>仓储中心 24 小时发货时效</Title>
|
||||
<Title level={3}>仓储中心发货时效(到点未发即超时)</Title>
|
||||
<Text type="secondary">
|
||||
按聚水潭订单表实时状态核验 11 个主仓是否在承诺时效内完成发货,只列超时未发货的单量。
|
||||
{/* 2026-10-28 业务定:名字里不再出现「24 小时」—— 规则本身没有 24h 门槛,
|
||||
原名会让 10-27 16:00 创建(只等了 21 小时)的单看着像「没到时限也被判超时」。*/}
|
||||
</Text>
|
||||
<Text type="secondary" className="wst-note">
|
||||
每天 <b>13:00</b> 核验「<b>今天 07:00 前创建</b>」、<b>18:00</b> 核验「<b>截止 16:00 前创建</b>」、
|
||||
到核验时刻仍未发货的订单;两个轮次都<b>包含今天之前创建、至今未发的历史未发单</b>
|
||||
(18:00 轮也会带上 7 点前创建、13:00 还没发出的那批)—— 滚动待发池,不按自然日切。
|
||||
每天 <b>13:00</b> 核验「<b>今天 07:00 前审核</b>」、<b>18:00</b> 核验「<b>截止 16:00 前审核</b>」、
|
||||
到核验时刻仍未发货的订单;两个轮次都<b>包含今天之前审核、至今未发的历史未发单</b>
|
||||
(18:00 轮也会带上 7 点前审核、13:00 还没发出的那批)—— 滚动待发池,不按自然日切。
|
||||
13:00 之前不核验,显示「未到核验时间」。
|
||||
<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>(= 聚水潭「审核时间」,业务口径即订单创建时间 jst_Orders.created);
|
||||
明细并列「创建时间 / 发货时间」,方便直接和聚水潭对账。
|
||||
<b>已发 / 未发直接取订单表 send_date</b>:send_date 有值就是已发(SQL 侧直接过滤,
|
||||
根本不进候选),为空的才是未发;不再读状态镜像表的 send_time ——
|
||||
那张表会漏回写(近 7 天 563 单已发却仍算未发)甚至整批停更,是这次换源的根因。
|
||||
<b>仓级例外</b>:18:00 轮里 <b>山东青岗集仓 / 山东品牌仓</b> 只看 <b>15:00 前审核</b>的单、
|
||||
<b>河南洛宁仓</b> 只看 <b>13:00 前审核</b>的单(核验时刻仍是 18:00)。
|
||||
<b>轮次与「已等时长」都按审核时间算</b>(聚水潭 last_oa_confirm_time:
|
||||
审核才是仓库接到单的时刻,按下单时间算会把客服审核积压算到仓库头上);
|
||||
明细并列「审核时间 / 下单时间 / 发货时间」,方便直接和聚水潭对账。
|
||||
<b>已发 / 未发以订单表 send_date 为准</b>(有值即已发,SQL 侧直接过滤不进候选);
|
||||
订单表 send_date 没回写、但状态表 send_time 有值的,也按已发剔除(不漏冤仓库)。
|
||||
<b>已打单但还没揽收的</b>(聚水潭已生成运单号、发货时间还空着)会标成「已打单 · 未揽收」,
|
||||
鼠标悬停能看到运单号 —— 这类单还留在列表里,等聚水潭回传发货时间才消失。
|
||||
<b>{excludedCarrierText} 不考核</b>(客户自提、专线大件或加盟快运网点,揽收与在途仓库控制不了),
|
||||
@ -654,7 +687,7 @@ function WarehouseShipTimeliness() {
|
||||
<>
|
||||
<span className="wst-status__badge">{round.label}</span>
|
||||
<span className="wst-status__label">
|
||||
核验今天 {cutoffText} 前创建的订单
|
||||
核验今天 {cutoffText} 前审核的订单
|
||||
</span>
|
||||
<span className="wst-status__window">
|
||||
含历史未发 · 到点未发即超时
|
||||
@ -666,7 +699,7 @@ function WarehouseShipTimeliness() {
|
||||
<span className="wst-status__badge wst-status__badge--idle">未到核验时间</span>
|
||||
<span className="wst-status__label">
|
||||
{nextRoundText} 开始首轮核验
|
||||
(核验今天 {nextWindowEndText} 前创建的订单)
|
||||
(核验今天 {nextWindowEndText} 前审核的订单)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@ -709,7 +742,7 @@ function WarehouseShipTimeliness() {
|
||||
<Card bordered={false} className="wst-stat">
|
||||
<Statistic
|
||||
title={(
|
||||
<Tooltip title="到 13:00 / 18:00 核验时刻仍未发货的订单(按订单创建时间分轮),不设满 24 小时门槛">
|
||||
<Tooltip title="到 13:00 / 18:00 核验时刻仍未发货的订单(按审核时间分轮),不设满 24 小时门槛">
|
||||
<span>超时未发货</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -775,7 +808,7 @@ function WarehouseShipTimeliness() {
|
||||
? '系统仓库表中暂无同名仓,待上游建仓后自动接入'
|
||||
: notStarted
|
||||
? `今日 ${nextRoundText} 首轮核验开始后才展示超时数据`
|
||||
: `${c.name}:本轮核验 ${String(c.windowEnd || '').slice(11, 16) || windowEndText} 前创建的单,`
|
||||
: `${c.name}:本轮核验 ${String(c.windowEnd || '').slice(11, 16) || windowEndText} 前审核的单,`
|
||||
+ `超时未发货 ${c.overdueUnshipped} 单`
|
||||
+ `${c.oldestDays > 0 ? `(最久已等 ${c.oldestDays} 天)` : ''}`
|
||||
+ `${c.heldCount > 0 ? `\n另有不计考核(待审核/异常)${c.heldCount} 单` : ''}`
|
||||
@ -957,7 +990,7 @@ function WarehouseShipTimeliness() {
|
||||
setNotifyForm((old) => ({ ...old, noonSendTime: value.format('HH:mm') }));
|
||||
}}
|
||||
/>
|
||||
<Text type="secondary">核验「今天 07:00 前创建」的超时单,默认核验点 + 5 分钟</Text>
|
||||
<Text type="secondary">核验「今天 07:00 前审核」的超时单,默认核验点 + 5 分钟</Text>
|
||||
</div>
|
||||
|
||||
<div className="wst-notify__row">
|
||||
@ -973,7 +1006,7 @@ function WarehouseShipTimeliness() {
|
||||
setNotifyForm((old) => ({ ...old, afternoonSendTime: value.format('HH:mm') }));
|
||||
}}
|
||||
/>
|
||||
<Text type="secondary">核验「截止 16:00 前创建」的超时单(含历史未发),默认核验点 + 5 分钟</Text>
|
||||
<Text type="secondary">核验「截止 16:00 前审核」的超时单(含历史未发),默认核验点 + 5 分钟</Text>
|
||||
</div>
|
||||
|
||||
<div className="wst-notify__row">
|
||||
@ -1075,11 +1108,12 @@ function WarehouseShipTimeliness() {
|
||||
|
||||
<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 核验
|
||||
(含 7 点前创建、13:00 还没发出的,也含今天之前创建至今未发的历史单)。
|
||||
<b>今天 7:00 前审核</b>的订单 13:00 核验;<b>截止 16:00 前审核</b>的单 18:00 核验
|
||||
(含 7 点前审核、13:00 还没发出的,也含今天之前审核至今未发的历史单)。
|
||||
<b>到点仍未发货即超时</b>(不设「满 24 小时」门槛);
|
||||
轮次与「已等时长」都按创建时间(jst_Orders.created)算 —— 业务口径的「审核时间」即订单创建时间。
|
||||
发货 = 订单表 send_date(为空才算未发);状态只取「发货中」。
|
||||
轮次与「已等时长」都按<b>审核时间</b>算(状态表 last_oa_confirm_time,审核才是仓库接到单的时刻;
|
||||
取不到审核时间的单回退订单创建时间,列上带 <b>*</b>)。
|
||||
发货 = 订单表 send_date(为空才算未发,但状态表 send_time 有值的也判已发);状态只取「发货中」。
|
||||
<b>作废单不考核</b>(labels 含「发货前售后」= 客户发货前取消)
|
||||
{summary.voidCount > 0 ? ` —— 本轮已自动剔除 ${summary.voidCount} 单` : ''};
|
||||
<b>{excludedCarrierText}不考核</b>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user