新增 §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>
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
"""账号订单编目与定时下派触发路由(docs/order-gateway.md §12)
|
|
|
|
collector 周期性把账号真实订单沉淀进 `account_orders` 编目,这里是让上游与运维
|
|
读取编目、以及手动触发一轮扫描的入口:
|
|
|
|
- GET /api/account/orders 列出已发现的账号订单(分页、可按状态过滤)
|
|
- GET /api/account/orders/{order_number} 单笔编目订单
|
|
- POST /api/account/discovery/trigger 立即派一轮 order_list 扫描
|
|
|
|
与 `/api/account/queries` 是两回事:那边是上游主动提交的一次性查询单;这里是
|
|
网关自己周期盘点产生的持久编目。编目行只是规范化字段(订单号/店铺/日期/配送
|
|
状态),不含站点原始 JSON——要看细节去拿对应订单的 order_detail 查询单。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
from app.gateway.container import GatewayContainer
|
|
from app.gateway.models import (
|
|
CatalogOrderListData,
|
|
CatalogedOrder,
|
|
TriggerDiscoveryData,
|
|
)
|
|
from app.shared.api import ApiResponse, get_container, require_bearer_token
|
|
from app.shared.errors import CatalogOrderNotFoundError
|
|
|
|
router = APIRouter(prefix="/api/account", tags=["account-orders"])
|
|
|
|
|
|
@router.get(
|
|
"/orders",
|
|
response_model=ApiResponse[CatalogOrderListData],
|
|
dependencies=[Depends(require_bearer_token)],
|
|
)
|
|
async def list_cataloged_orders(
|
|
state: str | None = Query(default=None, description="按映射后的 OrderState 过滤"),
|
|
limit: int = Query(default=50, ge=1, le=500),
|
|
offset: int = Query(default=0, ge=0),
|
|
container: GatewayContainer = Depends(get_container),
|
|
) -> ApiResponse[CatalogOrderListData]:
|
|
"""列出网关编目里已发现的账号订单,按最近出现时间倒序"""
|
|
rows, total = await container.db.list_account_orders(
|
|
state=state, limit=limit, offset=offset
|
|
)
|
|
items = [_row_to_model(r) for r in rows]
|
|
return ApiResponse[CatalogOrderListData](
|
|
success=True, msg="success",
|
|
data=CatalogOrderListData(items=items, total=total, limit=limit, offset=offset),
|
|
code=0,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/orders/{order_number}",
|
|
response_model=ApiResponse[CatalogedOrder],
|
|
dependencies=[Depends(require_bearer_token)],
|
|
)
|
|
async def get_cataloged_order(
|
|
order_number: str,
|
|
container: GatewayContainer = Depends(get_container),
|
|
) -> ApiResponse[CatalogedOrder]:
|
|
"""取单笔编目订单;编目里没有则 6005(与查询单同号语义)"""
|
|
row = await container.db.get_account_order(order_number)
|
|
if row is None:
|
|
raise CatalogOrderNotFoundError(order_number)
|
|
return ApiResponse[CatalogedOrder](
|
|
success=True, msg="success", data=_row_to_model(row), code=0
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/discovery/trigger",
|
|
response_model=ApiResponse[TriggerDiscoveryData],
|
|
dependencies=[Depends(require_bearer_token)],
|
|
)
|
|
async def trigger_discovery(
|
|
container: GatewayContainer = Depends(get_container),
|
|
) -> ApiResponse[TriggerDiscoveryData]:
|
|
"""手动立即派一轮 order_list 扫描,不等定时器到点
|
|
|
|
返回本轮下派的单号,结果稍后看对应订单的 order_detail 查询单或编目。
|
|
"""
|
|
data = await container.collector.trigger_list_sweep()
|
|
return ApiResponse[TriggerDiscoveryData](
|
|
success=True, msg="success", data=TriggerDiscoveryData(**data), code=0
|
|
)
|
|
|
|
|
|
def _row_to_model(row) -> CatalogedOrder:
|
|
return CatalogedOrder(
|
|
order_number=row.order_number,
|
|
shop_id=row.shop_id,
|
|
shop_name=row.shop_name,
|
|
order_date=row.order_date,
|
|
delivery_status=row.delivery_status,
|
|
order_state=row.order_state,
|
|
discovered_at=row.discovered_at,
|
|
detail_fetched_at=row.detail_fetched_at,
|
|
last_seen_at=row.last_seen_at,
|
|
)
|