feat(gateway): 账号只读查询通道——从已登录账号取真实订单
上游要的不只是网关记的任务状态镜像,还有「已登录账号在站点上的真实订单」,
但账号只在 NAT 后本地机上,只能经网关队列走。新增独立查询通道(§11):
- gateway 单开 account_queries 表 + QueryStatus 状态机,接口
POST /api/account/queries(幂等)/ lease / {id}/result / {id}
- 不复用下单任务队列:查询是只读,租约过期可安全重投(与下单「绝不自动
重投」相反),且不该被全局并发度 1 堵死、task_reports 是订单镜像不能污染
- 本地交易服务起第二条常驻循环 query_runner,领到即调 SiteInteractor 真读:
order_list 复用已实测的 list_recent_orders(规范化字段 + 站点
orderListData 原文),order_detail 复用 fetch_order_detail(配送阶段 +
页面 __INITIAL_STATE__ 原样透传,结构未经真实样本,不抽字段)
- 账号级串行仍由 SiteInteractor 的锁保证;每次执行套超时按失败回报
- 错误码 6005/6006(查询通道,可重试只读区别于 6001-6004);/health 暴露
queued_query_count;结果体积上限先丢原始 JSON
openapi.json 重导,docs/order-gateway.md §11、README、.env.example 补全
配置与实测边界。全量测试 404→454 通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -754,6 +754,109 @@ async def test_list_recent_orders_without_start_raises():
|
||||
await site.list_recent_orders(since=_SINCE)
|
||||
|
||||
|
||||
# ---- 账号只读查询通道用到的两条只读路径(docs/order-gateway.md §11)----
|
||||
#
|
||||
# 查询接口不新写解析,复用的就是上面这两条已实测路径;这里补的是「站点原始
|
||||
# JSON 有没有被完整带出来」和「翻页上限有没有生效」——这两点是查询接口独有的,
|
||||
# 恢复核对那条老路径不关心。
|
||||
|
||||
|
||||
def test_parse_order_list_keeps_raw_order_list_data():
|
||||
"""规范化字段之外,站点 orderListData 原文要原样留着供上游取用"""
|
||||
html = _wrap_state(_real_order_list_state())
|
||||
page = _parse_order_list(html)
|
||||
assert page.raw is not None
|
||||
assert page.raw["ordersFound"] == 1
|
||||
# 规范化模型里没有的字段也在(这正是「原样透传」的意义)
|
||||
assert page.raw["orderList"][0]["shopName"] == "BACKYARD FAMILY インテリアタウン"
|
||||
|
||||
|
||||
def test_parse_order_list_raw_is_none_when_page_type_wrong():
|
||||
"""不是 ph-list(改版 / 掉登录 / act 分支不对):raw 为 None,不给上游半截数据"""
|
||||
html = _wrap_state(_real_order_list_state(page_type="ph-detail"))
|
||||
assert _parse_order_list(html).raw is None
|
||||
|
||||
|
||||
def test_accumulate_collects_raw_pages_in_order():
|
||||
"""多页翻页时,每页的原始 JSON 按顺序累积"""
|
||||
acc = _OrderListAccumulator()
|
||||
page1 = OrderListPage(
|
||||
entries=[_entry("o1", "2026-08-10T00:00:00Z")], orders_found=3, raw={"page": 1}
|
||||
)
|
||||
acc, stop = _accumulate_order_list_page(acc, page1, since=_SINCE)
|
||||
assert stop is False
|
||||
page2 = OrderListPage(
|
||||
entries=[_entry("o2", "2026-07-01T00:00:00Z")], orders_found=3, raw={"page": 2}
|
||||
)
|
||||
acc, stop = _accumulate_order_list_page(acc, page2, since=_SINCE)
|
||||
assert stop is True
|
||||
assert acc.raw_pages == [{"page": 1}, {"page": 2}]
|
||||
|
||||
|
||||
async def test_list_recent_orders_respects_max_pages(tmp_path):
|
||||
"""max_pages 是硬上限:翻到上限就停,且必须如实报 window_fully_covered=False"""
|
||||
# 每页都还有更多订单(orders_found 远大于已取回数),正常会一直翻下去
|
||||
state = _real_order_list_state(orders_found=99)
|
||||
page = _FakeOrderPage([(_ORDER_LIST_LANDED_URL, _wrap_state(state))])
|
||||
site = _build_site(tmp_path, _FakeContext([page]), _FakeAuthSession())
|
||||
|
||||
window = await site.list_recent_orders(since=_SINCE, max_pages=2)
|
||||
|
||||
assert len(page.goto_urls) == 2
|
||||
assert page.goto_urls[1].endswith("?page=2")
|
||||
assert window.window_fully_covered is False
|
||||
assert len(window.raw_pages) == 2
|
||||
|
||||
|
||||
async def test_list_recent_orders_window_carries_raw_pages(tmp_path):
|
||||
html = _wrap_state(_real_order_list_state())
|
||||
page = _FakeOrderPage([(_ORDER_LIST_LANDED_URL, html)])
|
||||
site = _build_site(tmp_path, _FakeContext([page]), _FakeAuthSession())
|
||||
|
||||
window = await site.list_recent_orders(since=_SINCE)
|
||||
|
||||
assert window.window_fully_covered is True
|
||||
assert [raw["ordersFound"] for raw in window.raw_pages] == [1]
|
||||
|
||||
|
||||
async def test_fetch_order_detail_returns_status_and_raw_state(tmp_path):
|
||||
"""详情页:已实测的配送阶段照常解析,页面原始状态原样带出"""
|
||||
html = _wrap_state({"pageType": "ph-detail", "whatever": {"a": 1}}) + _stepper_html(
|
||||
active_stage="出荷"
|
||||
)
|
||||
page = _FakeOrderPage([(_ORDER_DETAIL_LANDED_URL, html)])
|
||||
site = _build_site(tmp_path, _FakeContext([page]), _FakeAuthSession())
|
||||
|
||||
detail = await site.fetch_order_detail(_REAL_ORDER_ID)
|
||||
|
||||
assert detail.status.found is True
|
||||
assert detail.status.order_state == OrderState.SHIPPED
|
||||
assert detail.raw == {"pageType": "ph-detail", "whatever": {"a": 1}}
|
||||
|
||||
|
||||
async def test_fetch_order_detail_raw_is_none_without_inline_state(tmp_path):
|
||||
"""页面没有内联状态时 raw=None——不编造,也不因此把这次读取判成失败"""
|
||||
page = _FakeOrderPage([(_ORDER_DETAIL_LANDED_URL, _stepper_html(active_stage="出荷"))])
|
||||
site = _build_site(tmp_path, _FakeContext([page]), _FakeAuthSession())
|
||||
|
||||
detail = await site.fetch_order_detail(_REAL_ORDER_ID)
|
||||
|
||||
assert detail.raw is None
|
||||
assert detail.status.found is True
|
||||
|
||||
|
||||
async def test_check_order_status_still_returns_only_status(tmp_path):
|
||||
"""付款后监控那条老路径不受影响:仍然拿到 OrderStatusSnapshot"""
|
||||
page = _FakeOrderPage([(_ORDER_DETAIL_LANDED_URL, _stepper_html(active_stage="配達完了"))])
|
||||
site = _build_site(tmp_path, _FakeContext([page]), _FakeAuthSession())
|
||||
|
||||
snapshot = await site.check_order_status(_REAL_ORDER_ID)
|
||||
|
||||
assert isinstance(snapshot, OrderStatusSnapshot)
|
||||
assert snapshot.order_state == OrderState.DELIVERED
|
||||
|
||||
|
||||
|
||||
# ---- submit_order / pay:没有 enter_checkout 留存的确认页会话时应报错,不静默成功 ----
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user