feat: 新增「仓储中心24小时发货时效」侧栏(11 个主仓 · 12:00/16:00 双轮核验) #68

Merged
yanhui merged 1 commits from codex/warehouse-ship-timeliness-1020 into main 2026-10-20 11:28:12 +08:00
Collaborator

目的与范围

在 MKH-BOM 列表左侧新增「仓储中心24小时发货时效」侧栏(需求提出:yanhui 2026-10-20)。

业务需求原文

在列表左侧加一栏仓储中心24小时发货时效,在这一栏下面加以下子栏,横林二仓,横林三仓,
横林二仓品牌仓,崔桥工厂仓,崔桥工厂品牌仓,崔桥工厂半成品仓,新苏北仓,睢宁仓,
山东青岗集仓,山东品牌仓,河南洛宁仓,从系统库存抓取已发货订单以及未发货订单,
按照订单创建时间来算发货是否及时,每天中午 12:00,核实当天创建时间 7:00 之前是否已完成发货,
每天下午 16:00,核实当天创建时间 16:00 前订单是否已完成

已确认的三条设计决策

决策点 结论
子仓归属 连子仓一起算(京东云仓 / 自营仓 / 运营C专用仓 / 唯品仓等并入主仓)
崔桥工厂半成品仓 系统无同名仓 → 先留空占位(不映射到配件仓 15718196)
展示结构 只显示超时未发

改动文件

  • 新增 backend/src/services/warehouseShipTimelinessRegistry.js —— 主仓关键词、核验轮次、状态排除口径
  • 新增 backend/src/services/warehouseShipTimelinessService.js —— 取数与三态聚合
  • 新增 backend/src/api/warehouseShipTimeliness.js —— GET /list、GET /rounds
  • 新增 backend/test/warehouseShipTimeliness.test.js —— 10 条口径回归
  • 新增 frontend/src/api/warehouseShipTimeliness.js
  • 新增 frontend/src/pages/MkhBomData/WarehouseShipTimelinessSidebar.jsx
  • 修改 backend/src/index.js —— 挂载路由(紧跟 mkh-bom 之后)
  • 修改 frontend/src/pages/MkhBomData/index.jsx / style.css —— 两栏布局 + 超时明细卡

共 9 个文件、+1648 / -0。

实现要点

① 仓库归属:关键词最长匹配,不写死 wms_co_id
新增子仓只要名字里含主仓关键词就自动归入,映射缓存 1h。实测 28 个子仓全部正确归入 11 个主仓,
132 个代发仓零误并(重点验证了「山东」「河南」这类高频前缀不会把「工厂-山东XX代发仓」吞进品牌仓)。

② 12:00 之前不做核验
resolveActiveRound 返回 null,服务层直接返回 notStarted:不查订单表(接口 165ms),
也不回填昨天的数据。前端显示「未到核验时间」+「12:00 开始首轮核验(核验当日 07:00 前创建的订单)」,
11 个仓结构保留、数字显示「—」、全部不可点。

③ 两表并行 + Node 侧合并
jst_Orders(京东/拼多多/抖音/得物/唯品会)与 jst_qmcust_orderlist(天猫/淘宝/1688)店铺互斥,
并行分开查后按 o_id 去重合并。用 Promise.allSettled:单表失败时另一表数据仍可用,
detailDegraded + note 显式标记「数据不完整」,避免拿残缺数据考核仓库。

④ 三态判定 pending / shippedLate / onTime
send_date 与出库表 io_date 取先到的发货痕迹,避免上游 send_date 回写滞后导致
「已发的被判成没发」。freshness 字段如实报告出库表最新 io_date,前端出「发货数据可能滞后」告警。

⑤ 状态过滤
排除 Cancelled / Merged / Split;Question 仍计入待发(与 mkhBomService 待发口径一致)。
Split 必须排除:拆单父单永不发货,不排除会捞出一年前 1.3 万条历史单。

两次被实测推翻的「优化」(都在注释里留了数据与复测脚本)

❌ 用 created 替代 order_date 做窗口过滤(快 10 倍)
已证伪。2,581 行里 order_date 与 created 完全相等的只有 4 行,差 >1h 的 412 行,
198 行跨日;典型形态 order_date=10-18 23:58 而 created=10-19 04:00:08
(上游凌晨 4 点批量补录,created 是灌库时间不是下单时间)。业务口径是「订单创建时间」,
只有 order_date 正确,已回滚。

❌ 「UNION ALL 一条 SQL 慢 5~8 倍」
被后续实测推翻(UNION ALL 46.3s vs 两表并行 48.2s,基本持平)。保留「两表分开」是因为
allSettled 的降级能力,不是因为性能。

真实性能数据(冷缓存 → 热缓存)
同一批查询:冷缓存 40~110s,热缓存 6.1s。抖动来源是 SQL Server buffer pool 的
read-ahead 补页(STATISTICS IO 实测 63.6 万逻辑读 / 44 万 read-ahead),不是 SQL 写法。
生产环境有常驻流量保持缓存热度,不会长期停在冷态。

同步与兼容性

  • 纯新增功能,不改动任何既有接口/口径/页面行为;未配置到任何菜单之外的入口。
  • 新增路由 /api/warehouse-ship-timeliness/*,走既有 authMiddleware。
  • 环境变量无新增。依赖的库表均为只读视图(rpt.jst_Orders、rpt.jst_qmcust_orderlist、
    rpt.jst_salesOutboundInfo、rpt.jst_warehouseInfo)。

验证

单测(node --test test/warehouseShipTimeliness.test.js)
10 条全绿:最长匹配不误并、子仓命名覆盖、山东/河南前缀不误并、占位仓、
12:00 前返回 null、12:00 轮窗口 00:00~07:00、16:00 轮窗口 00:00~16:00、
状态排除表、三态分桶互斥 + 跨表 o_id 去重、round.windowEnd 必须带出。

全量回归
check-syntax 185 文件通过;node --test test/*.test.js → 415 tests / 413 pass / 2 fail。
两条失败均已确认是环境因素、与本次无关:

  • 负缓存带 TTL:backend/.env 里的 CLOUD_DB_* 让 hasCompleteCloudEnvConfig() 短路,
    「系统库不可达」的测试前提不成立(临时移走 .env 后立即通过,已还原)
  • resilientSecretStore 的 Windows 权限断言(基线恒失败项)

真渲染验收(组件级沙盒 + 真实接口数据,两态 0 控制台错误)

  • 未核验态:未到核验时间 / — 单超时未发 / 11 仓全部禁用 / 半成品仓「未接入」/ 底部提示正确
  • 已核验态(round=AFTERNOON):358 单超时未发、8 个仓有数字、点仓切换选中与再点取消均正常、
    收起态正常;meta 正确显示「16:00 轮 核验 16:00 前」
  • 多分辨率:1920/1600 侧栏 208px、1440/1366 收窄 176px(列表始终竖向,不横排)、
    1024 以下才兜底整行铺开

实测业务数据(2026-10-20 16:00 轮,仅供参考,随上游变化)

主仓 超时未发 窗口订单
崔桥工厂仓 190 208
山东青岗集仓 55 72
新苏北仓 49 61
睢宁仓 19 29
山东品牌仓 19 21
横林二仓 14 15
崔桥工厂品牌仓 6 6
河南洛宁仓 6 8
横林三仓 / 横林二仓品牌仓 0 0
合计 358 420

风险与回退

风险 1:按时率偏低(本次 14.8%)
已排查为真实数据而非口径问题:窗口 420 单中 170 单是 Delivering + shop_status=WAIT_SELLER_SEND_GOODS
(等待卖家发货),抽样 5 单在出库表命中 0 条。出库表近 7 天每天 8,770~12,298 条,没有滞后。
但「12:00 核验 7:00 前下单的单」本身是强考核,建议上线后由业务观察一两周再定是否调窗口。

风险 2:数据新鲜度
上游 send_date 回写集中在上午 8~9 点,出库表每天 04:00 起才有数据。若在 12:00 刚过时查看,
部分已发货的单可能尚未回写 → ���时数偏高。已通过 freshness.stale + 前端告警显式提示,
不做任何「猜测补数」。

风险 3:冷缓存首查慢
冷态 40~110s、热态 6s。前端请求超时设为 3 分钟,侧栏独立 10 分钟轮询(不跟列表 2 分钟轮询)。
若生产环境出现长期冷态,需要评估是否加服务端定时预热。

回退方式
纯新增功能,回退只需 revert 本提交 —— 不涉及数据迁移、不留残留状态;
index.js 的路由挂载与 index.jsx 的侧栏是仅有的两处对既有文件的改动,删除后页面恢复原状。

## 目的与范围 在 MKH-BOM 列表左侧新增「仓储中心24小时发货时效」侧栏(需求提出:yanhui 2026-10-20)。 **业务需求原文** > 在列表左侧加一栏仓储中心24小时发货时效,在这一栏下面加以下子栏,横林二仓,横林三仓, > 横林二仓品牌仓,崔桥工厂仓,崔桥工厂品牌仓,崔桥工厂半成品仓,新苏北仓,睢宁仓, > 山东青岗集仓,山东品牌仓,河南洛宁仓,从系统库存抓取已发货订单以及未发货订单, > 按照订单创建时间来算发货是否及时,每天中午 12:00,核实当天创建时间 7:00 之前是否已完成发货, > 每天下午 16:00,核实当天创建时间 16:00 前订单是否已完成 **已确认的三条设计决策** | 决策点 | 结论 | |---|---| | 子仓归属 | 连子仓一起算(京东云仓 / 自营仓 / 运营C专用仓 / 唯品仓等并入主仓) | | 崔桥工厂半成品仓 | 系统无同名仓 → 先留空占位(**不**映射到配件仓 15718196) | | 展示结构 | 只显示超时未发 | **改动文件** - 新增 `backend/src/services/warehouseShipTimelinessRegistry.js` —— 主仓关键词、核验轮次、状态排除口径 - 新增 `backend/src/services/warehouseShipTimelinessService.js` —— 取数与三态聚合 - 新增 `backend/src/api/warehouseShipTimeliness.js` —— `GET /list`、`GET /rounds` - 新增 `backend/test/warehouseShipTimeliness.test.js` —— 10 条口径回归 - 新增 `frontend/src/api/warehouseShipTimeliness.js` - 新增 `frontend/src/pages/MkhBomData/WarehouseShipTimelinessSidebar.jsx` - 修改 `backend/src/index.js` —— 挂载路由(紧跟 mkh-bom 之后) - 修改 `frontend/src/pages/MkhBomData/index.jsx` / `style.css` —— 两栏布局 + 超时明细卡 共 9 个文件、+1648 / -0。 ## 实现要点 **① 仓库归属:关键词最长匹配,不写死 `wms_co_id`** 新增子仓只要名字里含主仓关键词就自动归入,映射缓存 1h。实测 28 个子仓全部正确归入 11 个主仓, 132 个代发仓零误并(重点验证了「山东」「河南」这类高频前缀不会把「工厂-山东XX代发仓」吞进品牌仓)。 **② 12:00 之前不做核验** `resolveActiveRound` 返回 `null`,服务层直接返回 `notStarted`:**不查订单表**(接口 165ms), **也不回填昨天的数据**。前端显示「未到核验时间」+「12:00 开始首轮核验(核验当日 07:00 前创建的订单)」, 11 个仓结构保留、数字显示「—」、全部不可点。 **③ 两表并行 + Node 侧合并** `jst_Orders`(京东/拼多多/抖音/得物/唯品会)与 `jst_qmcust_orderlist`(天猫/淘宝/1688)店铺互斥, 并行分开查后按 `o_id` 去重合并。用 `Promise.allSettled`:单表失败时另一表数据仍可用, `detailDegraded` + `note` 显式标记「数据不完整」,避免拿残缺数据考核仓库。 **④ 三态判定 pending / shippedLate / onTime** `send_date` 与出库表 `io_date` 取先到的发货痕迹,避免上游 `send_date` 回写滞后导致 「已发的被判成没发」。`freshness` 字段如实报告出库表最新 `io_date`,前端出「发货数据可能滞后」告警。 **⑤ 状态过滤** 排除 `Cancelled` / `Merged` / **`Split`**;`Question` 仍计入待发(与 mkhBomService 待发口径一致)。 `Split` 必须排除:拆单父单永不发货,不排除会捞出一年前 1.3 万条历史单。 ## 两次被实测推翻的「优化」(都在注释里留了数据与复测脚本) **❌ 用 `created` 替代 `order_date` 做窗口过滤(快 10 倍)** 已证伪。2,581 行里 `order_date` 与 `created` 完全相等的只有 4 行,差 >1h 的 412 行, **198 行跨日**;典型形态 `order_date=10-18 23:58` 而 `created=10-19 04:00:08` (上游凌晨 4 点批量补录,`created` 是灌库时间不是下单时间)。业务口径是「订单创建时间」, 只有 `order_date` 正确,已回滚。 **❌ 「UNION ALL 一条 SQL 慢 5~8 倍」** 被后续实测推翻(UNION ALL 46.3s vs 两表并行 48.2s,基本持平)。保留「两表分开」是因为 `allSettled` 的降级能力,不是因为性能。 **真实性能数据**(冷缓存 → 热缓存) 同一批查询:冷缓存 40~110s,热缓存 **6.1s**。抖动来源是 SQL Server buffer pool 的 read-ahead 补页(`STATISTICS IO` 实测 63.6 万逻辑读 / 44 万 read-ahead),不是 SQL 写法。 生产环境有常驻流量保持缓存热度,不会长期停在冷态。 ## 同步与兼容性 - 纯新增功能,不改动任何既有接口/口径/页面行为;未配置到任何菜单之外的入口。 - 新增路由 `/api/warehouse-ship-timeliness/*`,走既有 `authMiddleware`。 - 环境变量无新增。依赖的库表均为只读视图(`rpt.jst_Orders`、`rpt.jst_qmcust_orderlist`、 `rpt.jst_salesOutboundInfo`、`rpt.jst_warehouseInfo`)。 ## 验证 **单测**(`node --test test/warehouseShipTimeliness.test.js`) 10 条全绿:最长匹配不误并、子仓命名覆盖、山东/河南前缀不误并、占位仓、 12:00 前返回 null、12:00 轮窗口 `00:00~07:00`、16:00 轮窗口 `00:00~16:00`、 状态排除表、三态分桶互斥 + 跨表 `o_id` 去重、`round.windowEnd` 必须带出。 **全量回归** `check-syntax` 185 文件通过;`node --test test/*.test.js` → **415 tests / 413 pass / 2 fail**。 两条失败均已确认是环境因素、与本次无关: - `负缓存带 TTL`:`backend/.env` 里的 `CLOUD_DB_*` 让 `hasCompleteCloudEnvConfig()` 短路, 「系统库不可达」的测试前提不成立(临时移走 `.env` 后立即通过,已还原) - `resilientSecretStore` 的 Windows 权限断言(基线恒失败项) **真渲染验收**(组件级沙盒 + 真实接口数据,两态 0 控制台错误) - 未核验态:`未到核验时间` / `— 单超时未发` / 11 仓全部禁用 / 半成品仓「未接入」/ 底部提示正确 - 已核验态(`round=AFTERNOON`):358 单超时未发、8 个仓有数字、点仓切换选中与再点取消均正常、 收起态正常;`meta` 正确显示「16:00 轮 核验 16:00 前」 - 多分辨率:1920/1600 侧栏 208px、1440/1366 收窄 176px(**列表始终竖向**,不横排)、 1024 以下才兜底整行铺开 **实测业务数据**(2026-10-20 16:00 轮,仅供参考,随上游变化) | 主仓 | 超时未发 | 窗口订单 | |---|---|---| | 崔桥工厂仓 | 190 | 208 | | 山东青岗集仓 | 55 | 72 | | 新苏北仓 | 49 | 61 | | 睢宁仓 | 19 | 29 | | 山东品牌仓 | 19 | 21 | | 横林二仓 | 14 | 15 | | 崔桥工厂品牌仓 | 6 | 6 | | 河南洛宁仓 | 6 | 8 | | 横林三仓 / 横林二仓品牌仓 | 0 | 0 | | **合计** | **358** | **420** | ## 风险与回退 **风险 1:按时率偏低(本次 14.8%)** 已排查为真实数据而非口径问题:窗口 420 单中 170 单是 `Delivering` + `shop_status=WAIT_SELLER_SEND_GOODS` (等待卖家发货),抽样 5 单在出库表命中 0 条。出库表近 7 天每天 8,770~12,298 条,**没有滞后**。 但「12:00 核验 7:00 前下单的单」本身是强考核,建议上线后由业务观察一两周再定是否调窗口。 **风险 2:数据新鲜度** 上游 `send_date` 回写集中在上午 8~9 点,出库表每天 04:00 起才有数据。若在 12:00 刚过时查看, 部分已发货的单可能尚未回写 → ���时数偏高。已通过 `freshness.stale` + 前端告警显式提示, 不做任何「猜测补数」。 **风险 3:冷缓存首查慢** 冷态 40~110s、热态 6s。前端请求超时设为 3 分钟,侧栏独立 10 分钟轮询(不跟列表 2 分钟轮询)。 若生产环境出现长期冷态,需要评估是否加服务端定时预热。 **回退方式** 纯新增功能,回退只需 revert 本提交 —— 不涉及数据迁移、不留残留状态; `index.js` 的路由挂载与 `index.jsx` 的侧栏是仅有的两处对既有文件的改动,删除后页面恢复原状。
yanhui added 1 commit 2026-10-20 11:23:58 +08:00
需求(yanhui 2026-10-20):在 MKH-BOM 列表左侧加一栏「仓储中心24小时发货时效」,
下列 11 个主仓(横林二仓、横林三仓、横林二仓品牌仓、崔桥工厂仓、崔桥工厂品牌仓、
崔桥工厂半成品仓、新苏北仓、睢宁仓、山东青岗集仓、山东品牌仓、河南洛宁仓),
从系统库存抓取已发货/未发货订单,按订单创建时间判定发货是否及时:
· 每天 12:00 核验「当天创建时间 7:00 之前」的订单是否已完成发货
· 每天 16:00 核验「当天创建时间 16:00 之前」的订单是否已完成发货

已确认的三条设计决策:
1. 子仓归属 → 连子仓一起算(京东云仓/自营仓/运营C专用仓/唯品仓等并入主仓)
2. 崔桥工厂半成品仓 → 系统无同名仓,先留空占位(不映射到配件仓 15718196)
3. 展示结构 → 只显示超时未发

实现要点
- 仓库归属用「关键词最长匹配」(不写死 wms_co_id),新增子仓只要名字含主仓关键词
  就自动归入,缓存 1h。实测 28 个子仓全部正确归入 11 个主仓,132 个代发仓零误并。
- 12:00 之前**不做核验**:resolveActiveRound 返回 null,服务层直接返回 notStarted,
  既不查订单表(接口 165ms)也不回填昨天的数据。前端显示「未到核验时间」+
  「12:00 开始首轮核验(核验当日 07:00 前创建的订单)」,11 个仓结构保留、数字显示「—」。
- 订单取数:两张表店铺互斥(jst_Orders = 京东/拼多多/抖音/得物/唯品会,
  jst_qmcust_orderlist = 天猫/淘宝/1688),并行分开查 + Node 侧按 o_id 去重合并,
  用 Promise.allSettled 保证单表失败时另一表数据仍可用(detailDegraded 标记)。
- 三态判定 pending / shippedLate / onTime:send_date 与出库表 io_date 取先到的发货痕迹,
  避免上游 send_date 回写滞后导致「已发的被判成没发」。
- 状态过滤:排除 Cancelled / Merged / **Split**(拆单父单永不发货,不排除会捞出一年前
  1.3 万条历史单),Question 仍计入待发。
- freshness 字段如实报告出库表最新 io_date,前端「发货数据可能滞后」告警,
  不拿一份可能滞后的数据去考核仓库。

排除的两次错误优化(均在代码注释里留了实测数据与复测脚本路径)
1. 用 created 替代 order_date 做窗口过滤(快 10 倍)—— 已证伪:2581 行里完全相等的只有 4 行,
   差 >1h 的 412 行,198 行跨日;典型形态 order_date=10-18 23:58 而 created=10-19 04:00:08
   (上游凌晨 4 点批量补录,created 是灌库时间不是下单时间)。业务口径是「订单创建时间」,
   只有 order_date 正确。
2. 「UNION ALL 一条 SQL 慢 5~8 倍」—— 被后续实测推翻(46.3s vs 并行 48.2s,基本持平)。

验证
- 新增 backend/test/warehouseShipTimeliness.test.js,10 条用例全绿,覆盖:
  最长匹配不误并、子仓命名、山东/河南高频前缀不误并、占位仓、12:00 前返回 null、
  12:00 轮窗口 00:00~07:00、16:00 轮窗口 00:00~16:00、状态排除表、三态分桶互斥、
  跨表重复 o_id 去重、round.windowEnd 必须带出(否则前端渲染成「核验 前」)。
- check-syntax 185 文件通过;全量 node --test 415 项 / 413 过 / 2 fail,
  两条失败均为已知环境因素(负缓存带 TTL 因 .env 有 CLOUD_DB_* 短路;
  resilientSecretStore 的 Windows 权限断言,基线恒失败)。
- 真渲染验收(组件级沙盒 + 真实接口数据):两态均 0 控制台错误;
  已核验态 358 单超时未发、8 个仓有数字、点仓切换选中/再点取消均正常;
  多分辨率验证 1920/1600 侧栏 208px、1440/1366 收窄 176px、列表始终竖向。
yanhui merged commit 6833b7bc79 into main 2026-10-20 11:28:12 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lunanting/cangku#68
No description provided.