feat(gateway): 定时下派通道——周期性盘点账号订单并回写网关编目

新增 §12 定时下派通道:网关自己定期派 order_list / order_detail 查询,把账号
真实订单沉淀进新表 account_orders(回写网关),上游可直接查 GET /api/account/orders
拿到账号里实际有哪些订单,不必自己记 order_number。

- collector.py: OrderDiscoveryCollector 常驻后台任务(与 sweep 并列)。每隔
  account_discovery_interval_seconds 派 order_list,收割结果后把「编目里没有的
  订单」upsert 进编目,再逐笔派 order_detail 沉淀 delivery_status/order_state。
  状态无痕:不新增编排跟踪表,仅内存 _pending + _detail_dispatched_this_day;
  detail query_id 按天分段(discover-detail-<order>-<日期>),同天幂等防重复派。
  边界:collector 只消费 worker 结果的规范化字段,绝不解析 raw/raw_pages。
- db.py: account_orders 表 + AccountOrderRow + upsert/single/list/stale 访问;
  upsert 以 order_number 为主键,重复采集只刷新,列表重扫不抹详情节点。
- 路由: GET /api/account/orders(列编目)、GET /api/account/orders/{n}(6005)、
  POST /api/account/discovery/trigger(手动立即派一轮 list)。
- config: RAKUTEN_ACCOUNT_DISCOVERY_ENABLED / _INTERVAL_SECONDS / _MAX_PAGES /
  RAKUTEN_ACCOUNT_DETAIL_REFRESH_SECONDS。
- 既有网关测试夹具统一关 discovery(会启动即派扫描污染查询队列语义),
  新表测试在 tests/test_gateway_discovery.py(13 用例,真实 QueryQueue+DB 装配)。

472 tests passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-16 23:35:51 +08:00
co-authored by Claude Opus 5
parent 6b77f3bcc3
commit f6c3976c0a
14 changed files with 1045 additions and 2 deletions
+66
View File
@@ -503,3 +503,69 @@ worker 回结果撞上 6006 属于正常情况(上一轮超时、单子已重
- [x] 执行超时按失败回报,不把查询单挂到租约过期
- [x] worker 任何异常都不逃出主循环,每张单都有一次回报
## 12. 定时下派通道:周期性发现账号订单并回写网关(2026-08-16)
### 12.1 解决什么
§11 的查询通道要拿订单详情,得**上游自己已经知道 order_number 再下派**。可账号在
站点上可能走了别的渠道下单、被商家改状态,网关的状态镜像根本看不到。本通道让
网关**自己定期盘点账号**:派一张 `order_list` 查询(走 §11 那条队列,本地 worker
出站真读),把「网关编目里还没有的订单」**回写进新的 `account_orders` 表**,再逐笔
`order_detail` 查询把配送阶段沉淀进去。之后上游直接查 `GET /api/account/orders`
就能拿到账号里真实有哪些订单,不必自己记单号。
一句话:**gateway 从「等上游告诉它账号里有什么」变成「自己定时去账号里看,并把
新订单写回自己」**。
### 12.2 实现与边界(最重要的两条)
**1. collector 只消费「规范化字段」,绝不解析站点原始 JSON。**「网关不解释
query 的 params/result」这条原则照旧适用于上游自提的查询单;collector 自主派发的
这批 `discover-*` 单是**网关自己的编排数据**,它们的结果里 worker 已经把
`orders[].order_number/order_date/shop_id/shop_name``delivery_status/order_state`
抽成了稳定契约字段(§11.3 明说这是「规范化字段是稳定契约」),collector 把它
沉淀进编目不算解释站点内容。代码见 `app/gateway/collector.py::_parse_list_result`
只读这几个键)。
**2. 状态无痕(不新增任何编排跟踪表)。** collector 只在内存记「本进程派出去
但还没处理结果」的 `_pending`;进程重启即清空,重启后下一轮 tick 派一张全新 list
扫描,结果幂等 upsert 进编目,没有重复行。`order_detail` 的 query_id 用
`discover-detail-<order_number>-<日期>` **按天分段**:同一天内重复 submit 命中幂等
返回既有单(不会因每轮 tick 都满足「详情过期」而反复创建),跨天自然生成新单刷新
详情;同一天内已派过详情的订单由 `_detail_dispatched_this_day` 在内存里挡掉,避免
「found=False 订单一直算过期、每轮都重发」。已消费的 `discover-*` 查询单留在
`account_queries`,由既有 sweep 按保留期(默认 7 天)清理。
### 12.3 接口
- `GET /api/account/orders?state=&limit=&offset=` — 列出编目订单,按最近出现倒序。
编目行只含规范化字段(订单号/店铺/日期/配送状态),要看细节仍去拿对应订单的
`order_detail` 查询单。
- `GET /api/account/orders/{order_number}` — 单笔编目订单;编目里没有返回 6005。
- `POST /api/account/discovery/trigger` — 手动立即派一轮 `order_list` 扫描
(不等定时器到点),返回本轮下派的单号与 `created`
collector 本身不回写「已经存在于下单任务表 `tasks` 的订单」——`account_orders`
**账号真实订单的编目**,与 `tasks`(下单意图及其镜像)是两回事,两者通过
`order_number` 天然对账(上游要确认「这单网上下了没」就是查 `tasks`,要「账号里
实际有什么」就是查 `account_orders`)。
### 12.4 配置项
| 配置 | 默认 | 说明 |
| --- | --- | --- |
| `RAKUTEN_ACCOUNT_DISCOVERY_ENABLED` | true | 关闭则 collector 完全不启动 |
| `RAKUTEN_ACCOUNT_DISCOVERY_INTERVAL_SECONDS` | 21600 | 两轮 `order_list` 扫描间隔(6h) |
| `RAKUTEN_ACCOUNT_DISCOVERY_MAX_PAGES` | 3 | list 扫描翻页上限 |
| `RAKUTEN_ACCOUNT_DETAIL_REFRESH_SECONDS` | 86400 | 单笔详情刷新间隔(24h) |
### 12.5 验收清单
- [x] 定时派 `order_list` / `order_detail`,结果落到 `account_orders` 编目
- [x] 采集到编目中不存在的订单 → 回写新行;重复采集只刷新不重复
- [x] 列表扫描不抹掉已取过的详情阶段(配送状态/详情时间保留)
- [x] 同一天内同一订单的详情单只派一张;跨天自然刷新详情
- [x] 手动 trigger 立即派一轮 list,返回的单在查询队列里可见
- [x] `GET /api/account/orders` 列编目;单笔不存在返回 6005
- [x] collector 只读规范化字段,不解析 raw/raw_pages 站点原始 JSON