上游要的不只是网关记的任务状态镜像,还有「已登录账号在站点上的真实订单」,
但账号只在 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>
103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
"""任务与订单状态枚举:网关与本地 worker 共享的词汇表
|
|
|
|
放在 shared 层是因为这是网关与 worker 之间的 HTTP 契约——两侧都需要知道合法
|
|
取值,不能由任何一方私有持有。shared 本身不引入对 scraping/trading 的依赖,
|
|
这个模块也一样:纯枚举与状态集合,不引用任何业务模块。
|
|
|
|
两层状态不要混:task.status 描述「这个任务被谁领了、做完没有」,order state
|
|
描述「这笔订单在站点上走到哪一步」。前者权威方是网关,后者权威方是本地 trading,
|
|
网关只存镜像。详见 docs/order-gateway.md §3。
|
|
|
|
迁移图(task.status):
|
|
|
|
queued ──lease──> leased ──首次 report──> running ──terminal report──> succeeded
|
|
▲ │ │ └─> failed
|
|
│ └──── 租约过期 ──────────┴──> stale └─> needs_human
|
|
└── 只有人工介入才能从 stale 回到 leased(reclaim)
|
|
|
|
注意 stale **不**自动回 queued——下单不可逆,自动重投等于再买一次。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from enum import StrEnum
|
|
|
|
|
|
class TaskStatus(StrEnum):
|
|
"""任务状态(网关权威)"""
|
|
|
|
QUEUED = "queued" # 已入队,等待 worker 领取
|
|
LEASED = "leased" # 已被 worker 领走,尚未首次 report
|
|
RUNNING = "running" # worker 已首次 report,正在进行
|
|
SUCCEEDED = "succeeded" # 终态:成功
|
|
FAILED = "failed" # 终态:worker 明确失败
|
|
NEEDS_HUMAN = "needs_human" # 终态:需要人介入(如 3DS、核对不出结论)
|
|
STALE = "stale" # 租约过期,等人工 reclaim,不自动重投
|
|
|
|
|
|
# 终态集合:到达后任何 renew / report / reclaim 都报 6003
|
|
TERMINAL_STATUSES: frozenset[TaskStatus] = frozenset(
|
|
{TaskStatus.SUCCEEDED, TaskStatus.FAILED, TaskStatus.NEEDS_HUMAN}
|
|
)
|
|
|
|
# 处于「执行中」的状态:这些状态存在时,lease 一律返回空(全局并发度 1)
|
|
ACTIVE_STATUSES: frozenset[TaskStatus] = frozenset({TaskStatus.LEASED, TaskStatus.RUNNING})
|
|
|
|
# 可以被普通 lease 领走的状态:只有 queued
|
|
LEASABLE_STATUSES: frozenset[TaskStatus] = frozenset({TaskStatus.QUEUED})
|
|
|
|
# 可以被 reclaim 领走的状态:只有 stale
|
|
RECLAIMABLE_STATUSES: frozenset[TaskStatus] = frozenset({TaskStatus.STALE})
|
|
|
|
|
|
class QueryStatus(StrEnum):
|
|
"""账号只读查询单状态(网关权威,见 docs/order-gateway.md §11)
|
|
|
|
与 TaskStatus 刻意分成两套词汇表,因为安全约束正好相反:
|
|
|
|
- 下单是不可逆写操作 → 租约过期只能置 stale 等人工 reclaim,绝不自动重投
|
|
- 查询是只读操作 → 租约过期直接回 queued 自动重投,重复执行没有副作用
|
|
|
|
没有 running:查询是「领走 → 一次性回结果」,中间没有需要单独表达的进行态,
|
|
也不需要续租(超时就重投)。
|
|
"""
|
|
|
|
QUEUED = "queued" # 已入队,等待 worker 领取
|
|
LEASED = "leased" # 已被 worker 领走,等待回结果
|
|
SUCCEEDED = "succeeded" # 终态:拿到结果
|
|
FAILED = "failed" # 终态:worker 明确失败,或重投次数用尽
|
|
EXPIRED = "expired" # 终态:超过查询单 TTL 仍未完成(多半是 worker 不在线)
|
|
|
|
|
|
# 查询单终态集合:到达后 result 一律报 6006,sweep 也不再动它
|
|
QUERY_TERMINAL_STATUSES: frozenset[QueryStatus] = frozenset(
|
|
{QueryStatus.SUCCEEDED, QueryStatus.FAILED, QueryStatus.EXPIRED}
|
|
)
|
|
|
|
|
|
class AccountQueryKind(StrEnum):
|
|
"""账号只读查询的种类
|
|
|
|
放在 shared 是因为它是网关与 worker 之间的 HTTP 契约的一部分:网关不解释
|
|
`params` 的内容(与 intent 同样原样透传),但**校验 kind 合法**——不然上游
|
|
拼错一个字符,要等 worker 领走、执行、回报失败才知道,比当场 422 差得多。
|
|
"""
|
|
|
|
ORDER_LIST = "order_list" # 订单列表(可带时间窗与翻页上限)
|
|
ORDER_DETAIL = "order_detail" # 单笔订单详情(按注文番号)
|
|
|
|
|
|
class OrderState(StrEnum):
|
|
"""订单状态(本地权威,网关只存镜像)
|
|
|
|
沿用 docs/order-gateway.md §3 给的取值。网关本身不解释这些值,只是字符串透传。
|
|
"""
|
|
|
|
CREATED = "created"
|
|
IN_CART = "in_cart"
|
|
ORDERED = "ordered"
|
|
AWAITING_PAYMENT = "awaiting_payment"
|
|
PAID = "paid"
|
|
SHIPPED = "shipped"
|
|
DELIVERED = "delivered"
|
|
CANCELLED = "cancelled"
|