Files
rakuten-api/app/trading/models.py
T
q792602257andClaude Opus 4.6 55c01ae4f5 加 /api/cart/* 接口;加购链路完全归 trading
trading 新增 4 条购物车接口(POST /api/cart/{add,status,clear,remove}),
全部 Bearer 鉴权、走 SiteInteractor(Playwright + storage_state)。同步把
SiteInteractor 从 gateway URL 解耦——lifespan 总是构造与启停,container
字段 worker_site → site,加 asyncio.Lock 让 HTTP 与 worker 共用同一把锁
(同账号串行硬约束)。clear/remove 用 UI 点击 button[aria-label="削除"],
探针回报这是稳定 selector;真账号实测前先用此路径。

抽 ichiba 加购字段解析到 app/shared/purchase_contract.py(常量 +
inventory_flag_for + basket_domain_of + base_form_fields),原本 scraping
与 trading 重复实现同一段 __INITIAL_STATE__.purchase 解析。进一步发现
README 写的「purchase 块是两服务契约」实际未落地——trading 必须 Playwright
开页(httpx 被 TLS 指纹拦死),本地抽比再调 /api/item_detail 更快更新鲜。
删除 scraping 端 PurchaseInfo/PurchaseOption/PurchaseOptionValue 模型、
各站 _purchase_info 函数、tests/test_purchase.py。ItemDetailData 保留
purchase_condition / is_sold_out / purchase_unit / sku 等商品状态字段。
README「加购与下单」段重写。

328 测试全绿(含架构测试守住三方互不 import)。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-28 17:18:00 +08:00

133 lines
3.6 KiB
Python

"""交易侧 API 数据模型:登录态、(后续的)加购、下单与订单监控
与抓取侧模型(app/scraping/models/scrape.py)分开的理由和服务本身拆开的理由
一致:抓取模型描述的是「站点上有什么」,这里描述的是「我们对某个账号做了什么、
现在处于哪一步」——后者有生命周期、有状态迁移,会持久化,不是一次请求的产物。
响应信封 `ApiResponse` 与抓取侧共用,在 app/shared/api.py。
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, Field
class AuthSite(StrEnum):
"""支持登录的站点
交易服务只管理乐天市场的购物车购买、支付与订单监控;ラクマ 不进入交易链路。
"""
RAKUTEN = "rakuten"
class AuthStatusRequest(BaseModel):
"""登录态查询请求"""
site: AuthSite | None = None # 不传则返回所有已配置站点
refresh: bool = True # 是否真实打一次请求探测;false 时只读缓存
class AuthSiteStatus(BaseModel):
"""单站登录态"""
site: str
state_file_exists: bool # 是否已跑过 scripts/login.py
logged_in: bool | None # None 表示尚未探测
checked_age_seconds: float | None = None
detail: str = ""
class AuthStatusData(BaseModel):
"""登录态查询响应"""
sites: list[AuthSiteStatus] = Field(default_factory=list)
class AuthReloadRequest(BaseModel):
"""重新加载登录态请求(人工登录完成后调用,免重启服务)"""
site: AuthSite | None = None # 不传则重载所有已配置站点
class AuthReloadData(BaseModel):
"""重新加载登录态响应"""
reloaded: dict[str, int] = Field(default_factory=dict) # site -> cookie 条数
sites: list[AuthSiteStatus] = Field(default_factory=list)
class TradingHealthData(BaseModel):
"""交易服务健康检查响应数据
只读缓存的登录态,不触发网络探测——健康检查会被高频轮询,实时结果请用
POST /api/auth/status。
"""
status: str
auth: dict[str, Any] = Field(default_factory=dict)
# ---- 购物车接口(/api/cart/*)----
class CartAddRequest(BaseModel):
"""加购请求
item_url 必填;variant_id / choice 不传时由 SiteInteractor 按商品页
__INITIAL_STATE__.purchase 自动选(多规格选第一个非售罄,必填选项选第一个候选值)。
choice 接受字符串("颜色:赤")或字符串列表(["颜色:赤", "サイズ:M"])。
"""
item_url: str
quantity: int = 1
variant_id: str | None = None
choice: str | list[str] | None = None
class CartStatusRequest(BaseModel):
"""购物车状态查询请求(无入参,保留结构对称)"""
class CartClearRequest(BaseModel):
"""清空购物车请求(无入参)"""
class CartRemoveRequest(BaseModel):
"""删除指定商品请求"""
item_id: str
class CartAddData(BaseModel):
"""加购响应数据"""
added: bool = True
item_id: str
shop_bid: str
cart_count: int # -1 表示加购成功但末尾 count 查询失败
class CartStatusData(BaseModel):
"""购物车状态响应数据"""
logged_in: bool
count: int
raw_status: str # 站点状态码字符串,"100" 表示正常
class CartClearData(BaseModel):
"""清空购物车响应数据"""
removed_count: int # 实际点击「削除」按钮的次数
cart_count: int # -1 表示末尾 count 查询失败
class CartRemoveData(BaseModel):
"""删除指定商品响应数据"""
removed: bool # 末尾 cart HTML 已不含 item_id 时为 true
item_id: str