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:
@@ -20,7 +20,7 @@ from app.shared.config import Settings
|
||||
from app.shared.errors import AppError
|
||||
from app.shared.proxy import httpx_client_options
|
||||
from app.shared.task_state import OrderState, TaskStatus
|
||||
from app.trading.worker.models import LeaseTask
|
||||
from app.trading.worker.models import LeaseTask, QueryTask
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -158,3 +158,55 @@ class GatewayClient:
|
||||
lease_count=data.get("lease_count", 1),
|
||||
known_state=data.get("known_state"),
|
||||
)
|
||||
|
||||
# ---- 账号只读查询通道(docs/order-gateway.md §11)----
|
||||
|
||||
async def lease_query(self, worker_id: str, *, wait: int = 30) -> QueryTask | None:
|
||||
"""长轮询领取一张只读查询单。无单可领时返回 None
|
||||
|
||||
与 lease() 走的是两条互不干扰的通道:下单任务在跑的时候,查询照样能领到
|
||||
(账号级串行由 SiteInteractor 那把锁保证,不靠网关排队)。
|
||||
"""
|
||||
body = await self._request(
|
||||
"GET",
|
||||
"/api/account/queries/lease",
|
||||
params={"worker_id": worker_id, "wait": wait},
|
||||
)
|
||||
data = body.get("data")
|
||||
if not data:
|
||||
return None
|
||||
return QueryTask(
|
||||
query_id=data["query_id"],
|
||||
site=data["site"],
|
||||
kind=data["kind"],
|
||||
params=data.get("params") or {},
|
||||
lease_expires_at=data.get("lease_expires_at", ""),
|
||||
attempt=data.get("attempt", 1),
|
||||
)
|
||||
|
||||
async def report_query_result(
|
||||
self,
|
||||
query_id: str,
|
||||
worker_id: str,
|
||||
*,
|
||||
success: bool,
|
||||
result: dict[str, Any] | None = None,
|
||||
error_code: int | None = None,
|
||||
error_message: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""回报查询结果,返回网关响应里的 data 字段
|
||||
|
||||
网关可能回 6006(租约已被重投给下一轮)——那说明本次结果已经作废,
|
||||
调用方按「丢弃」处理即可,不要重发。
|
||||
"""
|
||||
payload: dict[str, Any] = {
|
||||
"worker_id": worker_id,
|
||||
"success": success,
|
||||
"result": result,
|
||||
"error_code": error_code,
|
||||
"error_message": error_message,
|
||||
}
|
||||
body = await self._request(
|
||||
"POST", f"/api/account/queries/{query_id}/result", json=payload
|
||||
)
|
||||
return body["data"]
|
||||
|
||||
Reference in New Issue
Block a user