Files
InboundVerify/docs/2026-08-03-应到驱动差缺统计重构-design.md
Misaka 36b204abe5 refactor(db_compare): 差缺统计改为应到驱动,以出库日为批次归属日
将未到统计从实到驱动(实到锚点反推批次)改为应到驱动(batch_out_date 归属日直接取应到批次),以吸收应到任务提前 1~2 天提交的扰动(韵达固定 +1、中通偶发 +1)。

- expected_record 新增 out_date / batch_out_date + 索引;入库解析出库时间并聚合批次归属日,支持历史回填

- 新增 compare_site_outdate 应到驱动入口,保留 compare_site_date 实到驱动作对照

- 未到任务 / POST /compare / 全站汇总切换到应到驱动

- 附现状梳理、设计、实现总结三篇文档
2026-08-03 22:52:28 +08:00

180 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 应到驱动差缺统计重构 · 设计方案
> 日期2026-08-03
> 分支:`refactor/undelivered-by-expected`
> 状态:待审核
> 目标:差缺统计从"实到驱动(反推应到)"改为"应到驱动(以应到为统计起点)",并用「出库日」作为批次归属日,吸收"提前提交"扰动
---
## 一、背景与问题
### 1.1 业务诉求
- 业务部门日常看的是**应到货物数据**:要"应到了哪些、实到了哪些、差缺是什么"。
- 当前实现是**实到驱动**:以目标日实到扫描为锚点 → 反推交接批次 → 展开批次全量应到 → 逐单比对。
- 方向与业务诉求相反,需重构为**应到驱动**。
### 1.2 核心扰动:应到任务可能被提前提交
- 应到数据理论当天提交(韵达固定提前 1 天),但实际可能提前 1~2 天。
- 若完全按"下载日"统计应到,会把"提前提交、货次日才到"的批次计入当天,产生假差缺。
### 1.3 数据实证7 月全量)
`downloads/archive/` 7 月应到文件 × PG 实到数据交叉验证:
| 站点 | 下载日=出库大头日 | 出库大头日=下载日+1 |
|------|-----------------|-------------------|
| 顺心 | 83/83100% | 0 |
| 中通 | 28/3093% | 2`071402``071902` |
| 韵达 | 0 | 29/29100%,固定提前) |
| 安能 | 31/31100% | 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 取应到批次<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`(出库日,批次归属日的持久化依据):
```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 数据。
---
## 八、决策记录(已确认)
- ✅ 出库时间字段业务含义 = **货物实际发出时间**(按此处理)。
- ✅ 批次内出库日并列众数取法 = **取较早日期**
- ✅ "无应到"呈现 = 报表中**直接写**(如实呈现,无需特殊文案)。
-**保留实到驱动入口**,长期作为对照(不删除)。