修复安能实到日期设定 + 站点统计口径重构
- site_anneng.py: set_scan_date 改用 execCommand 全选替换+回车(修复 Ant DatePicker 受控组件未写入导致实到下成当天); wait_scan_form_ready 增加 settle 与 DatePicker 预热,消除首设竞态 - expected_undelivered.py: 应到口径改交接件数、实到改单号去重、未到明细列已到单号(不再编子单号),百世排除 - docs: 补充站点统计逻辑审查报告与韵达/安能计算逻辑梳理
This commit is contained in:
119
docs/站点统计逻辑审查报告.md
Normal file
119
docs/站点统计逻辑审查报告.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 后端各站点「应到件数 / 已到件数」统计逻辑审查报告
|
||||
|
||||
> 审查范围:`InboundVerify` 后端
|
||||
> 核心模块:`expected_undelivered.py`(比对统计)、`runtime.py`(调度/下载路由)、`site_*.py`(各站数据采集)、`server.py`(对外接口)
|
||||
> 审查重点:每个站点的「应到件数」和「已到件数」如何统计、数据来源、口径与潜在歧义。
|
||||
|
||||
---
|
||||
|
||||
## 一、总体架构(两层)
|
||||
|
||||
统计逻辑分两层,前端看到的「应到/已到/未到」计数最终都来自**第二层**,且只在跑比对时才生成。
|
||||
|
||||
| 层 | 模块 | 职责 | 产物 |
|
||||
|---|---|---|---|
|
||||
| **① 数据采集层** | `site_顺心/中通/韵达/安能.py`、`site_百世.py` | 用 Playwright(安能用 CDP)登录各承运商后台,导出 Excel 到 `downloads/` | `downloads/<站>-应到货物数据.xlsx`、`downloads/<站>-实到货物数据.xlsx`、`downloads/<站>-未到数据.xlsx` |
|
||||
| **② 比对统计层** | `expected_undelivered.py` | 读 `downloads/` 源文件,两表比对,算出应到/已到/未到件数并出报表 | `output/应到未到数据.xlsx`(汇总+各站明细) |
|
||||
|
||||
调度入口:`runtime.py` 的 `TASK_HANDLERS`。
|
||||
- 4 站:`expected` 下载 + `actual` 下载 + `undelivered`(先下应到+实到,再调 `write_site_file` 算出未到)。
|
||||
- 百世:只有 `undelivered`(直接导「当日未扫」明细,无应到/实到两份基表)。
|
||||
- `__compare__`(菜单[9]):调 `expected_undelivered.main()`,用 `downloads/` 现有文件生成全站汇总报表。
|
||||
|
||||
---
|
||||
|
||||
## 二、核心统计公式(4 站统一口径)
|
||||
|
||||
`expected_undelivered.process(name)` 是 4 站统计的唯一实现:
|
||||
|
||||
```
|
||||
对每一条应到运单(按运单号去重,保留首条):
|
||||
应到件数 N = 应到数据中的「录单件数」(输出列名「总件数」)
|
||||
应到序号集 = {1, 2, …, N}
|
||||
已到序号集 = 从「实到货物数据.xlsx」解析该运单实际到货的子单序号
|
||||
未到序号集 = {1…N} − 已到序号集
|
||||
未到件数 += len(未到序号集) # 每缺 1 个子单 → 1 行未到明细
|
||||
应到件数 += N
|
||||
|
||||
站点级:
|
||||
应到件数 = Σ N(所有应到运单)
|
||||
未到件数 = Σ len(未到序号集)
|
||||
已到件数 = 应到件数 − 未到件数 ← 注意:是「减出来」的,不是直接数实到
|
||||
未到率 = 未到件数 ÷ 应到件数
|
||||
完全未到运单 = 未到序号集 == {1…N} 的运单数
|
||||
部分未到运单 = 0 < 未到 < N 的运单数
|
||||
```
|
||||
|
||||
**关键事实**:`已到件数` 是 `应到件数 − 未到件数` 推算出来的(`exp_pieces - len(rows)`),并非独立去数实到表。它等价于 `Σ|已到序号集|`,前提是实到解析正确且已到序号 ⊆ 应到序号。
|
||||
|
||||
---
|
||||
|
||||
## 三、各站点统计明细
|
||||
|
||||
| 站点 | 应到件数来源 | 已到件数来源 | 实到序号解析规则(`arrived`) | 源文件(`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 站合计/图表口径:`expected_undelivered.build_summary` 只累加 4 站(`应到−实到`口径),**百世不计入合计**(无应到基数)。百世在表中单列,未到件数 = 其明细行数。
|
||||
|
||||
---
|
||||
|
||||
## 四、百世的特殊口径(务必注意)
|
||||
|
||||
百世是唯一「无应到/实到基数」的站点:
|
||||
- 它的数据来自后台「扫描综合查询 → 到/接件扫描 → 当日 → 未扫」,导出即「当日未扫」明细(`site_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_*`)不匹配,该序号进不了「已到序号集」→ 误判为未到 → 已到件数被低估、未到率虚高。解析规则是硬编码的,承运商改版号段即失准。
|
||||
2. **应到件数依赖「录单件数」**:若应到数据某运单 `录单件数` 缺失/为 0/非数字,该运单被 `continue` 跳过,既不计入应到也不计入未到 → 静默漏统(应到总量被低估,但该运单若出现在实到中也因不在应到循环而永不计入已到,方向一致)。
|
||||
3. **应到按运单号去重(keep first)**:同一运单多条交接记录只取首条 `录单件数`。若重复行的件数不同,取首条,可能与实际不符。
|
||||
4. **百世不可并入合计**:4 站合计的「已到总件数」不含百世;跨站看「已到」时别把百世当成有应到基数的站。
|
||||
5. **应到/实到业务日期可能错位**(见第五节):两表不同步下载或偏移不一致时,比对口径失真。
|
||||
6. **计数是比对产物,非实时**:前端若要「实时件数」需先触发比对任务;`/status` 的 `ready` 仅表示「下载成功」,不代表「已比对出数」。
|
||||
7. **子单号是程序现拼的**:4 站缺件的「子单号/扫描单号」由 `code_*` 按各站编号规则生成(`code_shunxin` 等),并非实到原始记录——明细里的缺件单号是推算值,用于人工核对,不是系统回执。
|
||||
|
||||
---
|
||||
|
||||
## 八、关键文件索引
|
||||
|
||||
| 文件 | 角色 |
|
||||
|---|---|
|
||||
| `expected_undelivered.py` | 统计核心:`process()`(4站比对)、`process_baishi()`(百世)、`build_summary()`(汇总报表)、`STATIONS`(各站解析配置) |
|
||||
| `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`(直接导「当日未扫」) |
|
||||
| `state_store.py` | `site_status`(ready/business_date)、`site_config`(offset/schedule)、`get_offset` |
|
||||
| `server.py` | `/status`、`/report`、`/data/{filename}` 接口 |
|
||||
124
docs/韵达安能计算逻辑梳理.md
Normal file
124
docs/韵达安能计算逻辑梳理.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 韵达 / 安能 应到未到计算逻辑梳理(基于 expected_undelivered.py 重构版代码)
|
||||
|
||||
> 用途:供审核两站「应到件数 / 实到件数 / 未到件数」的实现逻辑与关联键。
|
||||
> 代码基准:`InboundVerify/expected_undelivered.py`(重构版)。行号对应本次读取。
|
||||
|
||||
---
|
||||
|
||||
## 〇、两站共用的计算骨架(process 函数,第 134–216 行)
|
||||
|
||||
无论韵达还是安能,最终都走同一个 `process(name)`:
|
||||
|
||||
1. 读应到表 + 实到表(第 148–149 行)。
|
||||
2. 应到表按关联键列去重、保留首条(第 152–153 行)。
|
||||
3. **应到件数** = 应到表「交接件数」之和(第 158–168 行)。
|
||||
4. **实到件数** = 实到表单件号列的**全局去重数**(第 176–177 行,`act_pieces = sum(len(s) for s in arrived.values())`)。
|
||||
5. 逐运单比较:实到扫到数 `<` 应到件数 的运单,才进未到明细(第 183–201 行)。
|
||||
6. **未到件数** = `max(0, 应到件数 − 实到件数)`(第 210 行)。
|
||||
|
||||
两站差异**只在配置**(关联键列、单件号列不同),算法完全一致。
|
||||
|
||||
---
|
||||
|
||||
## 一、韵达
|
||||
|
||||
### 1.1 配置(STATIONS 第 106–115 行)
|
||||
| 配置项 | 值 | 含义 |
|
||||
|---|---|---|
|
||||
| exp(应到文件) | `韵达-应到货物数据.xlsx` | 源 |
|
||||
| act(实到文件) | `韵达-实到货物数据.xlsx` | 源 |
|
||||
| exp_qty | `交接件数` | 应到件数的取值列 |
|
||||
| exp_wb | `运单号` | 应到表去重键 **+ 关联键** |
|
||||
| exp_jd | `交接单号` | 未到明细展示字段 |
|
||||
| arrived_pieces | `arrived_pieces_by_cols("主单号", "子单号")` | 实到解析 |
|
||||
| ├ wb_col | `主单号`(实到表) | 实到侧**关联基号**列 |
|
||||
| └ piece_col | `子单号`(实到表) | 实到侧**单件号**列 |
|
||||
|
||||
### 1.2 两套表的字段角色
|
||||
- **应到表**:`运单号`(去重+关联)、`交接件数`(该单应到件数)、`交接单号`(展示)。
|
||||
- **实到表**:`主单号`(关联基号,须与应到`运单号`同值域)、`子单号`(每件货物的单号,一个单号=一件)。
|
||||
|
||||
### 1.3 关联(Join)条件
|
||||
```
|
||||
应到表.运单号 == 实到表.主单号
|
||||
```
|
||||
机制:`arrived_pieces_by_cols` 以「主单号」为 key 建 dict(第 76–81 行);`process` 第 185 行 `arrived.get(wb)` 用**应到运单号 wb** 去查该 dict。两列值必须相等才对得上。
|
||||
|
||||
### 1.4 计算口径
|
||||
- 应到件数 = Σ 交接件数(按运单号去重后)。
|
||||
- 实到件数 = 实到表「子单号」全局去重数(不同主单号下即使子单号文本相同也只计一次)。
|
||||
- 未到件数 = `max(0, 应到 − 实到)`。
|
||||
- 未到明细(每行一个短少运单):`交接单号 | 运单号 | 总件数(=应到件数) | 已到单号1..k`;
|
||||
「已到单号1..k」= 该主单号下所有子单号(排序后填入),k = 实际扫到件数(第 199–200 行)。
|
||||
仅当「实到扫到数 < 应到件数」才列入(第 187–188 行 `arrived_cnt >= n` 跳过)。
|
||||
|
||||
### 1.5 当前数据状态(实测,2026-07-19 10:50)
|
||||
- 应到运单数 **57**,实到基号数 **58**,交集命中 **57** ✅
|
||||
- 应到件数 = 140,实到单号去重 = 141
|
||||
- 样本:`应到 617936212` == `实到主单号 617936212`,完全对齐
|
||||
- **结论:韵达关联键现已对齐,逻辑可正确产出结果。**(此前 2/57 为旧数据,已失效)
|
||||
|
||||
---
|
||||
|
||||
## 二、安能
|
||||
|
||||
### 2.1 配置(STATIONS 第 116–125 行)
|
||||
| 配置项 | 值 | 含义 |
|
||||
|---|---|---|
|
||||
| exp(应到文件) | `安能-应到货物数据.xlsx` | 源 |
|
||||
| act(实到文件) | `安能-实到货物数据.xlsx` | 源 |
|
||||
| exp_qty | `交接件数` | 应到件数取值列 |
|
||||
| exp_wb | `运单号` | 应到表去重键 **+ 关联键** |
|
||||
| exp_jd | `交接单号` | 未到明细展示字段 |
|
||||
| arrived_pieces | `arrived_pieces_by_cols("所属单号", "扫描单号")` | 实到解析 |
|
||||
| ├ wb_col | `所属单号`(实到表) | 实到侧**关联基号**列 |
|
||||
| └ piece_col | `扫描单号`(实到表) | 实到侧**单件号**列 |
|
||||
|
||||
### 2.2 两套表的字段角色
|
||||
- **应到表**:`运单号`(去重+关联)、`交接件数`(应到件数)、`交接单号`(展示)。
|
||||
- **实到表**:`所属单号`(关联基号,须与应到`运单号`同值域)、`扫描单号`(每件单号,格式=`所属单号`+总数4位+顺序4位,一个=一件)。
|
||||
|
||||
### 2.3 关联(Join)条件
|
||||
```
|
||||
应到表.运单号 == 实到表.所属单号
|
||||
```
|
||||
机制同韵达:`arrived_pieces_by_cols` 以「所属单号」建 key dict;`process` 用应到运单号去查。
|
||||
|
||||
### 2.4 计算口径(与韵达结构完全一致)
|
||||
- 应到件数 = Σ 交接件数。
|
||||
- 实到件数 = 实到表「扫描单号」全局去重数。
|
||||
- 未到件数 = `max(0, 应到 − 实到)`。
|
||||
- 未到明细:`交接单号 | 运单号 | 总件数 | 已到单号1..k`(k=该所属单号下扫描单号数)。
|
||||
|
||||
### 2.5 当前数据状态(实测,2026-07-19 10:50)
|
||||
- 应到运单数 **97**,实到基号数 **140**,交集命中 **4** ❌
|
||||
- 应到件数 = 236,实到单号去重 = 279
|
||||
- 样本:`应到 750101236894` vs `实到所属单号 760237639793` —— 两套不同编号体系
|
||||
- **后果**:实到 279 件几乎全无法对应到任何应到运单 → 明细把 93 个运单列「完全未到」,但汇总层 `实到=279 > 应到=236` → 未到净额被 `max(0,…)` 截断为 **0**,于是「明细列 96 行短少」与「汇总未到=0」自相矛盾。
|
||||
- **结论:安能关联键未对齐,当前产出不可信。**
|
||||
|
||||
---
|
||||
|
||||
## 三、两站逻辑差异对照
|
||||
|
||||
| 维度 | 韵达 | 安能 |
|
||||
|---|---|---|
|
||||
| 应到关联键列 | `运单号` | `运单号` |
|
||||
| 实到关联基号列 | `主单号` | `所属单号` |
|
||||
| 实到单件号列 | `子单号` | `扫描单号` |
|
||||
| 单件号是否带后缀 | 是(主单号+顺序号) | 是(所属单号+总数+顺序) |
|
||||
| 关联键是否对齐(当前数据) | ✅ 57/57 | ❌ 4/97 |
|
||||
| 计算结果是否可信 | 可信 | 不可信(自相矛盾) |
|
||||
|
||||
> 两站**算法完全相同**,唯一区别是实到侧的「基号列 / 单件号列」列名不同。因此问题不在代码逻辑,而在**安能的关联键取值域对不上**。
|
||||
|
||||
---
|
||||
|
||||
## 四、审核要点(请你判断)
|
||||
|
||||
1. **韵达**:关联键 `应到.运单号 == 实到.主单号` 是否符合业务实际?当前数据已对齐,似乎正确;若你确认,韵达逻辑可定稿。
|
||||
2. **安能**:关联键 `应到.运单号 == 实到.所属单号` 是否正确?实测两套编号不重合(97 个应到运单仅 4 个能在实到找到)。可能的方向:
|
||||
- (a) 实到表存在另一列能与应到`运单号`对应(需确认列名,可能改 `wb_col`);
|
||||
- (b) 应到/实到文件是按不同条件/批次拉的,需要按同一天、同线路重新拉取;
|
||||
- (c) 暂时把安能排除(同百世),等数据对齐再加回。
|
||||
3. **共同结构问题**:汇总「未到件数」用净额 `max(0, 应到−实到)`,而明细按「逐运单短少」列——当关联键断裂时二者会矛盾(安能现例)。关联键对齐后此矛盾自然消失;是否需要在代码里对「净额 vs 明细」做一致性校验/告警,也请你定。
|
||||
Reference in New Issue
Block a user