Compare commits
2 Commits
09e05f8dfc
...
95597fbb0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95597fbb0c | ||
|
|
f66e6dd39e |
332
docs/2026-07-31-四站点差缺对比逻辑审查报告.md
Normal file
332
docs/2026-07-31-四站点差缺对比逻辑审查报告.md
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
# 四站点差缺对比逻辑审查报告
|
||||||
|
|
||||||
|
> 审查日期:2026-07-31
|
||||||
|
> 审查范围:顺心、中通、韵达、安能 四个站点的应到 vs 实到差缺对比逻辑
|
||||||
|
> 排除:百世(站点直供未到明细,不参与四站比对)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、比对算法总览(四站共用)
|
||||||
|
|
||||||
|
`compare.py:process()` 对四个站点执行**完全相同**的算法步骤。站点间的差异仅由 `domain.py:STATIONS` 配置注入——列名映射 + 实到单号解析器。
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤1: 读应到Excel → 按运单号去重keep-first → 构建 {运单号 → (交接单号, 交接件数=n)}
|
||||||
|
步骤2: 读实到Excel → 站点专用解析器 → 构建 {运单基号 → {已到单号集合}}
|
||||||
|
步骤3: 逐运单比对
|
||||||
|
arrived_cnt >= n → 足额到货,跳过
|
||||||
|
arrived_cnt == 0 → 完全未到
|
||||||
|
0 < arrived < n → 部分未到
|
||||||
|
步骤4: 产出未到明细(交接单号 | 运单号 | 总件数 | 已到单号1 | 已到单号2 | ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心口径
|
||||||
|
|
||||||
|
| 指标 | 口径 |
|
||||||
|
|------|------|
|
||||||
|
| 应到件数 | **交接件数**(非录单件数);按运单号去重 keep-first |
|
||||||
|
| 实到件数 | 单号去重计数(每扫描一件=一个单号) |
|
||||||
|
| 未到件数 | max(0, 应到件数 − 实到件数) |
|
||||||
|
| 未到率 | 未到件数 ÷ 应到件数 |
|
||||||
|
|
||||||
|
### 未到明细输出约定
|
||||||
|
|
||||||
|
- 仅列出**短少运单**(实到 < 应到)
|
||||||
|
- 列出该运单**实际已到的单号**(已到单号1, 已到单号2, ...)
|
||||||
|
- **不编造缺件子单号**——实到扫描顺序号乱序,无法反推缺了哪个顺序号
|
||||||
|
|
||||||
|
### 统计指标
|
||||||
|
|
||||||
|
| 指标 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| 运单数 | 应到运单去重数 |
|
||||||
|
| 应到件 | Σ 交接件数 |
|
||||||
|
| 已到件 | Σ 实到单号去重数 |
|
||||||
|
| 未到件 | max(0, 应到件 − 已到件) |
|
||||||
|
| 涉及运单 | 存在短少的运单数 |
|
||||||
|
| 完全未到 | 整单零到货运单数 |
|
||||||
|
| 部分未到 | 部分缺件运单数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、四站配置对照
|
||||||
|
|
||||||
|
`domain.py:STATIONS` — 所有差异集中于此配置表,比对核心代码不感知站点差异。
|
||||||
|
|
||||||
|
| 维度 | 中通 | 顺心 | 韵达 | 安能 |
|
||||||
|
|------|------|------|------|------|
|
||||||
|
| 应到文件 | `中通-应到货物数据.xlsx` | `顺心-应到货物数据.xlsx` | `韵达-应到货物数据.xlsx` | `安能-应到货物数据.xlsx` |
|
||||||
|
| 实到文件 | `中通-实到货物数据.xlsx` | `顺心-实到货物数据.xlsx` | `韵达-实到货物数据.xlsx` | `安能-实到货物数据.xlsx` |
|
||||||
|
| 应到-运单号列 | `运单号` | `运单号` | `运单号` | `运单号` |
|
||||||
|
| 应到-件数列 | `交接件数` | `交接件数` | `交接件数` | `交接件数` |
|
||||||
|
| 应到-交接单号列 | `交接单号` | `交接单号` | `交接单号` | `交接单号` |
|
||||||
|
| 实到-基号列 | —(从复合串推导) | `运单号` | **`主单号`** | **`所属单号`** |
|
||||||
|
| 实到-单号列 | `运单号`(复合串) | `子单号` | `子单号` | `扫描单号` |
|
||||||
|
| 解析器 | `arrived_pieces_zhongtong` | `arrived_pieces_by_cols` | `arrived_pieces_by_cols` | `arrived_pieces_by_cols` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、逐站点详细分析
|
||||||
|
|
||||||
|
### 3.1 中通(ZTO)
|
||||||
|
|
||||||
|
#### 业务逻辑
|
||||||
|
|
||||||
|
实到货物数据中的「运单号」为复合串,由三部分构成:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬────────────┬──────────┐
|
||||||
|
│ 运单号 │ 录单件数 │ 顺序号 │
|
||||||
|
│ (12位) │ (4位) │ (4位) │
|
||||||
|
└──────────┴────────────┴──────────┘
|
||||||
|
总长 20 位
|
||||||
|
|
||||||
|
示例: 330953527953 0001 0001
|
||||||
|
├─ 运单号 ─┤├录单┤├顺序┤
|
||||||
|
```
|
||||||
|
|
||||||
|
- **运单号(12位)**: 与应到货物数据中的运单号对齐
|
||||||
|
- **录单件数(4位)**: 该运单在系统中的录单总件数,0占位
|
||||||
|
- **顺序号(4位)**: 0占位,如 `0001`, `0002`, `0003`, `0004`
|
||||||
|
|
||||||
|
对比逻辑:
|
||||||
|
1. 从应到数据取运单号 + 交接件数(**非录单件数**)
|
||||||
|
2. 从实到数据取复合串,掐尾8位得运单基号,完整串为子运单号
|
||||||
|
3. 按运单基号分组,子运单号去重得实到件数
|
||||||
|
4. 实到件数 < 交接件数 → 差缺
|
||||||
|
|
||||||
|
> **重要**: 录单件数仅作参考。举例:某运单录单件数=4、交接件数=2,实到最多出现2条数据。如果只出现了1条,我们只知道差缺了,但**无法判断具体差缺了哪一件**(顺序号乱序)。
|
||||||
|
|
||||||
|
#### 代码实现
|
||||||
|
|
||||||
|
`domain.py:17-26` — 实到解析器:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def arrived_pieces_zhongtong(df):
|
||||||
|
res = defaultdict(set)
|
||||||
|
for v in df["运单号"]:
|
||||||
|
v = str(v).strip()
|
||||||
|
if len(v) > 8 and v[-4:].isdigit():
|
||||||
|
res[v[:-8]].add(v) # 基号=前12位, 已到单号=完整20位复合串
|
||||||
|
return res
|
||||||
|
```
|
||||||
|
|
||||||
|
`domain.py:48-56` — 站点配置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"name": "中通",
|
||||||
|
"exp_qty": "交接件数", # 应到件数口径:交接件数(非录单件数)
|
||||||
|
"exp_wb": "运单号",
|
||||||
|
"exp_jd": "交接单号",
|
||||||
|
"arrived_pieces": arrived_pieces_zhongtong,
|
||||||
|
"columns": ["交接单号", "运单号", "总件数"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 对齐情况:✅ 对齐
|
||||||
|
|
||||||
|
代码实现与业务逻辑一致。`v[:-8]` 掐尾8位得12位运单基号,保留完整复合串作为已到单号——不解析、不推断录单件数和顺序号的具体含义。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 安能(Anneng)
|
||||||
|
|
||||||
|
#### 业务逻辑
|
||||||
|
|
||||||
|
与中通相同的差缺对比逻辑。
|
||||||
|
|
||||||
|
安能实到数据同样为复合串,结构:`运单号(12位) + 录单件数(4位) + 顺序号(4位)`(20位)。
|
||||||
|
|
||||||
|
与中通的关键区别:安能实到表有**独立的「所属单号」列**(干净运单基号),无需像中通那样从复合串掐尾8位推导基号。
|
||||||
|
|
||||||
|
#### 代码实现
|
||||||
|
|
||||||
|
`domain.py:76-86`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"name": "安能",
|
||||||
|
"arrived_pieces": arrived_pieces_by_cols("所属单号", "扫描单号"),
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
安能使用 `arrived_pieces_by_cols` 而非 `arrived_pieces_zhongtong`——直接从「所属单号」列读基号、从「扫描单号」列读完整单号,效果等价。
|
||||||
|
|
||||||
|
| 差异点 | 中通 | 安能 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 实到基号来源 | 从复合串解析(`v[:-8]`) | 直接读「所属单号」列 |
|
||||||
|
| 实到单号来源 | 复合串本身(「运单号」列) | 「扫描单号」列 |
|
||||||
|
| 解析器 | `arrived_pieces_zhongtong` | `arrived_pieces_by_cols` |
|
||||||
|
| 最终产出 | `{基号 → {完整单号集合}}` | 相同 |
|
||||||
|
|
||||||
|
#### 数据库验证
|
||||||
|
|
||||||
|
```
|
||||||
|
piece_no=61003282264500140014 → waybill_no=610032822645 (12位), total=0014, seq=0014
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 对齐情况:✅ 对齐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 顺心(Shunxin)⚠️
|
||||||
|
|
||||||
|
#### 业务逻辑
|
||||||
|
|
||||||
|
顺心站点需区分两类运单:
|
||||||
|
|
||||||
|
**A. 非SF开头运单(占 97%):**
|
||||||
|
|
||||||
|
实到「子单号」结构为两部分:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬──────────┐
|
||||||
|
│ 运单号 │ 顺序号 │
|
||||||
|
│ (不定长) │ (3位) │
|
||||||
|
└──────────┴──────────┘
|
||||||
|
|
||||||
|
示例: S71623721115 001
|
||||||
|
├─ 运单号 ──┤├顺序┤
|
||||||
|
|
||||||
|
注意:顺心子单号无录单件数部分(仅两部分)
|
||||||
|
```
|
||||||
|
|
||||||
|
对比时从实到取「子单号」列,按「运单号」分组,子单号去重得实到件数。
|
||||||
|
|
||||||
|
**B. SF开头运单(占 3%):**
|
||||||
|
|
||||||
|
SF订单的「子单号」为**随机号码**(非由运单号衍生),不能用于差缺推导。
|
||||||
|
|
||||||
|
对比逻辑:
|
||||||
|
1. 在实到数据中按「运单号」字段查找,统计出现次数
|
||||||
|
2. 出现次数 < 交接件数 → 差缺
|
||||||
|
3. 将找到的子单号(虽随机但可以列出来)填入「已到单号」列
|
||||||
|
|
||||||
|
SF订单的差缺判定:**只基于交接件数与实到运单号出现次数的比较**,不依赖子单号的结构解析。
|
||||||
|
|
||||||
|
#### 代码实现
|
||||||
|
|
||||||
|
`domain.py:57-65`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"name": "顺心",
|
||||||
|
"arrived_pieces": arrived_pieces_by_cols("运单号", "子单号"),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SF 与非 SF 没有任何区分处理。** 所有运单走同一条路径。
|
||||||
|
|
||||||
|
#### 数据库验证
|
||||||
|
|
||||||
|
**非SF(正常):**
|
||||||
|
```
|
||||||
|
子单号=S71623721115001 → 运单号=S71623721115 + 后缀=001 ✅
|
||||||
|
子单号=S71934073996002 → 运单号=S71934073996 + 后缀=002 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**SF(异常):**
|
||||||
|
```
|
||||||
|
运单号=SF1225002296515 的两条实到记录:
|
||||||
|
子单号=SF2025318183224 (随机SF号码)
|
||||||
|
子单号=SF1225002296515 (与运单号相同)
|
||||||
|
```
|
||||||
|
数据中有 10 个SF运单存在多条实到记录。
|
||||||
|
|
||||||
|
#### 对齐情况:⚠️ 部分对齐,SF特殊逻辑缺失
|
||||||
|
|
||||||
|
| 检查项 | 代码现状 | 业务要求 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| 非SF处理 | ✅ `arrived_pieces_by_cols("运单号", "子单号")` | 一致 |
|
||||||
|
| 非SF子单号结构 | ✅ 运单号 + 顺序号(两部分) | 一致 |
|
||||||
|
| SF处理 | ❌ 与非SF完全一致,使用子单号去重 | **不能**使用子单号,只按运单号行数计数 |
|
||||||
|
| 功能影响 | 子单号虽随机但值唯一,按目前逻辑也能正确去重计数 | 但语义不正确——SF子单号不由运单号衍生 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 韵达(Yunda)❌
|
||||||
|
|
||||||
|
#### 业务逻辑
|
||||||
|
|
||||||
|
**去重规则:** 韵达实到数据存在重复行(同一子单号出现两次)。去重依据为「交接单号」字段:
|
||||||
|
- **保留**交接单号为**空**的行
|
||||||
|
- **丢弃**交接单号**非空**的行
|
||||||
|
|
||||||
|
**子单号结构:** 两部分——单号 + 顺序号(无录单件数部分)。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬──────────┐
|
||||||
|
│ 主单号 │ 顺序号 │
|
||||||
|
│ (不定长) │ (4位) │
|
||||||
|
└──────────┴──────────┘
|
||||||
|
|
||||||
|
示例: 713326603 0003
|
||||||
|
├─主单号─┤├顺序┤
|
||||||
|
```
|
||||||
|
|
||||||
|
**对比方式:** 与中通/安能同——按「主单号」分组,「子单号」去重得实到件数,与交接件数比对。
|
||||||
|
|
||||||
|
#### 代码实现
|
||||||
|
|
||||||
|
`store.py:316-320`(入库过滤):
|
||||||
|
|
||||||
|
```python
|
||||||
|
if site == "韵达":
|
||||||
|
# 韵达业务清洗:抛弃「交接单号」为空的行(派件/签收等其他扫描无交接单号),
|
||||||
|
# 再按子单号去重(一件多扫只留一条;清洗后子单号已天然唯一,drop 为保险)。
|
||||||
|
df = df[df["交接单号"].astype(str).str.strip() != ""] # ← 保留非空
|
||||||
|
df = df.drop_duplicates(subset=[cm["piece"]], keep="last")
|
||||||
|
```
|
||||||
|
|
||||||
|
`domain.py:67-75`(比对配置):
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"name": "韵达",
|
||||||
|
"exp_wb": "运单号",
|
||||||
|
"arrived_pieces": arrived_pieces_by_cols("主单号", "子单号"),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 对齐情况:❌ 交接单号过滤逻辑完全相反
|
||||||
|
|
||||||
|
| 检查项 | 代码现状 | 业务要求 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| 交接单号过滤 | 保留 `!= ""`(**非空**) | 保留 `== ""`(**空**) |
|
||||||
|
| 子单号结构 | ✅ `7133266030003` = wb`713326603` + seq`0003` | 一致 |
|
||||||
|
| 实到解析 | ✅ `arrived_pieces_by_cols("主单号", "子单号")` | 一致 |
|
||||||
|
| compare.py 过滤 | ❌ **无过滤**,所有行参与比对 | 需要过滤 |
|
||||||
|
|
||||||
|
**影响分析:**
|
||||||
|
|
||||||
|
1. `store.py` 过滤反了——入库时留下了错误的数据集
|
||||||
|
2. `compare.py` 完全没有交接单号过滤——如果原始 Excel 中同时存在空和非空行,比对阶段会全部读入导致重复计数
|
||||||
|
3. 当前数据库中韵达 3483 条记录全部为非空交接单号——说明当前 Excel 数据中空交接单号行偏少或不存在,但这不改变逻辑错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、差异汇总
|
||||||
|
|
||||||
|
| # | 站点 | 问题 | 严重程度 | 影响范围 |
|
||||||
|
|---|------|------|----------|----------|
|
||||||
|
| 1 | **韵达** | 交接单号过滤反了:`!= ""` 应改为 `== ""` | ❌ 严重 | `store.py:319` + `compare.py` 需新增过滤 |
|
||||||
|
| 2 | **顺心** | SF运单无特殊处理,与非SF混用子单号 | ⚠️ 中等 | `domain.py` 需新增SF判断分支 |
|
||||||
|
| 3 | **中通** | 录单件数0占位描述与实际数据完全一致 | ✅ 无影响 | 代码不依赖此区分 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、代码位置索引
|
||||||
|
|
||||||
|
| 逻辑 | 文件 | 行号 |
|
||||||
|
|------|------|------|
|
||||||
|
| 单站比对 `process()` | `compare.py` | 61-143 |
|
||||||
|
| 站点配置 `STATIONS` | `domain.py` | 46-87 |
|
||||||
|
| 中通实到解析器 | `domain.py` | 17-26 |
|
||||||
|
| 通用实到解析器 | `domain.py` | 29-42 |
|
||||||
|
| 单站未到文件写入 | `compare.py` | 258-273 |
|
||||||
|
| 全量汇总报告 | `compare.py` | 293-324 |
|
||||||
|
| 未到触发编排 | `runtime.py` | 478-497 |
|
||||||
|
| 韵达入库过滤(需修) | `store.py` | 316-320 |
|
||||||
|
| 顺心实到配置(需修) | `domain.py` | 57-65 |
|
||||||
@@ -75,6 +75,12 @@ def process(name):
|
|||||||
df_exp = pd.read_excel(exp_path, dtype=str).fillna("")
|
df_exp = pd.read_excel(exp_path, dtype=str).fillna("")
|
||||||
df_act = pd.read_excel(act_path, dtype=str).fillna("")
|
df_act = pd.read_excel(act_path, dtype=str).fillna("")
|
||||||
|
|
||||||
|
if name == "韵达":
|
||||||
|
# 韵达实到数据有重复行(同子单号出现两次),保留交接单号为空的(到/接件扫描),
|
||||||
|
# 丢弃交接单号不为空的(派件/签收等),再按子单号去重。
|
||||||
|
df_act = df_act[df_act["交接单号"].astype(str).str.strip() == ""]
|
||||||
|
df_act = df_act.drop_duplicates(subset=["子单号"], keep="last")
|
||||||
|
|
||||||
# 同一运单可能有多条交接记录,按运单号去重、保留首条
|
# 同一运单可能有多条交接记录,按运单号去重、保留首条
|
||||||
dup = int(df_exp[cfg["exp_wb"]].duplicated().sum())
|
dup = int(df_exp[cfg["exp_wb"]].duplicated().sum())
|
||||||
df_exp = df_exp.drop_duplicates(subset=[cfg["exp_wb"]], keep="first")
|
df_exp = df_exp.drop_duplicates(subset=[cfg["exp_wb"]], keep="first")
|
||||||
|
|||||||
@@ -314,9 +314,10 @@ def _ingest_actual(cur, site):
|
|||||||
cm = ACTUAL_COLMAP[site]
|
cm = ACTUAL_COLMAP[site]
|
||||||
df = pd.read_excel(path, dtype=str).fillna("")
|
df = pd.read_excel(path, dtype=str).fillna("")
|
||||||
if site == "韵达":
|
if site == "韵达":
|
||||||
# 韵达业务清洗:抛弃「交接单号」为空的行(派件/签收等其他扫描无交接单号),
|
# 韵达业务清洗:保留「交接单号」为空的行(到/接件扫描),
|
||||||
|
# 抛弃「交接单号」不为空的行(派件/签收等,属重复数据)。
|
||||||
# 再按子单号去重(一件多扫只留一条;清洗后子单号已天然唯一,drop 为保险)。
|
# 再按子单号去重(一件多扫只留一条;清洗后子单号已天然唯一,drop 为保险)。
|
||||||
df = df[df["交接单号"].astype(str).str.strip() != ""]
|
df = df[df["交接单号"].astype(str).str.strip() == ""]
|
||||||
df = df.drop_duplicates(subset=[cm["piece"]], keep="last")
|
df = df.drop_duplicates(subset=[cm["piece"]], keep="last")
|
||||||
rows = []
|
rows = []
|
||||||
for r in df.to_dict("records"):
|
for r in df.to_dict("records"):
|
||||||
|
|||||||
Reference in New Issue
Block a user