将未到统计从实到驱动(实到锚点反推批次)改为应到驱动(batch_out_date 归属日直接取应到批次),以吸收应到任务提前 1~2 天提交的扰动(韵达固定 +1、中通偶发 +1)。 - expected_record 新增 out_date / batch_out_date + 索引;入库解析出库时间并聚合批次归属日,支持历史回填 - 新增 compare_site_outdate 应到驱动入口,保留 compare_site_date 实到驱动作对照 - 未到任务 / POST /compare / 全站汇总切换到应到驱动 - 附现状梳理、设计、实现总结三篇文档
7.6 KiB
7.6 KiB
应到驱动差缺统计重构 · 设计方案
日期:2026-08-03 分支:
refactor/undelivered-by-expected状态:待审核 目标:差缺统计从"实到驱动(反推应到)"改为"应到驱动(以应到为统计起点)",并用「出库日」作为批次归属日,吸收"提前提交"扰动
一、背景与问题
1.1 业务诉求
- 业务部门日常看的是应到货物数据:要"应到了哪些、实到了哪些、差缺是什么"。
- 当前实现是实到驱动:以目标日实到扫描为锚点 → 反推交接批次 → 展开批次全量应到 → 逐单比对。
- 方向与业务诉求相反,需重构为应到驱动。
1.2 核心扰动:应到任务可能被提前提交
- 应到数据理论当天提交(韵达固定提前 1 天),但实际可能提前 1~2 天。
- 若完全按"下载日"统计应到,会把"提前提交、货次日才到"的批次计入当天,产生假差缺。
1.3 数据实证(7 月全量)
对 downloads/archive/ 7 月应到文件 × PG 实到数据交叉验证:
| 站点 | 下载日=出库大头日 | 出库大头日=下载日+1 |
|---|---|---|
| 顺心 | 83/83(100%) | 0 |
| 中通 | 28/30(93%) | 2(071402、071902) |
| 韵达 | 0 | 29/29(100%,固定提前) |
| 安能 | 31/31(100%) | 0 |
关键结论:
- 出库大头日 ≈ 实到峰值日(154/158 一致,97.5%)——"出库日"基本等于"这批货实际到的那天"。
- "提前提交"的批次(中通
071402出库 7/15、071902出库 7/20)在出库时间上如实体现了真实归属日。 - 四站应到文件的「出库时间」字段非空率 100%,且已完整保留在 PG
expected_record.rawJSONB 中(四站 100% 有值)。
二、核心口径
2.1 批次归属日 = 出库大头日
一个交接批次内,取运单「出库时间」的**日期众数(大头日)**作为该批次归属日:
批次归属日 = mode(运单.出库时间::date)
- 顺心/安能:归属日 = 下载日(无扰动)
- 中通:偶发提前批次自动归属次日(
071402→ 7/15) - 韵达:所有批次归属日 = 下载日 + 1(与实到对齐,不再依赖
expected_offset=1)
2.2 统计 D 日差缺 = 取所有「出库日 = D」的应到批次
无论批次在 D / D-1 / D-2 哪天下载(business_date 为何),只要出库日 = D 即纳入 D 日统计:
目标批次 = expected_record WHERE site=? AND 出库日 = D
这样:
- 提前提交的批次(下载于 D-1/D-2、出库于 D)会被自然归入 D 日,不再遗漏也不提前计入;
- 不再需要"实到为 0 → 抛弃/标记留存"的状态机;
- 不再需要为韵达单独配置
expected_offset。
三、统计流程(compare_site_date 重构后)
flowchart TD
S[查询 D 日差缺] --> S1
S1["Step1 取应到批次<br/>expected_record<br/>WHERE site=? AND 出库日 = D"]
S1 -->|无应到| X[返回:明确当日无应到]
S1 -->|有批次| S2
S2["Step2 展开批次全量应到<br/>waybill_no, handover_no, handover_pieces"]
S2 --> S3
S3["Step3 查这些运单的全量实到<br/>actual_record WHERE waybill_no = ANY(应到)"]
S3 --> S4
S4["Step4 逐运单比对(_do_compare)<br/>应到=交接件数 实到=子单号去重/SF行计数"]
S4 --> R[CompareResult<br/>stats + 差缺明细]
3.1 与现实现的差异
| 环节 | 现状 | 重构后 |
|---|---|---|
| 应到来源 | 实到锚点反推批次 | 按出库日直接取应到批次 |
| 无实到表现 | 返回 None(不产出) | 应到空 → 明确"无应到";应到有实到空 → 记差缺 |
| 历史批次混入 | 1 单命中即整批展开 | 按出库日隔离,天然干净 |
| 提前提交 | 无感知(靠实到锚定) | 出库日归属,自动吸收 |
3.2 保留的能力
compare_site_batch(按交接单号精确比对)保留,供复核。- 实到驱动入口
compare_site_date旧逻辑保留为对照模式(或通过配置切换),便于回溯验证差异。 - 统计口径不变:应到=交接件数、实到=子单号去重(顺心 SF 行计数)、未到=
max(0,应到−实到)、明细含已到单号。
四、数据层改造
4.1 新增列:out_date
expected_record 新增 out_date DATE(出库日,批次归属日的持久化依据):
ALTER TABLE expected_record ADD COLUMN IF NOT EXISTS out_date DATE;
CREATE INDEX IF NOT EXISTS idx_expected_out_date ON expected_record (site, out_date);
- 入库时(
store._ingest_expected):从 raw 的「出库时间」解析出日期写入out_date。 - 历史数据回填:一次性 UPDATE,从
raw->>'出库时间'提取日期。 - 出库时间缺失/解析失败 →
out_date置 NULL,统计时回退business_date(下载日),保证不丢数据。
4.2 business_date 语义保持不变
business_date继续表示"下载目标日快照"(兼容就绪态派生 / 报表数据日期列 / 现有 API)。- 差缺统计改用
out_date,两者解耦,避免连锁改动。
4.3 出库时间字段来源(已验证)
| 站点 | 字段 | 覆盖率 |
|---|---|---|
| 顺心 | 出库时间 |
raw 100% |
| 中通 | 出库时间 |
raw 100% |
| 韵达 | 出库时间 |
raw 100% |
| 安能 | 出库时间 |
raw 100% |
五、边界与特殊处理
| 场景 | 处理 |
|---|---|
| 批次内出库日跨多天 | 取大头日(众数);众数并列时取较早日期 |
| 出库时间缺失/解析失败 | out_date 置 NULL,回退 business_date |
| 应到有、实到空 | 全部计入差缺(不再因"无实到"而返回 None) |
| 实到有、应到无(孤儿) | 保持现状,报表/明细可另行提示,不混入应到统计 |
韵达 expected_offset |
保留配置但重构后不再参与归属日计算(由 out_date 取代) |
| 异常小批次 | 规模很小(1~4 单)按常规逻辑走;如出现系统性偏差再单独讨论 |
六、涉及改动清单
| 文件 | 改动 |
|---|---|
schema.sql |
expected_record 增 out_date 列 + 索引 |
store.py |
_ingest_expected 写 out_date;新增历史回填逻辑(CLI) |
db_compare.py |
compare_site_date 改为按 out_date 取应到;新增"无应到"返回语义;保留批次入口与实到驱动对照 |
runtime.py |
_site_undelivered_handler 锚点日期逻辑随新口径调整 |
cli/server.py |
/compare 响应补充 out_date 语义说明;行为兼容 |
state_store.py |
视需要暴露 out_date 相关查询 |
前端 dashboard |
报表说明文案(批次归属=出库日);无结构变更预期 |
七、验证计划
- 单元验证:
out_date回填后,抽样核对与归档 Excel 出库日一致。 - 回溯对照:用 7 月归档应到 + PG 实到,分别跑"旧实到驱动"与"新应到驱动",对比差缺差异,重点:
- 韵达 7 月各日(应到归属日整体 +1 是否对齐实到)
- 中通 7/14、7/19(
071402/071902是否归入次日) - 顺心/安能(应无差异)
- 报表烟测:跑一次
__compare__/compare全站汇总,人工核对韵达 08-02 数据。
八、决策记录(已确认)
- ✅ 出库时间字段业务含义 = 货物实际发出时间(按此处理)。
- ✅ 批次内出库日并列众数取法 = 取较早日期。
- ✅ "无应到"呈现 = 报表中直接写(如实呈现,无需特殊文案)。
- ✅ 保留实到驱动入口,长期作为对照(不删除)。