# 后端各站点「应到件数 / 已到件数」统计逻辑审查报告 > 审查范围:`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` | **不含**计数,只给就绪标志 | | `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}` 接口 |