fix(wle): 转仓代发(转x00005)不算转运异常 + 导出复用页面缓存 #84

Merged
yanhui merged 1 commits from yanhui-wle-transfer-cache-20261023 into main 2026-10-23 16:03:16 +08:00
Collaborator

背景:两个业务反馈

① 崔桥工厂仓的单,快递却是「曹县韵达」,还是信誉订单

单号 351668937728(内部 9894520)。实查宽表只有一行:

warehouse_name        = 简尚-崔桥工厂仓
logistic_company_name = 曹县韵达小件
logistic_status_name  = 未揽收
order_labels          = ["京东供销订单","京喜","发货|赔偿|补件","店铺-京东供销","禁止拆分","线上发货","转x00005"]

关键就是 转x00005 —— 聚水潭「转仓」操作留在标签里的痕迹。记账仓是崔桥,货实际由
目标仓(x00005 = 曹县)发出,承运账号也跟着换成曹县韵达小件。原仓的物流状态必然停在
「未揽收 / 无物流单」,报出来全是假异常。

顺带说明为什么没命中「信誉单」判据:这单在 jst_Orders 里是 status=Sent、
type=普通订单、question_type=null,数仓里没有任何信誉单标记;而现有信誉单判据只认
标签 转空包(真正的信誉单长那样:近 14 天 转空包发货 506 单全在「简尚-信誉订单仓」)。

② 导出很卡

getLogisticsExceptionExportRows() 用 limit=5000,页面 /list 用 limit=1000,
而缓存键是 ${核验日}|${limit} → 导出永远命中不到页面那份缓存,每次点导出都整轮重算。
但 limit 只影响最后 slice(0, limit)(异常实际才几十行),扫库成本完全一样。

改动

1. 转仓代发不计异常

  • registry 新增 TRANSFER_WAREHOUSE_LABEL_PATTERN = /转\s*[xX]\s*\d{3,}/ +
    matchTransferWarehouse()。不写死 转x00005,后续出现 x00006 自动覆盖。
  • service 主循环在 classify() 之后、现场拦截之前判定。
  • 口径:保留发货分母,只从异常清单摘掉(转仓是真发货,只是不从记账仓发;
    与「现场拦截」同类,这样异常率不会被错误放大)。
  • 新增 exclusions.transferPackages / transferOrders / transferReasons;
    前端页脚、钉钉正文各加一行说明。

2. 缓存键只按核验日

  • 缓存存全量明细(键 = batch.verifyDate),limit 在出口 finalizeDetail() 截断。
  • truncated 按实际全量条数算,不再受调用方 limit 影响。

数据支撑(近 30 天,12 仓)

标签 单量 报未揽收类 主要快递
转x00005 438 46(10.5%) 曹县韵达小件 133、邮政快递包裹POSTB 124、睢宁中通 41
转空包发货 506 — 白云韵达-天猫亿家达、中通礼品单(全在信誉订单仓)

转x00005 状态分布:已签收 371 / 无物流单 31 / 运输中 20 / 未揽收 15 —— 说明绝大多数正常,
只有「记账仓看别仓货」这一小撮会误报。

验证

  • 后端单测 50/50:新增转仓标签识别 8 条断言(含大小写、空格、转空包 反例)、
    转仓单不进异常但进分母、导出复用页面缓存不重查、小 limit 不污染缓存。
  • 全量 npm run check 474 项 / 472 过,2 项为既有环境失败(Windows 权限位、负缓存需隧道故障态)。
  • 前端 eslint 0;npm run build 通过(含 router lazy 静态检查)。
  • 真实库(10-23 批):异常 73 单,转仓剔除 1 单(正是 9894520),目标单已消失;
    导出在页面加载后 3ms 返回 73 行(修复前同样操作要 317s)。

风险

  • 转仓单的「发货量」仍记在原仓(分母���变),如果业务认为不该算原仓发货量,是另一处口径,需另行确认。
  • 正则只认「转x+3 位以上数字」;若聚水潭改标签格式需要同步调整。
## 背景:两个业务反馈 ### ① 崔桥工厂仓的单,快递却是「曹县韵达」,还是信誉订单 单号 `351668937728`(内部 9894520)。实查宽表只有一行: ``` warehouse_name = 简尚-崔桥工厂仓 logistic_company_name = 曹县韵达小件 logistic_status_name = 未揽收 order_labels = ["京东供销订单","京喜","发货|赔偿|补件","店铺-京东供销","禁止拆分","线上发货","转x00005"] ``` 关键就是 **`转x00005`** —— 聚水潭「转仓」操作留在标签里的痕迹。记账仓是崔桥,货实际由 目标仓(x00005 = 曹县)发出,承运账号也跟着换成曹县韵达小件。原仓的物流状态必然停在 「未揽收 / 无物流单」,报出来全是假异常。 顺带说明为什么没命中「信誉单」判据:这单在 `jst_Orders` 里是 `status=Sent`、 `type=普通订单`、`question_type=null`,**数仓里没有任何信誉单标记**;而现有信誉单判据只认 标签 `转空包`(真正的信誉单长那样:近 14 天 `转空包发货` 506 单全在「简尚-信誉订单仓」)。 ### ② 导出很卡 `getLogisticsExceptionExportRows()` 用 `limit=5000`,页面 `/list` 用 `limit=1000`, 而缓存键是 `${核验日}|${limit}` → **导出永远命中不到页面那份缓存,每次点导出都整轮重算**。 但 `limit` 只影响最后 `slice(0, limit)`(异常实际才几十行),扫库成本完全一样。 ## 改动 ### 1. 转仓代发不计异常 - `registry` 新增 `TRANSFER_WAREHOUSE_LABEL_PATTERN = /转\s*[xX]\s*\d{3,}/` + `matchTransferWarehouse()`。**不写死 `转x00005`**,后续出现 x00006 自动覆盖。 - `service` 主循环在 `classify()` 之后、现场拦截之前判定。 - 口径:**保留发货分母,只从异常清单摘掉**(转仓是**真发货**,只是不从记账仓发; 与「现场拦截」同类,这样异常率不会被错误放大)。 - 新增 `exclusions.transferPackages / transferOrders / transferReasons`; 前端页脚、钉钉正文各加一行说明。 ### 2. 缓存键只按核验日 - 缓存存**全量明细**(键 = `batch.verifyDate`),limit 在出口 `finalizeDetail()` 截断。 - `truncated` 按实际全量条数算,不再受调用方 limit 影响。 ## 数据支撑(近 30 天,12 仓) | 标签 | 单量 | 报未揽收类 | 主要快递 | |---|---|---|---| | `转x00005` | 438 | 46(10.5%) | 曹县韵达小件 133、邮政快递包裹POSTB 124、睢宁中通 41 | | `转空包发货` | 506 | — | 白云韵达-天猫亿家达、中通礼品单(全在信誉订单仓) | `转x00005` 状态分布:已签收 371 / 无物流单 31 / 运输中 20 / 未揽收 15 —— 说明绝大多数正常, 只有「记账仓看别仓货」这一小撮会误报。 ## 验证 - **后端单测 50/50**:新增转仓标签识别 8 条断言(含大小写、空格、`转空包` 反例)、 转仓单不进异常但进分母、导出复用页面缓存不重查、小 limit 不污染缓存。 - 全量 `npm run check` **474 项 / 472 过**,2 项为既有环境失败(Windows 权限位、负缓存需隧道故障态)。 - 前端 eslint 0;`npm run build` 通过(含 router lazy 静态检查)。 - **真实库(10-23 批)**:异常 73 单,转仓剔除 1 单(正是 9894520),目标单已消失; 导出在页面加载后 **3ms** 返回 73 行(修复前同样操作要 317s)。 ## 风险 - 转仓单的「发货量」仍记在原仓(分母���变),如果业务认为不该算原仓发货量,是另一处口径,需另行确认。 - 正则只认「转x+3 位以上数字」;若聚水潭改标签格式需要同步调整。
yanhui added 1 commit 2026-10-23 10:47:08 +08:00
两件事,都是 2026-10-23 业务反馈直接引起的。

## 1. 转仓代发不再核对异常

业务反馈:崔桥工厂仓的单 351668937728 被判「未揽收」异常,快递却是「曹县韵达小件」,
而且这单在后台是信誉订单。

实查宽表:这单 `warehouse_name=简尚-崔桥工厂仓`,但 `order_labels` 里带一条
`转x00005` —— 聚水潭「转仓」操作留下的痕迹。记账仓还是崔桥,货却由目标仓
(x00005 = 曹县)实际发出,承运账号也跟着变成曹县韵达小件。结果就是原仓的
物流状态永远停在「未揽收 / 无物流单」,报出来全是假异常。

判据:`order_labels` 命中 `/转\s*[xX]\s*\d{3,}/`(不写死单个编码,后续可能出现
x00006)。实测近 30 天 12 仓:`转x00005` 438 单,报未揽收类 46 单(已签收 371 /
无物流单 31 / 运输中 20 / 未揽收 15);快递分布 曹县韵达小件 133、邮政快递包裹POSTB 124。

口径要点:
- 与「转空包」(信誉单,发空包)严格区分 —— 转仓是**真发货**,只是不从记账仓发;
- 所以**保留发货分母**,只从异常清单摘掉(与「现场拦截」同类,不稀释异常率);
- 新增 `exclusions.transferPackages/transferOrders/transferReasons`,
  前端页脚与钉钉正文各加一行说明。

## 2. 缓存键只按核验日(修导出卡顿)

原来键是 `${核验日}|${limit}`:页面传 1000、导出传 5000 → 两份缓存互不命中,
点一次导出就要把整轮重算一遍。可 `limit` 只影响最后 `slice(0, limit)`,
异常实际才几十行,扫库成本一模一样。

改为:缓存只存**全量明细**(键 = 核验日),limit 在出口 `finalizeDetail()` 里截断。
生产实测(修复前,容器内直调):页面冷启 189s、命中缓存 0ms、
**页面已缓存时点导出仍要 317s**;修复后导出复用页面结果 1~3ms。

## 验证

- 后端单测 50/50(新增:转仓标签识别 8 条断言、转仓单不进异常但进分母、
  导出复用页面缓存不重查、小 limit 不污染缓存)
- 全量 `npm run check` 474 项 / 472 过,2 项为既有环境失败(Windows 权限位、负缓存需隧道故障态)
- 前端 eslint 0、`npm run build` 通过(含 router lazy 静态检查)
- 真实库(10-23 批):异常 73 单,转仓剔除 1 单(正是 9894520),目标单已消失;
  导出在页面加载后 3ms 返回 73 行
yanhui merged commit d1bf4047e2 into main 2026-10-23 16:03:16 +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#84
No description provided.