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
+13
View File
@@ -174,6 +174,19 @@ class Settings(BaseSettings):
# 规范化字段并标记 raw_omitted——避免把几 MB 的页面状态塞进网关 SQLite。
query_result_max_bytes: int = 1024 * 1024
# ---- 定时下派通道(仅网关进程 app.gateway.main 使用,见 docs/order-gateway.md §12)----
# 网关周期性派 order_list / order_detail 查询、把账号真实订单沉淀进编目
# account_orders 表(回写网关)。关闭则 collector 完全不启动。
account_discovery_enabled: bool = True
# 两轮 order_list 扫描的间隔(秒)。间隔也是「该订单详情何时到期需刷新」的
# 参照之一,默认 6 小时一次列表、24 小时刷新一次单笔详情,符合「周期性盘点
# 账号」的节奏,也不至于频繁戳订单页触发风控。
account_discovery_interval_seconds: int = 6 * 3600
account_discovery_max_pages: int = 3
# 已编目订单的详情刷新间隔(秒)。detail_fetched_at 距今超过这个值就再派一张
# order_detail 查询更新配送阶段。
account_detail_refresh_seconds: int = 24 * 3600
# ---- 本地下单 worker(仅交易服务内的 worker 子模块使用)----
# 网关 URL。**留空则不启动 worker**,交易服务只跑登录态接口。
# 部署形态:本地机(NAT 后无公网入口)通过出站长轮询领任务,详见
+18
View File
@@ -277,3 +277,21 @@ class QueryLeaseInvalidError(AppError):
retryable=True,
status_code=409,
)
class CatalogOrderNotFoundError(AppError):
"""编目订单不存在:定时下派通道的 account_orders 里没有这笔订单
沿用 6005(与查询单同号的「不存在」语义),区别只在对象是编目里的订单而非
查询单,便于上游按同一张错误码表分支。
"""
def __init__(self, order_number: str):
super().__init__(
message=f"编目订单不存在:{order_number}",
code="CATALOG_ORDER_NOT_FOUND",
err_code=6005,
retryable=False,
status_code=404,
)
self.order_number = order_number