# 应到驱动差缺统计重构 · 设计方案 > 日期: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 | **关键结论**: 1. **出库大头日 ≈ 实到峰值日**(154/158 一致,97.5%)——"出库日"基本等于"这批货实际到的那天"。 2. "提前提交"的批次(中通 `071402` 出库 7/15、`071902` 出库 7/20)在**出库时间上如实体现**了真实归属日。 3. 四站应到文件的「出库时间」字段非空率 100%,且已完整保留在 PG `expected_record.raw` JSONB 中(四站 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 重构后) ```mermaid flowchart TD S[查询 D 日差缺] --> S1 S1["Step1 取应到批次
expected_record
WHERE site=? AND 出库日 = D"] S1 -->|无应到| X[返回:明确当日无应到] S1 -->|有批次| S2 S2["Step2 展开批次全量应到
waybill_no, handover_no, handover_pieces"] S2 --> S3 S3["Step3 查这些运单的全量实到
actual_record WHERE waybill_no = ANY(应到)"] S3 --> S4 S4["Step4 逐运单比对(_do_compare)
应到=交接件数 实到=子单号去重/SF行计数"] S4 --> R[CompareResult
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`(出库日,批次归属日的持久化依据): ```sql 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` | 报表说明文案(批次归属=出库日);无结构变更预期 | --- ## 七、验证计划 1. **单元验证**:`out_date` 回填后,抽样核对与归档 Excel 出库日一致。 2. **回溯对照**:用 7 月归档应到 + PG 实到,分别跑"旧实到驱动"与"新应到驱动",对比差缺差异,重点: - 韵达 7 月各日(应到归属日整体 +1 是否对齐实到) - 中通 7/14、7/19(`071402`/`071902` 是否归入次日) - 顺心/安能(应无差异) 3. **报表烟测**:跑一次 `__compare__/compare` 全站汇总,人工核对韵达 08-02 数据。 --- ## 八、决策记录(已确认) - ✅ 出库时间字段业务含义 = **货物实际发出时间**(按此处理)。 - ✅ 批次内出库日并列众数取法 = **取较早日期**。 - ✅ "无应到"呈现 = 报表中**直接写**(如实呈现,无需特殊文案)。 - ✅ **保留实到驱动入口**,长期作为对照(不删除)。