152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
"""恢复核对: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, Any
|
|
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('/')}"
|
|
|
|
|
|
def _intent_item_urls(intent: dict[str, Any]) -> list[str]:
|
|
"""读取新旧意图中的商品 URL,供恢复核对使用。"""
|
|
raw_items = intent.get("items")
|
|
if raw_items is None:
|
|
raw_items = [intent]
|
|
if not isinstance(raw_items, list):
|
|
return []
|
|
urls: list[str] = []
|
|
for item in raw_items:
|
|
if isinstance(item, str):
|
|
url = item
|
|
elif isinstance(item, dict):
|
|
url = item.get("item_url")
|
|
else:
|
|
url = None
|
|
normalized = _normalize_item_url(url)
|
|
if normalized:
|
|
urls.append(normalized)
|
|
return urls
|
|
|
|
|
|
async def verify_on_site(
|
|
task: LeaseTask, *, gateway: "GatewayClient", site: "SiteInteractor"
|
|
) -> VerifyResult:
|
|
"""核对一笔任务是否已在站点上下过单
|
|
|
|
核对链路:intent.items(或兼容的 intent.item_url)→ 查任务创建时间
|
|
(GET /api/orders/{task_id},LeaseTask 本身不带 created_at)→ 拉「创建时间之后」
|
|
的订单列表 → 按商品 URL 比对。任何一环拿不到足够信息都返回 UNKNOWN,不猜——尤其是 NOT_ORDERED,
|
|
只有在确认翻完了窗口内的全部订单后才允许返回,否则「没找到」可能只是没翻
|
|
到那一页。
|
|
"""
|
|
intent = task.intent or {}
|
|
targets = set(_intent_item_urls(intent))
|
|
if not targets:
|
|
return VerifyResult(
|
|
VerifyVerdict.UNKNOWN, detail="intent 缺商品 URL(item_url/items),无法比对商品"
|
|
)
|
|
|
|
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 = []
|
|
for entry in window.entries:
|
|
entry_urls = {
|
|
normalized
|
|
for normalized in (_normalize_item_url(it.item_url) for it in entry.items)
|
|
if normalized
|
|
}
|
|
# 多商品任务必须在同一笔订单中全部命中,避免部分匹配误判为已下单。
|
|
if targets.issubset(entry_urls):
|
|
matches.append(entry)
|
|
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="订单列表未能确认翻完任务创建时间之后的全部订单",
|
|
)
|