Files
InboundVerify/docs/站点统计逻辑审查报告.md
Misaka_Company 5e4889e845 docs: sync docs with current package layout and auto-ingest hook
- config.example.yaml: replace stale site_yunda/main_router with new
  module paths (sites.yunda, runtime)
- CLAUDE.md: add ingest-one to DB CLI list; new subsection documenting
  the download->PostgreSQL auto-ingest hook (_persist_to_db, ingest_task,
  ingest_state, /status.ingest, auto_ingest config)
- README.md: store tree comment lists all 5 CLI commands (add ingest-one)
- docs: /status row notes the ingest field; anneng CDP guide snippet
  gets timeout=15
- cli/server.py: docstring run command -> python -m inbound_verify.cli.server

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 12:35:54 +08:00

121 lines
9.3 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.
# 后端各站点「应到件数 / 已到件数」统计逻辑审查报告
> 审查范围:`InboundVerify` 后端
> 核心模块:`compare.py`(比对统计)、`domain.py`(站点/文件/列配置)、`runtime.py`(调度/下载路由)、`sites/*.py`(各站数据采集)、`cli/server.py`(对外接口)
> 审查重点:每个站点的「应到件数」和「已到件数」如何统计、数据来源、口径与潜在歧义。
---
## 一、总体架构(两层)
统计逻辑分两层,前端看到的「应到/已到/未到」计数最终都来自**第二层**,且只在跑比对时才生成。
| 层 | 模块 | 职责 | 产物 |
|---|---|---|---|
| **① 数据采集层** | `sites/{顺心,中通,韵达,安能,百世}.py` | 用 Playwright安能用 CDP登录各承运商后台导出 Excel 到 `downloads/` | `downloads/<站>-应到货物数据.xlsx``downloads/<站>-实到货物数据.xlsx``downloads/<站>-未到数据.xlsx` |
| **② 比对统计层** | `compare.py`(站点/文件/列配置取自 `domain.py` | 读 `downloads/` 源文件,两表比对,算出应到/实到/未到件数并出报表 | `output/应到未到数据.xlsx`(汇总+各站明细) |
调度入口:`runtime.py``TASK_HANDLERS`
- 4 站:`expected` 下载 + `actual` 下载 + `undelivered`(先下应到+实到,再调 `write_site_file` 算出未到)。
- 百世:只有 `undelivered`(直接导「当日未扫」明细,无应到/实到两份基表)。
- `__compare__`(菜单[9]):调 `compare.main()`,用 `downloads/` 现有文件生成全站汇总报表。
---
## 二、核心统计公式4 站统一口径)
`compare.process(name)` 是 4 站统计的唯一实现:
```
应到(按运单号去重,保留首条):
应到件数 N = 应到数据中的「交接件数」 # 注意:用「交接件数」,不是「录单件数」
(录单件数只是该单号总录单量,并非真正到站量)
应到件数 += N # 站点级应到 = Σ N
实到(直接数,不再由「应到−未到」倒推):
按「运单号」把实到表的「单号/子单号/扫描单号」分组,组内去重计数
→ 每个运单的实到件数;站点级实到 = Σ 各运单实到件数
未到:
未到件数 = max(0, 应到件数 实到件数) # 站点级
未到率 = 未到件数 ÷ 应到件数
短少运单 = 实到件数 < 应到件数 N 的运单
(未到明细 downloads/<站>-未到数据.xlsx 只列这些短少运单,
每行:交接单号 | 运单号 | 总件数(=N) | 已到单号1 | 已到单号2 | …)
```
**关键事实**`实到件数` 是直接数实到表「单号」、按运单号分组去重得到的(**不再由「应到−未到」倒推**`未到件数 = 应到件数 实到件数`。前提是实到单号能正确按运单号分组(分组规则见下表各站 `arrived_pieces_*`)。
---
## 三、各站点统计明细
| 站点 | 应到件数来源 | 实到件数来源 | 单号→运单号 分组规则(`arrived_pieces_*` | 源文件(`downloads/` |
|---|---|---|---|---|
| **中通** | `中通-应到货物数据.xlsx` 的「交接件数」 | 实到表单号去重(直接数) | 实到`运单号`是复合串 = 运单号 + 总数(4) + 顺序(4);按 `v[:-8]` 归并到应到运单号,每条复合串即 1 件 | `中通-应到货物数据.xlsx` / `中通-实到货物数据.xlsx` |
| **顺心** | `顺心-应到货物数据.xlsx` 的「交接件数」 | 实到表单号去重(直接数) | 按`运单号`分组,`子单号`=每件(一件一个子单号) | `顺心-应到货物数据.xlsx` / `顺心-实到货物数据.xlsx` |
| **韵达** | `韵达-应到货物数据.xlsx` 的「交接件数」 | 实到表单号去重(直接数) | 按`主单号`分组,`子单号`=每件 | `韵达-应到货物数据.xlsx` / `韵达-实到货物数据.xlsx` |
| **安能** | `安能-应到货物数据.xlsx` 的「交接件数」 | 实到表单号去重(直接数) | 按`所属单号`分组,`扫描单号`=每件 | `安能-应到货物数据.xlsx` / `安能-实到货物数据.xlsx` |
| **百世** | **无**(站点只给未到) | **无(显示「—」)** | 不适用(无实到基表) | 仅 `百世-应到未到货物数据.xlsx`= 当日未扫明细,本身就是未到结果) |
> 4 站合计/图表口径:`compare.build_summary` 只累加 4 站(`应到−实到`口径),**百世不计入合计**(无应到基数)。百世在表中单列,未到件数 = 其明细行数。
---
## 四、百世的特殊口径(务必注意)
百世是唯一「无应到/实到基数」的站点:
- 它的数据来自后台「扫描综合查询 → 到/接件扫描 → 当日 → 未扫」,导出即「当日未扫」明细(`sites/baishi.py`)。
- 因此 `process_baishi()` 只能给 `未到件数 = 行数``应到件数 = None``已到件数 = None`、完全/部分未到 = None。
- 报表里百世的应到/已到列显示「—」,未到率无法计算。
- **含义**:百世统计的是「今天还没扫到的件」,不是「相对应到总量的缺件率」。与 4 站口径不可直接相加比较。
---
## 五、数据日期与偏移(潜在口径不一致风险)
`runtime._record_business_date` 在下载成功后把业务日期写进状态库:
- `业务日期 = 下载当天 日期偏移`
- 各站 `expected_offset` / `actual_offset` 独立配置(前端 `/config` 可改;百世偏移恒 0锁定当天
- 韵达默认 `expected_offset=1`(取前一日应到)。
**风险点**4 站的「应到」和「实到」是**两次独立下载**,各自可能带不同偏移。若 `expected_offset ≠ actual_offset`,则「应到件数」和「已到件数」来自**不同业务日期**,比对会变成「拿昨天的应到对比今天的实到」,未到率失真。报表「数据日期」列分别标注各站,但汇总合计不标注,肉眼难发现。
---
## 六、统计结果如何暴露给前端
| 接口 | 返回内容 | 是否含应到/已到计数 |
|---|---|---|
| `GET /status` | 各站 `login_state` + `expected/actual/undelivered_ready` + `business_date` + `worker_ready` + `ingest`(每站每类入库 ok/count/时间) | **不含**应到/已到件数计数(`ingest` 是入库条数,非核销件数) |
| `GET /report` | `FileResponse(output/应到未到数据.xlsx)` | 计数只在 xlsx 里 |
| `GET /data/{filename}` | 下载 `downloads/` 下某源文件 | 原始数据,非统计值 |
**结论**:后端**没有**把应到/已到件数以 JSON 形式实时返回前端。计数仅物化在 `output/应到未到数据.xlsx`。任何前端界面显示的应到/已到数字,都是解析这份 xlsx 得到的——即**「截至上次跑比对」的快照**,不是实时值。
---
## 七、潜在歧义与风险点(审查结论)
1. **实到依赖单号→运单号分组正确**:实到件数靠把实到表「单号」按运单号分组去重计数(`arrived_pieces_*`)。一旦某站单号格式与分组规则不匹配(如复合串切分错),该件归不到对应运单 → 实到被低估、未到率虚高。规则硬编码,承运商改版号段即失准。
2. **应到件数依赖「交接件数」**:若应到数据某运单 `交接件数` 缺失/为 0/非数字,该运单被跳过,既不计入应到也不计入未到 → 静默漏统(应到总量被低估;该运单即便出现在实到中也因不在应到循环而无处抵扣)。
3. **应到按运单号去重keep first**:同一运单多条交接记录只取首条 `录单件数`。若重复行的件数不同,取首条,可能与实际不符。
4. **百世不可并入合计**4 站合计的「已到总件数」不含百世;跨站看「已到」时别把百世当成有应到基数的站。
5. **应到/实到业务日期可能错位**(见第五节):两表不同步下载或偏移不一致时,比对口径失真。
6. **计数是比对产物,非实时**:前端若要「实时件数」需先触发比对任务;`/status``ready` 仅表示「下载成功」,不代表「已比对出数」。
7. **子单号是程序现拼的**4 站缺件的「子单号/扫描单号」由 `code_*` 按各站编号规则生成(`code_shunxin` 等),并非实到原始记录——明细里的缺件单号是推算值,用于人工核对,不是系统回执。
---
## 八、关键文件索引
| 文件 | 角色 |
|---|---|
| `compare.py` | 统计核心:`process()`4站比对`process_baishi()`(百世)、`build_summary()`(汇总报表) |
| `domain.py` | 站点/文件名/列映射配置:`STATIONS`(各站解析配置)、`arrived_pieces_*`(实到单号→运单号分组)、`_site_cfg` |
| `runtime.py` | `TASK_HANDLERS`(下载/比对路由)、`_site_undelivered_handler`4站先下应到+实到再算未到)、`_record_business_date`(业务日期/偏移写入) |
| `sites/{中通,顺心,韵达,安能}.py` | 各站 `expected_download` / `actual_download`Playwright/CDP 导出源表) |
| `sites/baishi.py` | `baishi_download_undelivered_data`(直接导「当日未扫」) |
| `state_store.py` | `site_status`ready/business_date`site_config`offset/schedule`get_offset` |
| `cli/server.py` | `/status``/report``/data/{filename}` 接口 |