"""交易侧 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 AuthLoginRequest(BaseModel): """触发自动登录请求""" site: AuthSite | None = None # 不传则对所有已配置站点各跑一次 class AuthLoginData(BaseModel): """触发自动登录响应 logged_in 是本次动作的最终结论(所有涉及站点都登录着才为 true); relogin_attempted 标出哪些站点真的跑了登录流程——已登录的站点直接跳过, 不会白起一次浏览器。 """ logged_in: bool relogin_attempted: dict[str, bool] = Field(default_factory=dict) 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