diff --git a/docs/2026-07-29-指定日期下载接口-design.md b/docs/2026-07-29-指定日期下载接口-design.md new file mode 100644 index 0000000..80963d0 --- /dev/null +++ b/docs/2026-07-29-指定日期下载接口-design.md @@ -0,0 +1,131 @@ +# 指定日期下载接口(开发者)— 设计文档 + +> 日期:2026-07-29 +> 定位:面向开发者的 HTTP 接口,**不进前端**。提供"指定一个具体日期,下载该日应到 / 实到数据"的能力,用于补下历史数据。 +> 前置:中通跨月导航已实现并验证(见 `feat(zto): cross-month calendar navigation`)。 + +## 一、背景与目标 + +现状:下载日期由各站 **offset 偏移**(0=今天,1=昨天…,存 `state.db`,上限 `MAX_DATE_OFFSET=30`)决定,周期调度与手动触发都用 offset。无法指定一个具体日期。 + +目标:新增"指定日期"入口(开发者用),传一个 `YYYY-MM-DD` 日期,下载该日应到 / 实到数据。不替换 offset 机制,与之并存:传 date 用 date,不传走 offset。 + +## 二、范围 + +| 站点 | 支持指定日期 | 说明 | +| --- | --- | --- | +| 顺心 / 中通 / 韵达 / 安能 | ✅ | 应到、实到均支持 | +| 百世 | ❌ | 固定下载当天,传 date 返回 400 | + +## 三、接口契约 + +复用 `POST /tasks`,body 新增可选字段 `date`(与 `force` 并列): + +```json +{ "site": "中通", "kind": "expected", "date": "2026-06-14" } +``` + +- `date: Optional[str] = None`,格式 `YYYY-MM-DD`。 +- **优先级**:传 `date` 则本次用 date;不传则走站点 offset 配置(默认行为完全不变)。 +- `date` 与 `force` 可共存(指定日期 + 强制重下)。 + +### 合法性校验(仅在传了 date 时执行,失败返回 400) + +1. **格式**:`datetime.strptime(date, "%Y-%m-%d")` 解析成功,否则 400。 +2. **范围**:`今天 - 90 天 ≤ date ≤ 今天`。 + - `date > 今天` → 400(未来日期;日历未来格子 `invalid` 物理上点不动,且不应下未来数据)。 + - `date < 今天 - 90 天` → 400(回溯上限 90 天)。 +3. **百世**:site=百世 且传 date → 400(固定当天)。 + +> 合法性校验落在 `POST /tasks`(`server.py` `create_task`),入队前拦截,非法请求不产生任务。 + +## 四、透传链路(与现有 `force` 完全对称) + +``` +POST /tasks {site, kind, force, date} + → task_queue.put((tid, {site, kind, force, date})) + → dispatch_task(ctx, task_spec) # 读 task_spec["date"] + → handler(ctx, force, date) # _web_handler / 安能 lambda / _site_undelivered_handler + → impl(page, force, date) # 各站 download_impl +``` + +- `_web_handler`:`handler(ctx, force=False, date=None)`,透传 `download_func(pg, force, date)`;顺心双账号透传 `(pages, foreground, force, date)`。 +- `_site_undelivered_handler`(未到):连下 expected + actual,**两个子任务共用同一个 date**。 +- 安能 lambda:`(ctx, force=False, date=None) → anneng_xxx_download(force=force, date=date)`。 +- **周期调度** `_enqueue_fetch` 投递的 task_spec 只有 `{site, kind}`(不带 date)→ 恒走 offset,**无需改动**。 + +## 五、各站 impl 改造(核心) + +统一模式:**`target = parse(date) if date else (today − offset)`**。 + +### 中通(zto)—— 复用跨月算法 + +把 date 折算成 effective offset,复用现有 `target_time = today_time − offset*86400000` 与跨月翻页(`_zto_flip_to_target_month`),零额外 UI 逻辑: + +```python +def zto_expected_download_impl(page, force=False, date=None): + ... + offset = state_store.get_offset("中通") + if date: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + offset = (datetime.now().date() - target_date).days + # 后续 today_time / target_time / 跨月翻页 逻辑完全不变 +``` + +`zto_actual_download_impl` 同理(用 `("中通","actual")` offset)。expected / actual 两个 impl 都加 `date=None` 形参,`zto_expected_download` / `zto_actual_download` 公开入口同步加形参并透传。 + +### 韵达 / 顺心 / 安能 —— date 直接当 target + +这三站 offset→日期是 `target = today − timedelta(days=offset)` 后填**字符串**到日期控件(非日历格子),指定日期只需替换 target 来源: + +```python +if date: + target = datetime.strptime(date, "%Y-%m-%d") +else: + target = today - timedelta(days=offset) +``` + +后接的"填起始/截止日期字符串"逻辑完全不变。各站 expected / actual 入口与 impl 都加 `date=None` 形参。 + +- 韵达:`yunda_expected_download(_impl)` / `yunda_actual_download(_impl)`。 +- 顺心:`shunxin_expected_download(_impl)` / `shunxin_actual_download(_impl)`(双账号入口透传 date 到各账号 impl)。 +- 安能:`anneng_expected_download` / `anneng_actual_download`。 + +### 百世(baishi)—— 签名兼容 + +`baishi_download_undelivered_data(page, date=None)` 加 `date=None` 形参(**忽略**),仅为对齐 `_web_handler` 的统一透传签名;百世任务实际不会带 date(server 已拦截)。 + +## 六、业务日期快照 + +`_record_business_date(site, kind, date=None)`:有 date 则业务日期 = date,否则维持现状 `today − offset`。`dispatch_task` 把 `task_spec["date"]` 透传进去,保证状态盘 / 报告显示的"是哪天的数据"准确(不被 offset 算错)。 + +## 七、改动文件清单 + +| 文件 | 改动 | +| --- | --- | +| `inbound_verify/cli/server.py` | `TaskRequest.date` + `create_task` 合法性校验 + task_spec 透传 date | +| `inbound_verify/runtime.py` | `_web_handler` / `_site_undelivered_handler` / 安能 lambda 透传 date;`dispatch_task` 读 date 透传给 handler 与 `_record_business_date`;`_record_business_date` 加 date | +| `inbound_verify/sites/zto.py` | expected/actual 入口+impl 加 `date`;date→effective offset 复用跨月 | +| `inbound_verify/sites/yunda.py` | expected/actual 入口+impl 加 `date`;date→target | +| `inbound_verify/sites/shunxin.py` | 同上(双账号透传 date) | +| `inbound_verify/sites/anneng.py` | expected/actual 加 `date`;date→target | +| `inbound_verify/sites/baishi.py` | 加 `date=None` 形参兼容(忽略) | + +## 八、验证计划 + +1. **接口校验**(curl/python urllib): + - 合法 date(过去某日)→ 202,任务成功。 + - 未来日期 / 超 90 天 / 格式错 → 400。 + - 百世 + date → 400。 + - 不传 date → 走 offset(行为不变)。 +2. **各站实测**(指定一个过去日期触发任务): + - 中通:跨月日期(已知 OK,复用已验证的跨月导航)。 + - 顺心 / 韵达 / 安能:实测其日期控件是否接受任意过去日期字符串;若控件是日历选择器需翻月,则按中通同法扩展(本轮发现则记录、必要时追加改动)。 +3. **业务日期快照**:下载后 `GET /status` 的 `*_business_date` == 指定 date。 +4. 改完跑 Black + `py_compile`。 + +## 九、非目标(YAGNI) + +- 前端 UI(checkbox / 日期选择器)——开发者接口,不进前端。 +- 周期调度指定日期——周期恒走 offset。 +- 批量日期 / 日期范围下载——单次单日。