@@ -1,7 +1,7 @@
# 后端各站点「应到件数 / 已到件数」统计逻辑审查报告
> 审查范围:`InboundVerify` 后端
> 核心模块:`expected_undelive red .py`(比对统计)、`runtime.py`(调度/下载路由)、`site_ *.py`(各站数据采集)、`server.py`(对外接口)
> 核心模块:`compa re.py`(比对统计)、`domain.py`(站点/文件/列配置)、` runtime.py`(调度/下载路由)、`sites/ *.py`(各站数据采集)、`cli/ server.py`(对外接口)
> 审查重点:每个站点的「应到件数」和「已到件数」如何统计、数据来源、口径与潜在歧义。
---
@@ -12,60 +12,60 @@
| 层 | 模块 | 职责 | 产物 |
|---|---|---|---|
| * * ① 数据采集层** | `site_ 顺心/ 中通/ 韵达/ 安能.py` 、`site_ 百世.py` | 用 Playwright( 安能用 CDP) 登录各承运商后台, 导出 Excel 到 `downloads/` | `downloads/<站>-应到货物数据.xlsx` 、`downloads/<站>-实到货物数据.xlsx` 、`downloads/<站>-未到数据.xlsx` |
| * * ② 比对统计层** | `expected_undelivered .py` | 读 `downloads/` 源文件,两表比对,算出应到/已 到/未到件数并出报表 | `output/应到未到数据.xlsx` (汇总+各站明细) |
| * * ① 数据采集层** | `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]):调 `expected_undelive red .main()` ,用 `downloads/` 现有文件生成全站汇总报表。
- `__compare__` (菜单[9]):调 `compa re.main()` ,用 `downloads/` 现有文件生成全站汇总报表。
---
## 二、核心统计公式( 4 站统一口径)
`expected_undelive red .process(name)` 是 4 站统计的唯一实现:
`compa re.process(name)` 是 4 站统计的唯一实现:
```
对每一条应到运单 (按运单号去重,保留首条):
应到件数 N = 应到数据中的「录单件数」(输出列名「总 件数」)
应到序号集 = {1, 2, …, N}
已到序号集 = 从「实到货物数据.xlsx」解析该运单实际到货的子单序号
未到序号集 = {1…N} − 已到序号集
未到件数 += len(未到序号集) # 每缺 1 个子单 → 1 行未到明细
应到件数 += N
应到 (按运单号去重,保留首条):
应到件数 N = 应到数据中的「交接件数」 # 注意:用「交接件数」,不是「 录单件数」
(录单件数只是该单号总录单量,并非真正到站量)
应到件数 + = N # 站点级应到 = Σ N
站点级 :
应到件数 = Σ N( 所有应到运单)
未到件数 = Σ len(未到序号集)
已到件数 = 应到件数 − 未到件数 ← 注意:是「减出来」的,不是直接数实到
未到率 = 未到件数 ÷ 应到件数
完全未到运单 = 未到序号集 == {1…N} 的运单数
部分未到运单 = 0 < 未到 < N 的运单 数
实到(直接数,不再由「应到−未到」倒推) :
按「运单号」把实到表的「单号/子单号/扫描单号」分组,组内去重计数
→ 每个运单的实到件数;站点级实到 = Σ 各运单实到件数
未到:
未到件数 = max(0, 应到件数 − 实到件数) # 站点级
未到率 = 未到件数 ÷ 应到件 数
短少运单 = 实到件数 < 应到件数 N 的运单
(未到明细 downloads/<站>-未到数据.xlsx 只列这些短少运单,
每行:交接单号 | 运单号 | 总件数(=N) | 已到单号1 | 已到单号2 | …)
```
**关键事实 ** : `已 到件数` 是 ` 应到件数 − 未 到件数` 推算出来的(`exp_pieces - len(rows)` ),并非独立去数实到表。它等价于 `Σ|已到序号集|` ,前提是实到解析正确且已到序号 ⊆ 应到序号 。
**关键事实 ** : `实 到件数` 是直接数实到表「单号」、按运单号分组去重得到的(**不再由「应到−未到」倒推**) ; `未到件数 = 应到件数 − 实 到件数` 。前提是实到单号能正确按运单号分组(分组规则见下表各站 `arrived_pieces_*` ) 。
---
## 三、各站点统计明细
| 站点 | 应到件数来源 | 已 到件数来源 | 实到序号解析 规则(`arrived` ) | 源文件(`downloads/` ) |
| 站点 | 应到件数来源 | 实 到件数来源 | 单号→运单号 分组 规则(`arrived_pieces_* ` ) | 源文件(`downloads/` ) |
|---|---|---|---|---|
| **中通 ** | `中通-应到货物数据.xlsx` 的「录单 件数」 | 应到−未到(推算 ) | `运单号` 列 是复合串 = 运单号 + 总数(4位 ) + 顺序号 (4位 ); 取右 8 位,前 4 为总数、后 4 为顺序号 | `中通-应到货物数据.xlsx` / `中通-实到货物数据.xlsx` |
| **顺心 ** | `顺心-应到货物数据.xlsx` 的「录单 件数」 | 应到−未到(推算 ) | `运单号` (干净) + `子单号` = 运单号 + 顺序号(3位);后缀(3位)即顺序号 | `顺心-应到货物数据.xlsx` / `顺心-实到货物数据.xlsx` |
| **韵达 ** | `韵达-应到货物数据.xlsx` 的「录单 件数」 | 应到−未到(推算 ) | `主单号` =运单号 , `子单号` =主单号 + 顺序号(4位);后缀(4位)即顺序号 | `韵达-应到货物数据.xlsx` / `韵达-实到货物数据.xlsx` |
| **安能 ** | `安能-应到货物数据.xlsx` 的「录单 件数」 | 应到−未到(推算 ) | `所属单号` =运单号 , `扫描单号` =所属单号 + 总数(4位) + 顺序号(4位); `has_total=True` ,取末 4 位为顺序号 | `安能-应到货物数据.xlsx` / `安能-实到货物数据.xlsx` |
| **中通 ** | `中通-应到货物数据.xlsx` 的「交接 件数」 | 实到表单号去重(直接数 ) | 实到 `运单号` 是复合串 = 运单号 + 总数(4) + 顺序(4); 按 `v[:-8]` 归并到应到运单号,每条复合串即 1 件 | `中通-应到货物数据.xlsx` / `中通-实到货物数据.xlsx` |
| **顺心 ** | `顺心-应到货物数据.xlsx` 的「交接 件数」 | 实到表单号去重(直接数 ) | 按 `运单号` 分组,`子单号` =每件(一件一个子单号) | `顺心-应到货物数据.xlsx` / `顺心-实到货物数据.xlsx` |
| **韵达 ** | `韵达-应到货物数据.xlsx` 的「交接 件数」 | 实到表单号去重(直接数 ) | 按 `主单号` 分组 , `子单号` =每件 | `韵达-应到货物数据.xlsx` / `韵达-实到货物数据.xlsx` |
| **安能 ** | `安能-应到货物数据.xlsx` 的「交接 件数」 | 实到表单号去重(直接数 ) | 按 `所属单号` 分组 , `扫描单号` =每件 | `安能-应到货物数据.xlsx` / `安能-实到货物数据.xlsx` |
| **百世 ** | **无 ** (站点只给未到) | **无(显示「—」) ** | 不适用(无实到基表) | 仅 `百世-应到未到货物数据.xlsx` ( = 当日未扫明细,本身就是未到结果) |
> 4 站合计/图表口径:`expected_undelive red .build_summary` 只累加 4 站(`应到−实到`口径),**百世不计入合计**(无应到基数)。百世在表中单列,未到件数 = 其明细行数。
> 4 站合计/图表口径:`compa re.build_summary` 只累加 4 站(`应到−实到`口径),**百世不计入合计**(无应到基数)。百世在表中单列,未到件数 = 其明细行数。
---
## 四、百世的特殊口径(务必注意)
百世是唯一「无应到/实到基数」的站点:
- 它的数据来自后台「扫描综合查询 → 到/接件扫描 → 当日 → 未扫」,导出即「当日未扫」明细(`site_ baishi.py` )。
- 它的数据来自后台「扫描综合查询 → 到/接件扫描 → 当日 → 未扫」,导出即「当日未扫」明细(`sites/ baishi.py` )。
- 因此 `process_baishi()` 只能给 `未到件数 = 行数` , `应到件数 = None` 、`已到件数 = None` 、完全/部分未到 = None。
- 报表里百世的应到/已到列显示「—」,未到率无法计算。
- **含义**:百世统计的是「今天还没扫到的件」,不是「相对应到总量的缺件率」。与 4 站口径不可直接相加比较。
@@ -97,8 +97,8 @@
## 七、潜在歧义与风险点(审查结论)
1. **已到件数是减出来的,不是数出来的 ** :依赖实到子单号解析正确。一旦某站 `子单号` 格式与解析规则(`arrived_*` )不匹配,该序号进不了「已到序号集」→ 误判为未到 → 已到件数 被低估、未到率虚高。解析 规则是 硬编码的 ,承运商改版号段即失准。
2. **应到件数依赖「录单 件数」 ** :若应到数据某运单 `录单 件数` 缺失/为 0/非数字,该运单被 `continue` 跳过,既不计入应到也不计入未到 → 静默漏统(应到总量被低估,但 该运单若 出现在实到中也因不在应到循环而永不计入已到,方向一致 )。
1. **实到依赖单号→运单号分组正确 ** :实到件数靠把实到表「单号」按运单号分组去重计数(`arrived_pieces_*` )。一旦某站单号格式与分组规则不匹配(如复合串切分错),该件归不到对应运单 → 实到 被低估、未到率虚高。规则硬编码,承运商改版号段即失准。
2. **应到件数依赖「交接 件数」 ** :若应到数据某运单 `交接 件数` 缺失/为 0/非数字,该运单被跳过,既不计入应到也不计入未到 → 静默漏统(应到总量被低估; 该运单即便 出现在实到中也因不在应到循环而无处抵扣 )。
3. **应到按运单号去重( keep first) ** :同一运单多条交接记录只取首条 `录单件数` 。若重复行的件数不同,取首条,可能与实际不符。
4. **百世不可并入合计 ** : 4 站合计的「已到总件数」不含百世;跨站看「已到」时别把百世当成有应到基数的站。
5. **应到/实到业务日期可能错位 ** (见第五节):两表不同步下载或偏移不一致时,比对口径失真。
@@ -111,9 +111,10 @@
| 文件 | 角色 |
|---|---|
| `expected_undelive red .py` | 统计核心:`process()` ( 4站比对) 、`process_baishi()` (百世)、`build_summary()` (汇总报表)、`STATIONS` (各站解析配置) |
| `compa re.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` (业务日期/偏移写入) |
| `site_ 中通/ 顺心/ 韵达/ 安能.py` | 各站 `expected_download` / `actual_download` ( Playwright/CDP 导出源表) |
| `site_百世 .py` | `baishi_download_undelivered_data` (直接导「当日未扫」) |
| `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` |
| `server.py` | `/status` 、`/report` 、`/data/{filename}` 接口 |
| `cli/ server.py` | `/status` 、`/report` 、`/data/{filename}` 接口 |