# 应到数据「提交导出任务前」去重 — 实现总结 > 日期:2026-07-29 > 范围:顺心 / 中通 / 韵达 / 安能 4 站**应到(expected)**数据;百世与实到不在本次范围。 ## 一、运行机制 在周期 / 手动触发下载时,于**提交导出任务之前**按交接单号判断该批应到数据是否已落库,已落库则跳过,从源头消除重复下载与重复落库。 ```mermaid flowchart TD TRIG[周期调度 / 手动触发
task_spec: site, kind, force] --> DISP[dispatch_task → 站点 download_impl] DISP --> LOAD{force 强制重下?} LOAD -- 是 --> EMPTY[existing = 空集] LOAD -- 否 --> QRY[查 PG expected_record.handover_no] QRY -- cpolar 失败 --> EMPTY QRY -- 成功 --> SET[existing = 已落库交接单号集合] EMPTY --> LOOP[遍历本次查询到的班次/交接单号] SET --> LOOP LOOP --> JUDGE{交接单号 ∈ existing?} JUDGE -- 是 → 已落库 --> SKIP[⏭️ 跳过:不提交导出
不 append export_times] JUDGE -- 否 → 新单 --> EXP[提交导出任务 → 轮询下载 → 入库 UPSERT] SKIP --> DONE{全部处理完} EXP --> DONE DONE --> FINAL{本次提交了新任务?} FINAL -- 无 → 全跳过 --> BAIL[空兜底 return:不进下载轮询] FINAL -- 有 --> POLL[轮询导出任务管理页 → 下载 → 入库] ``` **核心要点:** - **去重数据源**:PostgreSQL `expected_record.handover_no`(已落库的权威记录),新增 `store.get_existing_handover_nos(site)` 查询。 - **判断时机**:提交导出任务**之前**(循环内逐单判断),而非下载之后。 - **安全降级**:PG 不可用 / `force=true` → `existing=空集` → 当作未落库 → 继续提交(**宁可重复、绝不漏**,UPSERT 兜底)。 - **空兜底**:全部跳过时 `export_times` 为空 → 直接 `return`,不进下载轮询(避免下载数校验失败 / 空转超时)。 - **force 开关**:前端 checkbox(默认关)→ `POST /tasks.force` → 一路透传到 impl;周期调度恒不 force。 ## 二、force 强制重下透传链路 ```mermaid flowchart LR UI[前端 checkbox
forceRedownload] --> POST["POST /api/tasks
{site,kind,force}"] POST --> BFF[Next BFF 透传] BFF --> TS["task_spec
{site,kind,force}"] TS --> DISP[dispatch_task] DISP --> HDR["handler(ctx, force)"] HDR --> DL["download(pg, force)"] DL --> IMPL["impl(pg, force)"] IMPL --> DEC{force?} DEC -- 是 --> EMPTY2["existing = 空集
强制重下,跳过去重"] DEC -- 否 --> LOAD2[查 PG 加载 existing] ``` > 周期调度(`_enqueue_fetch`)投递任务时不带 `force` → 默认不强制。 ## 三、4 站点标识获取 | 站点 | 提交前标识 | 来源 | | --- | --- | --- | | 中通 / 韵达 / 安能 | 交接单号(原有代码已读取) | DOM 列 / CDP 复选框 | | 顺心 | 交接单号 `RTS\d{3}WJ\d+` | 点"运单列表"后从界面读取(方式1) | > 顺心"班次号"业务上等同交接单号;4 站统一用交接单号(= DB `handover_no`)作去重键。 ## 四、改动概览 **后端 InboundVerify:** | 文件 | 改动 | | --- | --- | | `schema.sql` | +`idx_expected_handover` 索引 | | `inbound_verify/store.py` | +`get_existing_handover_nos(site)`(含 cpolar 降级) | | `inbound_verify/cli/server.py` | `TaskRequest.force` + 透传到 task_spec | | `inbound_verify/runtime.py` | `dispatch_task` + 所有 handler 透传 `force` 到 `download(impl)` | | `inbound_verify/sites/{zto,yunda,anneng,shunxin}.py` | `impl` 加 `force` + 提交导出前注入去重 + 空兜底 | | `inbound_verify/sites/baishi.py` | `force` 形参兼容 | **前端 dashboard:** | 文件 | 改动 | | --- | --- | | `app/page.tsx` | `forceRedownload` state + checkbox + `trigger`/`triggerPrimary`/`onTrigger` 透传 force | > BFF `app/api/tasks/route.ts` 是 generic 透传,无需改动。 ## 五、验证结论 4 站去重 + force 开关均实测通过: | 站点 | 二次触发行为 | 结果 | | --- | --- | --- | | 中通 | `...801` 已入库 → 跳过 → 空兜底 return | 20s(vs 首次 59s) ✅ | | 顺心 | 双账号 4 班次全跳过(`RTS023WJ375478` 等) | ✅ | | 韵达 | `...82001` 已入库 → 跳过 → 原有空兜底 | ✅ | | 安能 | `4008242619171180544` 已入库 → 跳过 | **5s(vs 4 分钟)** ✅ | | force | `[去重] 强制重下,跳过去重` + 已入库的重新导出 | ✅ | 实施过程中的两个问题均已解决: 1. **顺心 RTS 正则**:`RTS\d+` 遇字母 W 停(只抓 `RTS023`)→ 改 `RTS[A-Z0-9]+` 抓完整 `RTS023WJ375320`。 2. **韵达 force 偶发失败**:韵达站点自身 UI 不稳定(`section iframe` 匹配到 2 个 + 弹窗遮挡),与去重/force 无关;换安能验证 force 成功。 静态检查:Black(py310)+ compileall + tsc 全绿。