From e7e0b96b464d8e14c71147a79555fbe4a6ca2f22 Mon Sep 17 00:00:00 2001 From: yanhui <2353496031@qq.com> Date: Wed, 28 Oct 2026 15:34:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(wst):=20=E8=B5=B7=E7=AE=97=E7=82=B9?= =?UTF-8?q?=E6=94=B9=E5=9B=9E=E3=80=8C=E6=9C=80=E5=90=8E=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E3=80=8D=EF=BC=8C=E5=B9=B6=E5=89=94=E9=99=A4?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=A1=A8=E6=BC=8F=E5=9B=9E=E5=86=99=E7=9A=84?= =?UTF-8?q?=E5=B7=B2=E5=8F=91=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 业务 2026-10-28 下午拿 9837824 指出:页面「创建时间」显示 10-19 23:44, 而该单实际是 10-28 09:58:42 才审核。逐字段核对确认起算点口径错了 —— 上午那版按 jst_Orders.created 算,等于把客服审核积压算到仓库头上。 - 起算点 = 状态表 last_oa_confirm_time(业务选「最后审核时间」); 取不到审核时间的单回退 created,页面 / 导出 / 钉钉该列打 * - 已发判定补一条兜底:订单表 send_date 空、但状态表 send_time 有值的按已发剔除 (实测 144 单里 6 单属此类,且有揽收轨迹) - 明细与导出列改为「审核时间 / 下单时间 / 发货时间」并列,去掉旧「创建时间」列 - 钉钉明细第五段由「创建」改回「审核」;抬头「核验 XX 前创建」改「前审核」 - 回归测试同步改断言;stub 增加审核时间查询分支(默认空 = 全量回退 created) 实测(10-28 13:00 轮):599 → 144(按审核时间分轮)→ 138(剔除漏回写已发单)。 599 单里 376 单是当天 08:10~10:30 才批量审核的积压单,491 单其实已有运单号。 验收:后端 check 491/493(2 个失败为既有 Windows 权限 / TTL 用例,改动前同样失败); 前端 eslint 通过、build 23.92s 通过;本地真渲染 138 单、表头含「审核时间 / 下单时间」、 回退单带 *、无 pageerror。 --- .../jobs/warehouseShipTimelinessNotifyJob.js | 36 +-- .../warehouseShipTimelinessRegistry.js | 30 ++- .../warehouseShipTimelinessService.js | 208 ++++++++++++++---- backend/test/warehouseShipTimeliness.test.js | 30 ++- .../warehouseShipTimelinessNotify.test.js | 20 +- .../pages/WarehouseShipTimeliness/index.jsx | 116 ++++++---- 6 files changed, 303 insertions(+), 137 deletions(-) diff --git a/backend/src/jobs/warehouseShipTimelinessNotifyJob.js b/backend/src/jobs/warehouseShipTimelinessNotifyJob.js index 2c06fff..9fe41ed 100644 --- a/backend/src/jobs/warehouseShipTimelinessNotifyJob.js +++ b/backend/src/jobs/warehouseShipTimelinessNotifyJob.js @@ -7,11 +7,12 @@ * 「发货时效也需要钉钉通知: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} 天` : ''), ]; diff --git a/backend/src/services/warehouseShipTimelinessRegistry.js b/backend/src/services/warehouseShipTimelinessRegistry.js index 9b90bc9..bbc539a 100644 --- a/backend/src/services/warehouseShipTimelinessRegistry.js +++ b/backend/src/services/warehouseShipTimelinessRegistry.js @@ -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 直接从主查询取),保留给历史脚本/测试引用。 diff --git a/backend/src/services/warehouseShipTimelinessService.js b/backend/src/services/warehouseShipTimelinessService.js index d106d3d..536b20b 100644 --- a/backend/src/services/warehouseShipTimelinessService.js +++ b/backend/src/services/warehouseShipTimelinessService.js @@ -4,19 +4,13 @@ * 仓储中心发货时效 —— 服务层 * * ═══════════════════════════════════════════════════════════════════════════ - * 🔥 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= 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, }, diff --git a/backend/test/warehouseShipTimeliness.test.js b/backend/test/warehouseShipTimeliness.test.js index 29da148..df26185 100644 --- a/backend/test/warehouseShipTimeliness.test.js +++ b/backend/test/warehouseShipTimeliness.test.js @@ -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,无仓级例外'); diff --git a/backend/test/warehouseShipTimelinessNotify.test.js b/backend/test/warehouseShipTimelinessNotify.test.js index d914574..ddcde5e 100644 --- a/backend/test/warehouseShipTimelinessNotify.test.js +++ b/backend/test/warehouseShipTimelinessNotify.test.js @@ -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('不计考核 / 不考核快递 / 数据未覆盖核验点都要在正文里说明', () => { diff --git a/frontend/src/pages/WarehouseShipTimeliness/index.jsx b/frontend/src/pages/WarehouseShipTimeliness/index.jsx index b216b59..57a47d1 100644 --- a/frontend/src/pages/WarehouseShipTimeliness/index.jsx +++ b/frontend/src/pages/WarehouseShipTimeliness/index.jsx @@ -80,7 +80,7 @@ const NOTIFY_HELP_ITEMS = [ label: '③ 为什么一天推两次、时间怎么定', children: (
- 业务口径:13:00 核验 7 点前创建的超时单、18:00 核验 16 点前创建的单, + 业务口径:13:00 核验 7 点前审核的超时单、18:00 核验 16 点前审核的单, 各自核验完成后推一次,所以是两个独立时刻,互不影响。
默认给到 13:05 / 18:05(核验点 + 5 分钟):订单表的 updateTime 要晚几分钟才覆盖到核验时刻, @@ -165,12 +165,20 @@ const HELD_COLUMNS = [ }, }, { - title: '创建时间', + title: ( + + 审核时间 + + ), dataIndex: 'auditTime', width: 152, ellipsis: true, - render: (v) => (v || 无创建时间), + render: (v, row) => { + if (!v) return 无审核时间; + 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: ( - - 创建时间 + + 审核时间 ), dataIndex: 'auditTime', width: 152, ellipsis: true, - render: (v, row) => (v - ? v - : ( - - {row?.benchmarkTime || '—'} * - - )), + render: (v, row) => { + if (!v) return —; + if (row?.benchmarkSource === 'created') { + return ( + + {v} * + + ); + } + return v; + }, + }, + // 下单时间 = 客户在平台下单那一刻(只做参考对账,不参与计时) + { + title: ( + + 下单时间 + + ), + 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: ( - + 已等时长 ), @@ -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' }, @@ -325,12 +355,13 @@ const roundFileText = (roundInfo) => ( * 业务口径(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` 才进候选); * · 只显示超时未出库(发完的仓显示「—」),点仓库可筛右侧明细。 @@ -628,18 +659,18 @@ function WarehouseShipTimeliness() { 原名会让 10-27 16:00 创建(只等了 21 小时)的单看着像「没到时限也被判超时」。*/} - 每天 13:00 核验「今天 07:00 前创建」、18:00 核验「截止 16:00 前创建」、 - 到核验时刻仍未发货的订单;两个轮次都包含今天之前创建、至今未发的历史未发单 - (18:00 轮也会带上 7 点前创建、13:00 还没发出的那批)—— 滚动待发池,不按自然日切。 + 每天 13:00 核验「今天 07:00 前审核」、18:00 核验「截止 16:00 前审核」、 + 到核验时刻仍未发货的订单;两个轮次都包含今天之前审核、至今未发的历史未发单 + (18:00 轮也会带上 7 点前审核、13:00 还没发出的那批)—— 滚动待发池,不按自然日切。 13:00 之前不核验,显示「未到核验时间」。 到点还没发就算超时 —— 不设「满 24 小时」门槛(早上 7 点前那份单,13:00 还没走就是超时)。 - 仓级例外:18:00 轮里 山东青岗集仓 / 山东品牌仓 只看 15:00 前创建的单、 - 河南洛宁仓 只看 13:00 前创建的单(核验时刻仍是 18:00)。 - 轮次与「已等时长」都按创建时间算(= 聚水潭「审核时间」,业务口径即订单创建时间 jst_Orders.created); - 明细并列「创建时间 / 发货时间」,方便直接和聚水潭对账。 - 已发 / 未发直接取订单表 send_date:send_date 有值就是已发(SQL 侧直接过滤, - 根本不进候选),为空的才是未发;不再读状态镜像表的 send_time —— - 那张表会漏回写(近 7 天 563 单已发却仍算未发)甚至整批停更,是这次换源的根因。 + 仓级例外:18:00 轮里 山东青岗集仓 / 山东品牌仓 只看 15:00 前审核的单、 + 河南洛宁仓 只看 13:00 前审核的单(核验时刻仍是 18:00)。 + 轮次与「已等时长」都按审核时间算(聚水潭 last_oa_confirm_time: + 审核才是仓库接到单的时刻,按下单时间算会把客服审核积压算到仓库头上); + 明细并列「审核时间 / 下单时间 / 发货时间」,方便直接和聚水潭对账。 + 已发 / 未发以订单表 send_date 为准(有值即已发,SQL 侧直接过滤不进候选); + 订单表 send_date 没回写、但状态表 send_time 有值的,也按已发剔除(不漏冤仓库)。 已打单但还没揽收的(聚水潭已生成运单号、发货时间还空着)会标成「已打单 · 未揽收」, 鼠标悬停能看到运单号 —— 这类单还留在列表里,等聚水潭回传发货时间才消失。 {excludedCarrierText} 不考核(客户自提、专线大件或加盟快运网点,揽收与在途仓库控制不了), @@ -656,7 +687,7 @@ function WarehouseShipTimeliness() { <> {round.label} - 核验今天 {cutoffText} 前创建的订单 + 核验今天 {cutoffText} 前审核的订单 含历史未发 · 到点未发即超时 @@ -668,7 +699,7 @@ function WarehouseShipTimeliness() { 未到核验时间 {nextRoundText} 开始首轮核验 - (核验今天 {nextWindowEndText} 前创建的订单) + (核验今天 {nextWindowEndText} 前审核的订单) )} @@ -711,7 +742,7 @@ function WarehouseShipTimeliness() { + 超时未发货 )} @@ -777,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} 单` : ''}` @@ -959,7 +990,7 @@ function WarehouseShipTimeliness() { setNotifyForm((old) => ({ ...old, noonSendTime: value.format('HH:mm') })); }} /> - 核验「今天 07:00 前创建」的超时单,默认核验点 + 5 分钟 + 核验「今天 07:00 前审核」的超时单,默认核验点 + 5 分钟
@@ -975,7 +1006,7 @@ function WarehouseShipTimeliness() { setNotifyForm((old) => ({ ...old, afternoonSendTime: value.format('HH:mm') })); }} /> - 核验「截止 16:00 前创建」的超时单(含历史未发),默认核验点 + 5 分钟 + 核验「截止 16:00 前审核」的超时单(含历史未发),默认核验点 + 5 分钟
@@ -1077,11 +1108,12 @@ function WarehouseShipTimeliness() { 说明:数据源 = 聚水潭订单表 jst_Orders + jst_qmcust_orderlist(两张表店铺互斥,按 o_id 去重)。 - 今天 7:00 前创建的订单 13:00 核验;截止 16:00 前创建的单 18:00 核验 - (含 7 点前创建、13:00 还没发出的,也含今天之前创建至今未发的历史单)。 + 今天 7:00 前审核的订单 13:00 核验;截止 16:00 前审核的单 18:00 核验 + (含 7 点前审核、13:00 还没发出的,也含今天之前审核至今未发的历史单)。 到点仍未发货即超时(不设「满 24 小时」门槛); - 轮次与「已等时长」都按创建时间(jst_Orders.created)算 —— 业务口径的「审核时间」即订单创建时间。 - 发货 = 订单表 send_date(为空才算未发);状态只取「发货中」。 + 轮次与「已等时长」都按审核时间算(状态表 last_oa_confirm_time,审核才是仓库接到单的时刻; + 取不到审核时间的单回退订单创建时间,列上带 *)。 + 发货 = 订单表 send_date(为空才算未发,但状态表 send_time 有值的也判已发);状态只取「发货中」。 作废单不考核(labels 含「发货前售后」= 客户发货前取消) {summary.voidCount > 0 ? ` —— 本轮已自动剔除 ${summary.voidCount} 单` : ''}; {excludedCarrierText}不考核