12 KiB
四站点差缺对比逻辑审查报告
审查日期: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
对比逻辑:
- 从应到数据取运单号 + 交接件数(非录单件数)
- 从实到数据取复合串,掐尾8位得运单基号,完整串为子运单号
- 按运单基号分组,子运单号去重得实到件数
- 实到件数 < 交接件数 → 差缺
重要: 录单件数仅作参考。举例:某运单录单件数=4、交接件数=2,实到最多出现2条数据。如果只出现了1条,我们只知道差缺了,但无法判断具体差缺了哪一件(顺序号乱序)。
代码实现
domain.py:17-26 — 实到解析器:
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 — 站点配置:
{
"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:
{
"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订单的「子单号」为随机号码(非由运单号衍生),不能用于差缺推导。
对比逻辑:
- 在实到数据中按「运单号」字段查找,统计出现次数
- 出现次数 < 交接件数 → 差缺
- 将找到的子单号(虽随机但可以列出来)填入「已到单号」列
SF订单的差缺判定:只基于交接件数与实到运单号出现次数的比较,不依赖子单号的结构解析。
代码实现
domain.py:57-65:
{
"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(入库过滤):
if site == "韵达":
# 韵达业务清洗:抛弃「交接单号」为空的行(派件/签收等其他扫描无交接单号),
# 再按子单号去重(一件多扫只留一条;清洗后子单号已天然唯一,drop 为保险)。
df = df[df["交接单号"].astype(str).str.strip() != ""] # ← 保留非空
df = df.drop_duplicates(subset=[cm["piece"]], keep="last")
domain.py:67-75(比对配置):
{
"name": "韵达",
"exp_wb": "运单号",
"arrived_pieces": arrived_pieces_by_cols("主单号", "子单号"),
}
对齐情况:❌ 交接单号过滤逻辑完全相反
| 检查项 | 代码现状 | 业务要求 |
|---|---|---|
| 交接单号过滤 | 保留 != ""(非空) |
保留 == ""(空) |
| 子单号结构 | ✅ 7133266030003 = wb713326603 + seq0003 |
一致 |
| 实到解析 | ✅ arrived_pieces_by_cols("主单号", "子单号") |
一致 |
| compare.py 过滤 | ❌ 无过滤,所有行参与比对 | 需要过滤 |
影响分析:
store.py过滤反了——入库时留下了错误的数据集compare.py完全没有交接单号过滤——如果原始 Excel 中同时存在空和非空行,比对阶段会全部读入导致重复计数- 当前数据库中韵达 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 |