"""账号只读查询通道路由(docs/order-gateway.md §11) 上游要的是「已登录账号在站点上的真实订单」,而不是网关自己记录的任务状态镜像; 但本地机在 NAT 后没有公网入口,网关推不进去。所以这条通道与下单任务同构: 上游把查询意图放进队列,本地 worker 出站长轮询领走、去站点上真读一次、回结果。 - POST /api/account/queries 上游提交查询单(幂等) - GET /api/account/queries/lease 本地长轮询领取 - POST /api/account/queries/{id}/result 本地回结果 - GET /api/account/queries/{id} 取查询单状态与结果 - GET /api/account/queries 查询单列表(运维排查) **纯异步**:提交只拿 query_id,结果去 GET 取。网关不提供「挂起等结果」的接口—— 下单任务可能占着账号锁跑好几分钟,同步等待会把上游的连接一起卡住。 注意路由声明顺序:`/lease` 必须在 `/{query_id}` 之前,否则 lease 会被当成 query_id。 """ from __future__ import annotations from fastapi import APIRouter, Depends, Query from app.gateway.container import GatewayContainer from app.gateway.models import ( QueryDetail, QueryLeaseData, QueryListData, QueryResultData, QueryResultRequest, SubmitQueryData, SubmitQueryRequest, ) from app.shared.api import ApiResponse, get_container, require_bearer_token router = APIRouter(prefix="/api/account/queries", tags=["account-queries"]) @router.post( "", response_model=ApiResponse[SubmitQueryData], dependencies=[Depends(require_bearer_token)], ) async def submit_query( payload: SubmitQueryRequest, container: GatewayContainer = Depends(get_container), ) -> ApiResponse[SubmitQueryData]: """提交一张账号只读查询单 重复提交同一 query_id 不新建,返回既有单且 created=false。查询是只读的, 重复提交本身无害,幂等键的意义在于上游重发时不会拿到两个单号。 """ data = await container.query_queue.submit( query_id=payload.query_id, site=payload.site, kind=payload.kind.value, params=payload.params, ) return ApiResponse[SubmitQueryData](success=True, msg="success", data=data, code=0) @router.get( "/lease", response_model=ApiResponse[QueryLeaseData | None], dependencies=[Depends(require_bearer_token)], ) async def lease_query( worker_id: str = Query(...), wait: int = Query(default=30, ge=0, le=300), site: str | None = Query(default=None), container: GatewayContainer = Depends(get_container), ) -> ApiResponse[QueryLeaseData | None]: """本地长轮询领取查询单 无单可领时挂起到 wait 秒后返回 data: null,HTTP 仍 200。与下单 lease 不同, 这里没有「已有任务在执行就一律返回空」的闸门。 """ data = await container.query_queue.lease( worker_id=worker_id, wait=wait, site=site, max_wait=container.settings.lease_max_wait_seconds, ) return ApiResponse[QueryLeaseData | None](success=True, msg="success", data=data, code=0) @router.post( "/{query_id}/result", response_model=ApiResponse[QueryResultData], dependencies=[Depends(require_bearer_token)], ) async def submit_query_result( query_id: str, payload: QueryResultRequest, container: GatewayContainer = Depends(get_container), ) -> ApiResponse[QueryResultData]: """本地回报查询结果 租约不是自己的(多半是超时后被重投给了下一轮)时报 6006 并丢弃本次结果, 避免迟到的旧结果覆盖新结果。 """ data = await container.query_queue.submit_result( query_id, worker_id=payload.worker_id, success=payload.success, result=payload.result, error_code=payload.error_code, error_message=payload.error_message, ) return ApiResponse[QueryResultData](success=True, msg="success", data=data, code=0) @router.get( "/{query_id}", response_model=ApiResponse[QueryDetail], dependencies=[Depends(require_bearer_token)], ) async def get_query( query_id: str, container: GatewayContainer = Depends(get_container), ) -> ApiResponse[QueryDetail]: """取查询单状态与结果 status=succeeded 时 result 里是 worker 从站点读到的原文 + 规范化字段; failed/expired 时看 error。仍在 queued/leased 就是还没读到,稍后再来。 """ data = await container.query_queue.get_detail(query_id) return ApiResponse[QueryDetail](success=True, msg="success", data=data, code=0) @router.get( "", response_model=ApiResponse[QueryListData], dependencies=[Depends(require_bearer_token)], ) async def list_queries( status: str | None = Query(default=None), kind: str | None = Query(default=None), limit: int = Query(default=50, ge=1, le=500), offset: int = Query(default=0, ge=0), container: GatewayContainer = Depends(get_container), ) -> ApiResponse[QueryListData]: """查询单列表,按创建时间倒序(运维排查用)""" data = await container.query_queue.list_queries( status=status, kind=kind, limit=limit, offset=offset ) return ApiResponse[QueryListData](success=True, msg="success", data=data, code=0)