"""恢复核对:lease_count > 1 时,worker 必须先查站点订单列表确认是否已下单 规格 §5:租约过期 → stale → reclaim 后 worker 领回的任务 lease_count 必然 > 1。 此时 worker 不能盲目重新提交(可能上次已经下单成功,只是回报断网),必须先查 站点订单列表比对。 2026-08-13 实现:核对逻辑用真实账号 + 真实订单验证过订单列表的数据结构(见 app/trading/worker/site_interact.py::list_recent_orders 与 _parse_order_list 的 模块文档),但当时账号只有 1 笔订单,ALREADY_ORDERED 分支有真实数据支撑, NOT_ORDERED / 多笔命中 / 多页翻页这几个分支目前只有逻辑,没有被真实多单数据 跑过——**绝不**在没有把握时猜:任何一步信息不足都转 UNKNOWN,宁可卡住等人看 一眼,也不赌一次重复下单。 """ from __future__ import annotations from dataclasses import dataclass from enum import StrEnum from typing import TYPE_CHECKING from urllib.parse import urlsplit from app.shared.errors import AppError from app.trading.worker.models import LeaseTask from app.trading.worker.site_interact import parse_order_datetime if TYPE_CHECKING: from app.trading.worker.client import GatewayClient from app.trading.worker.site_interact import SiteInteractor class VerifyVerdict(StrEnum): """核对结论""" ALREADY_ORDERED = "already_ordered" # 确认已下单,补报状态即可 NOT_ORDERED = "not_ordered" # 确认未下单,可重新执行 UNKNOWN = "unknown" # 核对不出结论,交人工 @dataclass(slots=True) class VerifyResult: verdict: VerifyVerdict site_order_id: str | None = None detail: str = "" def _normalize_item_url(url: str | None) -> str | None: """去掉查询串与末尾斜杠,只留 scheme+host+path 用于比对 订单列表返回的 itemUrl 实测带 `?variantId=...`(见 _parse_order_list 的真实 样例),intent.item_url 通常不带——按查询串比对会产生假阴性,所以只比 路径本身。 """ if not url: return None parts = urlsplit(url) return f"{parts.scheme}://{parts.netloc}{parts.path.rstrip('/')}" async def verify_on_site( task: LeaseTask, *, gateway: "GatewayClient", site: "SiteInteractor" ) -> VerifyResult: """核对一笔任务是否已在站点上下过单 核对链路:intent.item_url → 查任务创建时间(GET /api/orders/{task_id}, LeaseTask 本身不带 created_at)→ 拉「创建时间之后」的订单列表 → 按商品 URL 比对。任何一环拿不到足够信息都返回 UNKNOWN,不猜——尤其是 NOT_ORDERED, 只有在确认翻完了窗口内的全部订单后才允许返回,否则「没找到」可能只是没翻 到那一页。 """ intent = task.intent or {} item_url = intent.get("item_url") if not item_url: return VerifyResult( VerifyVerdict.UNKNOWN, detail="intent 缺 item_url,无法比对商品" ) target = _normalize_item_url(item_url) try: task_detail = await gateway.get_task(task.task_id) except AppError as exc: return VerifyResult( VerifyVerdict.UNKNOWN, detail=f"查询任务创建时间失败:{exc.message}" ) created_at_raw = task_detail.get("created_at") created_at = parse_order_datetime(created_at_raw) if created_at_raw else None if created_at is None: return VerifyResult( VerifyVerdict.UNKNOWN, detail=f"任务创建时间不可解析:{created_at_raw!r}" ) try: window = await site.list_recent_orders(since=created_at) except Exception as exc: # noqa: BLE001 — 站点交互失败一律转 unknown,不重试 return VerifyResult( VerifyVerdict.UNKNOWN, detail=f"订单列表查询失败:{type(exc).__name__}: {exc}", ) matches = [ entry for entry in window.entries if any(_normalize_item_url(it.item_url) == target for it in entry.items) ] if len(matches) == 1: return VerifyResult( VerifyVerdict.ALREADY_ORDERED, site_order_id=matches[0].order_number, detail=f"订单列表命中 1 笔匹配商品的订单({matches[0].order_number})", ) if len(matches) > 1: return VerifyResult( VerifyVerdict.UNKNOWN, detail=( f"订单列表命中 {len(matches)} 笔匹配商品的订单," f"无法唯一确定:{[m.order_number for m in matches]}" ), ) if window.window_fully_covered: return VerifyResult( VerifyVerdict.NOT_ORDERED, detail="已核对任务创建时间之后的全部订单,未找到匹配商品", ) return VerifyResult( VerifyVerdict.UNKNOWN, detail="订单列表未能确认翻完任务创建时间之后的全部订单", )