Files
InboundVerify/docs/2026-07-29-应到提交导出去重-实现总结.md
Misaka_Company f710622a3d feat: dedup expected data by handover_no before export submit
- store.get_existing_handover_nos: query PG expected_record.handover_no

- inject dedup skip before submitting export in zto/yunda/anneng/shunxin

- shunxin reads RTS handover_no from waybill-list view (method 1)

- force-redownload switch threaded via task_spec -> dispatch -> impl

- schema: add idx_expected_handover index

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 10:57:07 +08:00

103 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 应到数据「提交导出任务前」去重 — 实现总结
> 日期2026-07-29
> 范围:顺心 / 中通 / 韵达 / 安能 4 站**应到expected**数据;百世与实到不在本次范围。
## 一、运行机制
在周期 / 手动触发下载时,于**提交导出任务之前**按交接单号判断该批应到数据是否已落库,已落库则跳过,从源头消除重复下载与重复落库。
```mermaid
flowchart TD
TRIG[周期调度 / 手动触发<br/>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[⏭️ 跳过:不提交导出<br/>不 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<br/>forceRedownload] --> POST["POST /api/tasks<br/>{site,kind,force}"]
POST --> BFF[Next BFF 透传]
BFF --> TS["task_spec<br/>{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 = 空集<br/>强制重下,跳过去重"]
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 | 20svs 首次 59s ✅ |
| 顺心 | 双账号 4 班次全跳过(`RTS023WJ375478` 等) | ✅ |
| 韵达 | `...82001` 已入库 → 跳过 → 原有空兜底 | ✅ |
| 安能 | `4008242619171180544` 已入库 → 跳过 | **5svs 4 分钟)** ✅ |
| force | `[去重] 强制重下,跳过去重` + 已入库的重新导出 | ✅ |
实施过程中的两个问题均已解决:
1. **顺心 RTS 正则**`RTS\d+` 遇字母 W 停(只抓 `RTS023`)→ 改 `RTS[A-Z0-9]+` 抓完整 `RTS023WJ375320`
2. **韵达 force 偶发失败**:韵达站点自身 UI 不稳定(`section iframe` 匹配到 2 个 + 弹窗遮挡),与去重/force 无关;换安能验证 force 成功。
静态检查Blackpy310+ compileall + tsc 全绿。