# 四站点差缺对比逻辑审查报告 > 审查日期: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 |