docs(models): 模型字段行尾注释迁移为 Field(description),补全 OpenAPI 字段说明
This commit is contained in:
+48
-30
@@ -380,32 +380,42 @@ class CatalogedOrder(BaseModel):
|
|||||||
已经规范化了的稳定契约字段(见 §12 说明)。
|
已经规范化了的稳定契约字段(见 §12 说明)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
order_number: str
|
order_number: str = Field(description="站点注文番号,编目与下单任务表对账的天然键")
|
||||||
shop_id: str | None = None
|
shop_id: str | None = Field(default=None, description="店铺 ID;列表扫描未给出为 null")
|
||||||
shop_name: str = ""
|
shop_name: str = Field(default="", description="店铺名(站点原文)")
|
||||||
order_date: str | None = None
|
order_date: str | None = Field(default=None, description="下单日期(站点原文);列表扫描未给出为 null")
|
||||||
delivery_status: str | None = None
|
delivery_status: str | None = Field(
|
||||||
order_state: str | None = None
|
default=None,
|
||||||
discovered_at: str
|
description="站点配送阶段的结构化枚举码(如 CHECKING_ORDER),原样带给上游对账;"
|
||||||
detail_fetched_at: str | None = None
|
"未识别时为 null 或 stepper 兜底文本",
|
||||||
last_seen_at: str
|
)
|
||||||
|
order_state: str | None = Field(
|
||||||
|
default=None, description="worker 规范化上报的订单状态(OrderState 词汇表);映射不到为 null"
|
||||||
|
)
|
||||||
|
discovered_at: str = Field(description="首次编目时间(ISO8601 UTC)")
|
||||||
|
detail_fetched_at: str | None = Field(
|
||||||
|
default=None, description="最近一次取到订单详情的时间(ISO8601 UTC);从未取过为 null"
|
||||||
|
)
|
||||||
|
last_seen_at: str = Field(description="最近一次列表扫描见到该订单的时间(ISO8601 UTC)")
|
||||||
|
|
||||||
|
|
||||||
class CatalogOrderListData(BaseModel):
|
class CatalogOrderListData(BaseModel):
|
||||||
"""编目订单列表(GET /api/account/orders)"""
|
"""编目订单列表(GET /api/account/orders)"""
|
||||||
|
|
||||||
items: list[CatalogedOrder]
|
items: list[CatalogedOrder] = Field(description="编目订单列表,按最近出现(last_seen_at)倒序")
|
||||||
total: int
|
total: int = Field(description="符合筛选条件的总条数")
|
||||||
limit: int
|
limit: int = Field(description="本次分页大小")
|
||||||
offset: int
|
offset: int = Field(description="本次分页偏移")
|
||||||
|
|
||||||
|
|
||||||
class TriggerDiscoveryData(BaseModel):
|
class TriggerDiscoveryData(BaseModel):
|
||||||
"""手动触发一轮下派的响应"""
|
"""手动触发一轮下派的响应"""
|
||||||
|
|
||||||
query_id: str # 本轮 order_list 下派的单号
|
query_id: str = Field(description="本轮 order_list 下派的查询单号")
|
||||||
status: QueryStatus
|
status: QueryStatus = Field(description="该查询单当前状态")
|
||||||
created: bool # 本轮扫描是否新建了一张 order_list 单(False=已有一张在飞/已入队)
|
created: bool = Field(
|
||||||
|
description="本轮扫描是否新建了一张 order_list 单(False=已有一张在飞/已入队)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---- GET /health ----
|
# ---- GET /health ----
|
||||||
@@ -414,18 +424,18 @@ class TriggerDiscoveryData(BaseModel):
|
|||||||
class WorkerHealthEntry(BaseModel):
|
class WorkerHealthEntry(BaseModel):
|
||||||
"""单个 worker 的健康指标"""
|
"""单个 worker 的健康指标"""
|
||||||
|
|
||||||
worker_id: str
|
worker_id: str = Field(description="worker 标识")
|
||||||
last_seen_at: str
|
last_seen_at: str = Field(description="最近一次心跳时间(ISO8601 UTC)")
|
||||||
last_seen_seconds: int
|
last_seen_seconds: int = Field(description="距上次心跳的秒数,超过 300 秒(5 分钟)判失联")
|
||||||
|
|
||||||
|
|
||||||
class QueuedAlertEntry(BaseModel):
|
class QueuedAlertEntry(BaseModel):
|
||||||
"""长时间无人领的任务告警项"""
|
"""长时间无人领的任务告警项"""
|
||||||
|
|
||||||
task_id: str
|
task_id: str = Field(description="任务 ID")
|
||||||
site: str
|
site: str = Field(description="站点标识(rakuten)")
|
||||||
created_at: str
|
created_at: str = Field(description="任务创建时间(ISO8601 UTC)")
|
||||||
age_seconds: int
|
age_seconds: int = Field(description="从创建到现在的秒数,超过阈值即视为长时间无人领")
|
||||||
|
|
||||||
|
|
||||||
class GatewayHealthData(BaseModel):
|
class GatewayHealthData(BaseModel):
|
||||||
@@ -435,12 +445,20 @@ class GatewayHealthData(BaseModel):
|
|||||||
不在网关——本地机 7×24 在线,那套逻辑放本地。
|
不在网关——本地机 7×24 在线,那套逻辑放本地。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status: str # "ok" 或 "degraded"
|
status: str = Field(description="整体健康状态:ok=无告警,degraded=有 worker 失联或任务长时间无人领")
|
||||||
queued_count: int
|
queued_count: int = Field(description="等待领取的下单任务数量(status=queued)")
|
||||||
# 等待本地 worker 领取的账号只读查询单数量。查询没有「无人领即告警」这条
|
# 等待本地 worker 领取的账号只读查询单数量。查询没有「无人领即告警」这条
|
||||||
# 规则(它有自己的 TTL 会自动 expired),这里只是给运维一个可见的积压信号。
|
# 规则(它有自己的 TTL 会自动 expired),这里只是给运维一个可见的积压信号。
|
||||||
queued_query_count: int = 0
|
queued_query_count: int = Field(default=0, description="等待领取的账号只读查询单数量(积压信号,不参与告警)")
|
||||||
active_tasks: list[TaskDetail] = Field(default_factory=list)
|
active_tasks: list[TaskDetail] = Field(
|
||||||
workers: list[WorkerHealthEntry] = Field(default_factory=list)
|
default_factory=list, description="当前 leased/running 的下单任务详情列表"
|
||||||
offline_workers: list[WorkerHealthEntry] = Field(default_factory=list)
|
)
|
||||||
stale_queued_tasks: list[QueuedAlertEntry] = Field(default_factory=list)
|
workers: list[WorkerHealthEntry] = Field(
|
||||||
|
default_factory=list, description="全部已知 worker 的健康指标"
|
||||||
|
)
|
||||||
|
offline_workers: list[WorkerHealthEntry] = Field(
|
||||||
|
default_factory=list, description="失联 worker(距上次心跳超过 300 秒),非空即 degraded"
|
||||||
|
)
|
||||||
|
stale_queued_tasks: list[QueuedAlertEntry] = Field(
|
||||||
|
default_factory=list, description="长时间无人领的 queued 任务告警项,非空即 degraded"
|
||||||
|
)
|
||||||
|
|||||||
+326
-294
@@ -46,26 +46,29 @@ class SearchRequest(BaseModel):
|
|||||||
3. 只传 genre_id:抓取该分类下的商品
|
3. 只传 genre_id:抓取该分类下的商品
|
||||||
"""
|
"""
|
||||||
|
|
||||||
keyword: str = ""
|
keyword: str = Field(default="", description="搜索关键词")
|
||||||
page: int = Field(default=1, ge=1, le=150) # 站点侧最多约 150 页(subset 6750 / 45)
|
page: int = Field(default=1, ge=1, le=150, description="页码,站点侧最多约 150 页(subset 6750 / 45)")
|
||||||
sort: SortOption = SortOption.STANDARD
|
sort: SortOption = Field(default=SortOption.STANDARD, description="排序方式,对应搜索页 `s=` 参数")
|
||||||
genre_id: str | None = None # 乐天分类 ID,如 565950
|
genre_id: str | None = Field(default=None, description="乐天分类 ID,如 565950")
|
||||||
min_price: int | None = Field(default=None, ge=0)
|
min_price: int | None = Field(default=None, ge=0, description="最低价格(日元)")
|
||||||
max_price: int | None = Field(default=None, ge=0)
|
max_price: int | None = Field(default=None, ge=0, description="最高价格(日元)")
|
||||||
shop_id: int | None = None # 限定店铺(对应 `sid` 参数,取搜索结果的 shop.shop_id)
|
shop_id: int | None = Field(default=None, description="限定店铺(对应 `sid` 参数,取搜索结果的 shop.shop_id)")
|
||||||
exclude_keyword: str | None = None # 排除词(`nitem`)
|
exclude_keyword: str | None = Field(default=None, description="排除词(`nitem`)")
|
||||||
title_only: bool = False # 仅在商品标题中匹配(`sf=1`)
|
title_only: bool = Field(default=False, description="仅在商品标题中匹配(`sf=1`)")
|
||||||
or_query: bool = False # 关键词之间用 OR 而非 AND(`st=O`)
|
or_query: bool = Field(default=False, description="关键词之间用 OR 而非 AND(`st=O`)")
|
||||||
min_review_score: int | None = Field(default=None, ge=1, le=5) # 最低评分
|
min_review_score: int | None = Field(default=None, ge=1, le=5, description="最低评分")
|
||||||
condition: ItemCondition | None = None # 新品 / 中古 / 租赁
|
condition: ItemCondition | None = Field(default=None, description="商品成色筛选:新品 / 中古 / 租赁")
|
||||||
include_sold_out: bool = False # 包含售罄商品
|
include_sold_out: bool = Field(default=False, description="包含售罄商品")
|
||||||
free_shipping: bool = False # 仅免运费
|
free_shipping: bool = Field(default=False, description="仅免运费")
|
||||||
has_review: bool = False # 仅有评论
|
has_review: bool = Field(default=False, description="仅有评论")
|
||||||
next_day_delivery: bool = False # 仅次日达
|
next_day_delivery: bool = Field(default=False, description="仅次日达")
|
||||||
super_deal: bool = False # 仅 SuperDEAL
|
super_deal: bool = Field(default=False, description="仅 SuperDEAL")
|
||||||
tags: list[str] = Field(default_factory=list) # 站点标签 ID(`tg`)
|
tags: list[str] = Field(default_factory=list, description="站点标签 ID(`tg`)")
|
||||||
search_url: HttpUrl | None = None
|
search_url: HttpUrl | None = Field(
|
||||||
exclude_ads: bool = True # 剔除搜索结果中混入的 CPC 广告位
|
default=None,
|
||||||
|
description="直接透传一条乐天搜索页 URL,服务端原样抓取;此时除 page 与 exclude_ads 外的筛选字段全部忽略",
|
||||||
|
)
|
||||||
|
exclude_ads: bool = Field(default=True, description="剔除搜索结果中混入的 CPC 广告位")
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_search_target(self) -> SearchRequest:
|
def check_search_target(self) -> SearchRequest:
|
||||||
@@ -84,8 +87,8 @@ class SearchRequest(BaseModel):
|
|||||||
class ShopDetailRequest(BaseModel):
|
class ShopDetailRequest(BaseModel):
|
||||||
"""乐天商家详情请求参数:传店铺代码(店铺 URL 的路径段),或直接传店铺页 URL"""
|
"""乐天商家详情请求参数:传店铺代码(店铺 URL 的路径段),或直接传店铺页 URL"""
|
||||||
|
|
||||||
shop_code: str | None = None # 店铺代码,如 edion
|
shop_code: str | None = Field(default=None, description="店铺代码,如 edion")
|
||||||
shop_url: HttpUrl | None = None
|
shop_url: HttpUrl | None = Field(default=None, description="店铺页 URL;与 shop_code 二选一")
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_shop_target(self) -> ShopDetailRequest:
|
def check_shop_target(self) -> ShopDetailRequest:
|
||||||
@@ -104,18 +107,18 @@ class ShopItemsRequest(BaseModel):
|
|||||||
换出 shop_id,多花一次请求,能直接给 shop_id 时优先给。
|
换出 shop_id,多花一次请求,能直接给 shop_id 时优先给。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
shop_id: int | None = None # 取自搜索结果或商家详情的 shop.shop_id
|
shop_id: int | None = Field(default=None, description="店铺 ID,取自搜索结果或商家详情的 shop.shop_id")
|
||||||
shop_code: str | None = None # 店铺代码,如 edion
|
shop_code: str | None = Field(default=None, description="店铺代码,如 edion")
|
||||||
keyword: str = "" # 在店铺内按关键词过滤
|
keyword: str = Field(default="", description="在店铺内按关键词过滤")
|
||||||
page: int = Field(default=1, ge=1, le=150)
|
page: int = Field(default=1, ge=1, le=150, description="页码")
|
||||||
sort: SortOption = SortOption.STANDARD
|
sort: SortOption = Field(default=SortOption.STANDARD, description="排序方式")
|
||||||
genre_id: str | None = None
|
genre_id: str | None = Field(default=None, description="乐天分类 ID,如 565950")
|
||||||
min_price: int | None = Field(default=None, ge=0)
|
min_price: int | None = Field(default=None, ge=0, description="最低价格(日元)")
|
||||||
max_price: int | None = Field(default=None, ge=0)
|
max_price: int | None = Field(default=None, ge=0, description="最高价格(日元)")
|
||||||
condition: ItemCondition | None = None
|
condition: ItemCondition | None = Field(default=None, description="商品成色筛选")
|
||||||
include_sold_out: bool = False
|
include_sold_out: bool = Field(default=False, description="包含售罄商品")
|
||||||
free_shipping: bool = False
|
free_shipping: bool = Field(default=False, description="仅免运费")
|
||||||
exclude_ads: bool = True
|
exclude_ads: bool = Field(default=True, description="剔除搜索结果中混入的 CPC 广告位")
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_shop_target(self) -> ShopItemsRequest:
|
def check_shop_target(self) -> ShopItemsRequest:
|
||||||
@@ -149,30 +152,32 @@ class ShopItemsRequest(BaseModel):
|
|||||||
class ShopDetailData(BaseModel):
|
class ShopDetailData(BaseModel):
|
||||||
"""乐天商家详情数据"""
|
"""乐天商家详情数据"""
|
||||||
|
|
||||||
shop_id: int | None = None
|
shop_id: int | None = Field(default=None, description="店铺 ID")
|
||||||
shop_code: str = ""
|
shop_code: str = Field(default="", description="店铺代码,如 edion")
|
||||||
shop_name: str = ""
|
shop_name: str = Field(default="", description="店铺名")
|
||||||
shop_url: str = ""
|
shop_url: str = Field(default="", description="店铺页地址")
|
||||||
introduction: str = "" # 店铺简介
|
introduction: str = Field(default="", description="店铺简介")
|
||||||
signboard_url: str = "" # 店铺招牌图
|
signboard_url: str = Field(default="", description="店铺招牌图地址")
|
||||||
logo_url: str = ""
|
logo_url: str = Field(default="", description="店铺 Logo 图地址")
|
||||||
review_score: float = 0.0
|
review_score: float = Field(default=0.0, description="综合评分")
|
||||||
review_count: int = 0
|
review_count: int = Field(default=0, description="评价数")
|
||||||
# 站点在评价数过少时不展示评分;此时 review_score 不可信
|
# 站点在评价数过少时不展示评分;此时 review_score 不可信
|
||||||
review_displayed: bool = False
|
review_displayed: bool = Field(default=False, description="站点是否展示了评分;未展示时 review_score 不可信")
|
||||||
is_39_shop: bool = False # 39ショップ(满 3980 日元免运费)
|
is_39_shop: bool = Field(default=False, description="39ショップ(满 3980 日元免运费)")
|
||||||
age_verification_required: bool = False
|
age_verification_required: bool = Field(default=False, description="购买该店铺商品需要年龄确认")
|
||||||
status: int | None = None # 站点店铺状态码,1 = 营业中
|
status: int | None = Field(default=None, description="站点店铺状态码,1 = 营业中")
|
||||||
holidays: list[str] = Field(default_factory=list) # 店铺休息日
|
holidays: list[str] = Field(default_factory=list, description="店铺休息日")
|
||||||
|
|
||||||
|
|
||||||
class ItemDetailRequest(BaseModel):
|
class ItemDetailRequest(BaseModel):
|
||||||
"""商品详情请求参数:传 shop_code + item_code,或直接传商品页 URL"""
|
"""商品详情请求参数:传 shop_code + item_code,或直接传商品页 URL"""
|
||||||
|
|
||||||
shop_code: str | None = None # 店铺代码,如 edion(商品 URL 的第一段)
|
shop_code: str | None = Field(default=None, description="店铺代码,如 edion(商品 URL 的第一段)")
|
||||||
item_code: str | None = None # 店铺内商品编号,如 4902370549263(商品 URL 的第二段)
|
item_code: str | None = Field(default=None, description="店铺内商品编号,如 4902370549263(商品 URL 的第二段)")
|
||||||
item_url: HttpUrl | None = None
|
item_url: HttpUrl | None = Field(default=None, description="商品页 URL;不传时需同时提供 shop_code 与 item_code")
|
||||||
include_sku_variants: bool = True # SKU 组合可能多达数百条,不需要时可关闭
|
include_sku_variants: bool = Field(
|
||||||
|
default=True, description="是否返回 SKU 组合明细;SKU 组合可能多达数百条,不需要时可关闭"
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_item_target(self) -> ItemDetailRequest:
|
def check_item_target(self) -> ItemDetailRequest:
|
||||||
@@ -187,164 +192,169 @@ class GenreRequest(BaseModel):
|
|||||||
不传 genre_id 时返回 39 个顶层分类;传入时返回该分类的信息、祖先路径与直接子分类。
|
不传 genre_id 时返回 39 个顶层分类;传入时返回该分类的信息、祖先路径与直接子分类。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
genre_id: str | None = None
|
genre_id: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="分类 ID。不传返回 39 个顶层分类;传入返回该分类的信息、祖先路径与直接子分类",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GenreNode(BaseModel):
|
class GenreNode(BaseModel):
|
||||||
"""分类树上的一个节点"""
|
"""分类树上的一个节点"""
|
||||||
|
|
||||||
genre_id: str = ""
|
genre_id: str = Field(default="", description="分类 ID")
|
||||||
name: str = ""
|
name: str = Field(default="", description="分类名")
|
||||||
# 该分类下的商品数。顶层列表不返回该值:站点给出的是「当前查询在该分类下的
|
# 该分类下的商品数。顶层列表不返回该值:站点给出的是「当前查询在该分类下的
|
||||||
# 命中数」,与分类自身的商品总量不是一回事,避免误用。
|
# 命中数」,与分类自身的商品总量不是一回事,避免误用。
|
||||||
item_count: int | None = None
|
item_count: int | None = Field(default=None, description="该分类下的商品数;顶层列表不返回该值")
|
||||||
shortcut: str = "" # 站点分类短代码,如 game / flower
|
shortcut: str = Field(default="", description="站点分类短代码,如 game / flower")
|
||||||
is_leaf: bool = False # 叶子分类,没有下级
|
is_leaf: bool = Field(default=False, description="叶子分类,没有下级")
|
||||||
url: str = "" # 分类页地址
|
url: str = Field(default="", description="分类页地址")
|
||||||
|
|
||||||
|
|
||||||
class GenreData(BaseModel):
|
class GenreData(BaseModel):
|
||||||
"""分类查询结果"""
|
"""分类查询结果"""
|
||||||
|
|
||||||
genre_id: str = "" # 空串表示顶层
|
genre_id: str = Field(default="", description="分类 ID,空串表示顶层")
|
||||||
name: str = ""
|
name: str = Field(default="", description="分类名")
|
||||||
full_name: str = "" # 站点给出的完整分类名,仅分类页有
|
full_name: str = Field(default="", description="站点给出的完整分类名,仅分类页有")
|
||||||
description: str = "" # 站点分类描述,仅分类页有
|
description: str = Field(default="", description="站点分类描述,仅分类页有")
|
||||||
is_leaf: bool = False
|
is_leaf: bool = Field(default=False, description="叶子分类,没有下级")
|
||||||
url: str = ""
|
url: str = Field(default="", description="分类页地址")
|
||||||
ancestors: list[GenreNode] = Field(default_factory=list) # 从顶层到父级,不含自身
|
ancestors: list[GenreNode] = Field(default_factory=list, description="祖先分类路径,从顶层到父级,不含自身")
|
||||||
children: list[GenreNode] = Field(default_factory=list) # 直接子分类
|
children: list[GenreNode] = Field(default_factory=list, description="直接子分类")
|
||||||
|
|
||||||
|
|
||||||
class ShopSummary(BaseModel):
|
class ShopSummary(BaseModel):
|
||||||
"""店铺信息"""
|
"""店铺信息"""
|
||||||
|
|
||||||
shop_id: int | None = None
|
shop_id: int | None = Field(default=None, description="店铺 ID")
|
||||||
shop_code: str = "" # 店铺 URL 代码,如 edion;与 item_code 一起可定位商品
|
shop_code: str = Field(default="", description="店铺 URL 代码,如 edion;与 item_code 一起可定位商品")
|
||||||
shop_name: str = ""
|
shop_name: str = Field(default="", description="店铺名")
|
||||||
shop_url: str = ""
|
shop_url: str = Field(default="", description="店铺页地址")
|
||||||
review_score: float = 0.0
|
review_score: float = Field(default=0.0, description="综合评分")
|
||||||
review_count: int = 0
|
review_count: int = Field(default=0, description="评价数")
|
||||||
|
|
||||||
|
|
||||||
class ReviewSummary(BaseModel):
|
class ReviewSummary(BaseModel):
|
||||||
"""评价信息"""
|
"""评价信息"""
|
||||||
|
|
||||||
score: float = 0.0
|
score: float = Field(default=0.0, description="评分")
|
||||||
count: int = 0
|
count: int = Field(default=0, description="评价数")
|
||||||
url: str = ""
|
url: str = Field(default="", description="评价页地址")
|
||||||
|
|
||||||
|
|
||||||
class SearchItem(BaseModel):
|
class SearchItem(BaseModel):
|
||||||
"""搜索结果中的单个商品"""
|
"""搜索结果中的单个商品"""
|
||||||
|
|
||||||
item_id: str = "" # 乐天内部商品 ID(搜索结果的 code 字段)
|
item_id: str = Field(default="", description="乐天内部商品 ID(搜索结果的 code 字段)")
|
||||||
item_code: str = "" # 商品 URL 第二段,调详情接口用
|
item_code: str = Field(default="", description="商品 URL 第二段,调详情接口用")
|
||||||
item_name: str = ""
|
item_name: str = Field(default="", description="商品名")
|
||||||
item_url: str = "" # 真实商品页地址;广告位已还原为 originalItemUrl
|
item_url: str = Field(default="", description="真实商品页地址;广告位已还原为 originalItemUrl")
|
||||||
catch_copy: str = "" # 商品副标题
|
catch_copy: str = Field(default="", description="商品副标题")
|
||||||
price: int = 0
|
price: int = Field(default=0, description="价格(日元)")
|
||||||
price_range: str = "" # 多 SKU 时的价格区间,如 "1000~2000"
|
price_range: str = Field(default="", description="多 SKU 时的价格区间,如「1000~2000」")
|
||||||
has_price_range: bool = False
|
has_price_range: bool = Field(default=False, description="是否为多 SKU 价格区间")
|
||||||
image_url: str = ""
|
image_url: str = Field(default="", description="主图地址")
|
||||||
image_urls: list[str] = Field(default_factory=list)
|
image_urls: list[str] = Field(default_factory=list, description="图片地址列表")
|
||||||
shop: ShopSummary = Field(default_factory=ShopSummary)
|
shop: ShopSummary = Field(default_factory=ShopSummary, description="所属店铺信息")
|
||||||
review: ReviewSummary = Field(default_factory=ReviewSummary)
|
review: ReviewSummary = Field(default_factory=ReviewSummary, description="商品评价信息")
|
||||||
genre_id: str = ""
|
genre_id: str = Field(default="", description="分类 ID")
|
||||||
genre_path: str = "" # 形如 /0/101205/565950/566404
|
genre_path: str = Field(default="", description="分类路径,形如 /0/101205/565950/566404")
|
||||||
genre_names: list[str] = Field(default_factory=list)
|
genre_names: list[str] = Field(default_factory=list, description="分类名列表")
|
||||||
shipping_fee: int | None = None # 站点未给出时为 null
|
shipping_fee: int | None = Field(default=None, description="运费(日元);站点未给出时为 null")
|
||||||
delivery_message: str = ""
|
delivery_message: str = Field(default="", description="配送说明")
|
||||||
point_count: int = 0
|
point_count: int = Field(default=0, description="乐天积分倍率(站点 point.count 原值)")
|
||||||
is_sold_out: bool = False
|
is_sold_out: bool = Field(default=False, description="已售罄")
|
||||||
is_ad: bool = False # CPC 广告位
|
is_ad: bool = Field(default=False, description="CPC 广告位")
|
||||||
has_multi_sku: bool = False
|
has_multi_sku: bool = Field(default=False, description="有多个 SKU")
|
||||||
variant_id: str = ""
|
variant_id: str = Field(default="", description="SKU 组合 ID(站点 variantId 原值)")
|
||||||
item_options: dict[str, Any] = Field(default_factory=dict) # 站点 itemOptions 原样透出
|
item_options: dict[str, Any] = Field(default_factory=dict, description="站点 itemOptions 原样透出")
|
||||||
|
|
||||||
|
|
||||||
class SearchResultData(BaseModel):
|
class SearchResultData(BaseModel):
|
||||||
"""搜索结果数据"""
|
"""搜索结果数据"""
|
||||||
|
|
||||||
keyword: str = ""
|
keyword: str = Field(default="", description="搜索关键词")
|
||||||
page: int = 1
|
page: int = Field(default=1, description="当前页码")
|
||||||
page_size: int = 0
|
page_size: int = Field(default=0, description="每页条数")
|
||||||
total_count: int = 0 # 站点声明的命中总数
|
total_count: int = Field(default=0, description="站点声明的命中总数")
|
||||||
reachable_count: int = 0 # 实际可翻页取到的上限(站点 subset,随查询条件变化)
|
reachable_count: int = Field(default=0, description="实际可翻页取到的上限(站点 subset,随查询条件变化)")
|
||||||
has_more: bool = False
|
has_more: bool = Field(default=False, description="是否还有下一页")
|
||||||
# 请求页码超出 reachable_count 对应的页数。站点此时不会返回空列表,而是
|
# 请求页码超出 reachable_count 对应的页数。站点此时不会返回空列表,而是
|
||||||
# 静默回绕到第 1 页;这里识别出来并把 items 置空,避免上游把重复数据当新数据。
|
# 静默回绕到第 1 页;这里识别出来并把 items 置空,避免上游把重复数据当新数据。
|
||||||
out_of_range: bool = False
|
out_of_range: bool = Field(
|
||||||
ad_count: int = 0 # 本页被识别出的广告位数量(exclude_ads=true 时已从 items 剔除)
|
default=False, description="请求页码超出可翻页范围;此时 items 置空(站点会静默回绕到第 1 页,已识别)"
|
||||||
request_url: str = "" # 实际抓取的乐天页面地址,便于排查
|
)
|
||||||
items: list[SearchItem] = Field(default_factory=list)
|
ad_count: int = Field(default=0, description="本页被识别出的广告位数量(exclude_ads=true 时已从 items 剔除)")
|
||||||
|
request_url: str = Field(default="", description="实际抓取的乐天页面地址,便于排查")
|
||||||
|
items: list[SearchItem] = Field(default_factory=list, description="本页商品列表")
|
||||||
|
|
||||||
|
|
||||||
class SkuAttribute(BaseModel):
|
class SkuAttribute(BaseModel):
|
||||||
"""SKU 属性项"""
|
"""SKU 属性项"""
|
||||||
|
|
||||||
title: str = ""
|
title: str = Field(default="", description="属性名")
|
||||||
value: str = ""
|
value: str = Field(default="", description="属性值")
|
||||||
|
|
||||||
|
|
||||||
class SkuAxisValue(BaseModel):
|
class SkuAxisValue(BaseModel):
|
||||||
"""SKU 选择轴上的一个取值"""
|
"""SKU 选择轴上的一个取值"""
|
||||||
|
|
||||||
value: str = ""
|
value: str = Field(default="", description="取值原值")
|
||||||
label: str = ""
|
label: str = Field(default="", description="取值展示名")
|
||||||
is_sold_out: bool = False
|
is_sold_out: bool = Field(default=False, description="该取值已售罄")
|
||||||
|
|
||||||
|
|
||||||
class SkuAxis(BaseModel):
|
class SkuAxis(BaseModel):
|
||||||
"""SKU 选择轴,如「颜色」「尺码」"""
|
"""SKU 选择轴,如「颜色」「尺码」"""
|
||||||
|
|
||||||
key: str = ""
|
key: str = Field(default="", description="轴标识")
|
||||||
label: str = ""
|
label: str = Field(default="", description="轴展示名")
|
||||||
values: list[SkuAxisValue] = Field(default_factory=list)
|
values: list[SkuAxisValue] = Field(default_factory=list, description="可选取值列表")
|
||||||
|
|
||||||
|
|
||||||
class SkuVariant(BaseModel):
|
class SkuVariant(BaseModel):
|
||||||
"""一个具体的 SKU 组合"""
|
"""一个具体的 SKU 组合"""
|
||||||
|
|
||||||
variant_id: str = ""
|
variant_id: str = Field(default="", description="SKU 组合 ID")
|
||||||
selector_values: list[str] = Field(default_factory=list) # 与 axis 顺序对应的取值
|
selector_values: list[str] = Field(default_factory=list, description="与 axis 顺序对应的取值")
|
||||||
price: int = 0
|
price: int = Field(default=0, description="价格(日元)")
|
||||||
quantity: int = 0
|
quantity: int = Field(default=0, description="库存数量")
|
||||||
is_sold_out: bool = False
|
is_sold_out: bool = Field(default=False, description="已售罄")
|
||||||
delivery_message: str = ""
|
delivery_message: str = Field(default="", description="配送说明")
|
||||||
attributes: list[SkuAttribute] = Field(default_factory=list)
|
attributes: list[SkuAttribute] = Field(default_factory=list, description="SKU 属性列表")
|
||||||
|
|
||||||
|
|
||||||
class SkuInfo(BaseModel):
|
class SkuInfo(BaseModel):
|
||||||
"""商品 SKU 信息"""
|
"""商品 SKU 信息"""
|
||||||
|
|
||||||
inventory_type: str = "" # single / multiple
|
inventory_type: str = Field(default="", description="库存类型:single / multiple")
|
||||||
quantity: int = 0
|
quantity: int = Field(default=0, description="库存数量")
|
||||||
show_inventory: bool = False
|
show_inventory: bool = Field(default=False, description="站点是否展示库存")
|
||||||
delivery_message: str = ""
|
delivery_message: str = Field(default="", description="配送说明")
|
||||||
attributes: list[SkuAttribute] = Field(default_factory=list)
|
attributes: list[SkuAttribute] = Field(default_factory=list, description="SKU 属性列表")
|
||||||
axis: list[SkuAxis] = Field(default_factory=list)
|
axis: list[SkuAxis] = Field(default_factory=list, description="SKU 选择轴,如「颜色」「尺码」")
|
||||||
variants: list[SkuVariant] = Field(default_factory=list) # include_sku_variants=false 时为空
|
variants: list[SkuVariant] = Field(default_factory=list, description="SKU 组合明细;include_sku_variants=false 时为空")
|
||||||
variant_count: int = 0 # 不受 include_sku_variants 影响,始终为真实组合数
|
variant_count: int = Field(default=0, description="SKU 组合总数;不受 include_sku_variants 影响,始终为真实组合数")
|
||||||
|
|
||||||
|
|
||||||
class ShippingInfo(BaseModel):
|
class ShippingInfo(BaseModel):
|
||||||
"""配送与运费信息"""
|
"""配送与运费信息"""
|
||||||
|
|
||||||
shipping_fee: int | None = None
|
shipping_fee: int | None = Field(default=None, description="运费(日元);站点未给出时为 null")
|
||||||
is_shipping_free: bool = False
|
is_shipping_free: bool = Field(default=False, description="免运费")
|
||||||
is_asuraku: bool = False # あす楽(次日达)
|
is_asuraku: bool = Field(default=False, description="あす楽(次日达)")
|
||||||
is_next_day_delivery: bool = False
|
is_next_day_delivery: bool = Field(default=False, description="次日达")
|
||||||
free_shipping_threshold: int | None = None
|
free_shipping_threshold: int | None = Field(default=None, description="免运费门槛(日元)")
|
||||||
prefecture_id: int | None = None # 站点默认收货地(13 = 东京都)
|
prefecture_id: int | None = Field(default=None, description="站点默认收货地(13 = 东京都)")
|
||||||
delivery_message: str = ""
|
delivery_message: str = Field(default="", description="配送说明")
|
||||||
|
|
||||||
|
|
||||||
class Breadcrumb(BaseModel):
|
class Breadcrumb(BaseModel):
|
||||||
"""分类面包屑"""
|
"""分类面包屑"""
|
||||||
|
|
||||||
name: str = ""
|
name: str = Field(default="", description="分类名")
|
||||||
url: str = ""
|
url: str = Field(default="", description="分类页地址")
|
||||||
|
|
||||||
|
|
||||||
class ItemDetailData(BaseModel):
|
class ItemDetailData(BaseModel):
|
||||||
@@ -358,28 +368,28 @@ class ItemDetailData(BaseModel):
|
|||||||
(purchase_condition / is_sold_out)、规格(sku.variants)、起订单位等。
|
(purchase_condition / is_sold_out)、规格(sku.variants)、起订单位等。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source: str = "ichiba" # ichiba / books / brandavenue / biccamera
|
source: str = Field(default="ichiba", description="数据来源站点:ichiba / books / brandavenue / biccamera")
|
||||||
source_url: str = "" # 实际解析的页面地址;跳转时与 item_url 不同
|
source_url: str = Field(default="", description="实际解析的页面地址;跳转时与 item_url 不同")
|
||||||
item_id: str = ""
|
item_id: str = Field(default="", description="乐天内部商品 ID")
|
||||||
item_code: str = ""
|
item_code: str = Field(default="", description="店铺内商品编号(商品 URL 第二段)")
|
||||||
item_name: str = ""
|
item_name: str = Field(default="", description="商品名")
|
||||||
catch_copy: str = ""
|
catch_copy: str = Field(default="", description="商品副标题")
|
||||||
description: str = "" # 店铺自填的商品说明,含 HTML
|
description: str = Field(default="", description="店铺自填的商品说明,含 HTML")
|
||||||
item_url: str = ""
|
item_url: str = Field(default="", description="商品页地址")
|
||||||
price: int = 0 # 最低售价(含税,多 SKU 时为最低价)
|
price: int = Field(default=0, description="最低售价(日元,含税;多 SKU 时为最低价)")
|
||||||
pre_tax_price: int = 0
|
pre_tax_price: int = Field(default=0, description="税前价格(日元)")
|
||||||
tax_flag: bool = False
|
tax_flag: bool = Field(default=False, description="价格是否含税(站点 taxFlag 原值)")
|
||||||
tax_rate: float = 0.0
|
tax_rate: float = Field(default=0.0, description="税率")
|
||||||
purchase_condition: str = "" # 站点原值,enabled 表示可购买
|
purchase_condition: str = Field(default="", description="站点原值,enabled 表示可购买")
|
||||||
is_sold_out: bool = False
|
is_sold_out: bool = Field(default=False, description="已售罄")
|
||||||
purchase_unit: int = 0 # 起订单位
|
purchase_unit: int = Field(default=0, description="起订单位")
|
||||||
images: list[str] = Field(default_factory=list)
|
images: list[str] = Field(default_factory=list, description="商品图片地址列表")
|
||||||
shop: ShopSummary = Field(default_factory=ShopSummary)
|
shop: ShopSummary = Field(default_factory=ShopSummary, description="所属店铺信息")
|
||||||
review: ReviewSummary = Field(default_factory=ReviewSummary)
|
review: ReviewSummary = Field(default_factory=ReviewSummary, description="商品评价信息")
|
||||||
genre_id: str = ""
|
genre_id: str = Field(default="", description="分类 ID")
|
||||||
breadcrumbs: list[Breadcrumb] = Field(default_factory=list)
|
breadcrumbs: list[Breadcrumb] = Field(default_factory=list, description="分类面包屑")
|
||||||
shipping: ShippingInfo = Field(default_factory=ShippingInfo)
|
shipping: ShippingInfo = Field(default_factory=ShippingInfo, description="该商品的配送与运费信息")
|
||||||
sku: SkuInfo = Field(default_factory=SkuInfo)
|
sku: SkuInfo = Field(default_factory=SkuInfo, description="SKU 信息")
|
||||||
|
|
||||||
|
|
||||||
class HealthData(BaseModel):
|
class HealthData(BaseModel):
|
||||||
@@ -388,11 +398,11 @@ class HealthData(BaseModel):
|
|||||||
这里不含登录态——登录态属于交易服务,查它请打交易服务的 /health。
|
这里不含登录态——登录态属于交易服务,查它请打交易服务的 /health。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status: str
|
status: str = Field(description="服务状态,正常为 ok")
|
||||||
browser_fallback_enabled: bool
|
browser_fallback_enabled: bool = Field(description="是否启用浏览器兜底抓取")
|
||||||
browser_fallback_ready: bool
|
browser_fallback_ready: bool = Field(description="浏览器兜底是否就绪")
|
||||||
browser_fallback_error: str | None = None
|
browser_fallback_error: str | None = Field(default=None, description="浏览器兜底不可用的原因;正常为 null")
|
||||||
sessions: dict[str, Any] = Field(default_factory=dict)
|
sessions: dict[str, Any] = Field(default_factory=dict, description="各站点抓取会话状态(rakuten / rakuma)")
|
||||||
|
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
@@ -449,21 +459,25 @@ class RakumaSearchRequest(BaseModel):
|
|||||||
keyword、category_id、brand_id、search_url 四者至少提供一个。
|
keyword、category_id、brand_id、search_url 四者至少提供一个。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
keyword: str = ""
|
keyword: str = Field(default="", description="搜索关键词")
|
||||||
page: int = Field(default=1, ge=1, le=100) # 站点侧 page>100 直接 404
|
page: int = Field(default=1, ge=1, le=100, description="页码,站点侧 page>100 直接 404")
|
||||||
sort: RakumaSortOption = RakumaSortOption.STANDARD
|
sort: RakumaSortOption = Field(default=RakumaSortOption.STANDARD, description="排序方式")
|
||||||
category_id: str | None = None # ラクマ 分类 ID,如 788
|
category_id: str | None = Field(default=None, description="ラクマ 分类 ID,如 788")
|
||||||
brand_id: str | None = None # ラクマ 品牌 ID,如 5296
|
brand_id: str | None = Field(default=None, description="ラクマ 品牌 ID,如 5296")
|
||||||
min_price: int | None = Field(default=None, ge=0)
|
min_price: int | None = Field(default=None, ge=0, description="最低价格(日元)")
|
||||||
max_price: int | None = Field(default=None, ge=0)
|
max_price: int | None = Field(default=None, ge=0, description="最高价格(日元)")
|
||||||
exclude_keyword: str | None = None # 排除词(站点 `excluded_query`,需与 keyword 同时使用)
|
exclude_keyword: str | None = Field(
|
||||||
conditions: list[RakumaCondition] = Field(default_factory=list) # 可多选
|
default=None, description="排除词(站点 `excluded_query`,需与 keyword 同时使用)"
|
||||||
transaction: RakumaTransaction | None = None # 不传表示不限
|
)
|
||||||
free_shipping: bool = False # 仅「送料込み」(卖家承担运费)
|
conditions: list[RakumaCondition] = Field(default_factory=list, description="商品状态筛选,可多选")
|
||||||
anonymous_shipping: bool = False # 仅匿名配送
|
transaction: RakumaTransaction | None = Field(default=None, description="售卖状态筛选,不传表示不限")
|
||||||
except_for_no_brand: bool = False # 排除无品牌商品;与 brand_id 互斥
|
free_shipping: bool = Field(default=False, description="仅「送料込み」(卖家承担运费)")
|
||||||
authenticity_types: list[RakumaAuthenticity] = Field(default_factory=list)
|
anonymous_shipping: bool = Field(default=False, description="仅匿名配送")
|
||||||
search_url: HttpUrl | None = None
|
except_for_no_brand: bool = Field(default=False, description="排除无品牌商品;与 brand_id 互斥")
|
||||||
|
authenticity_types: list[RakumaAuthenticity] = Field(default_factory=list, description="正品鉴定服务类型筛选,可多选")
|
||||||
|
search_url: HttpUrl | None = Field(
|
||||||
|
default=None, description="直接透传一条 fril.jp 搜索页 URL;此时除 page 外的筛选字段全部忽略"
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_search_target(self) -> RakumaSearchRequest:
|
def check_search_target(self) -> RakumaSearchRequest:
|
||||||
@@ -480,8 +494,8 @@ class RakumaSearchRequest(BaseModel):
|
|||||||
class RakumaItemDetailRequest(BaseModel):
|
class RakumaItemDetailRequest(BaseModel):
|
||||||
"""ラクマ 商品详情请求参数:传 item_id(商品 URL 的最后一段),或直接传商品页 URL"""
|
"""ラクマ 商品详情请求参数:传 item_id(商品 URL 的最后一段),或直接传商品页 URL"""
|
||||||
|
|
||||||
item_id: str | None = None # 商品页 hash,如 4aca1d6db3e422f3a251a8a8b61e1eff
|
item_id: str | None = Field(default=None, description="商品页 hash,如 4aca1d6db3e422f3a251a8a8b61e1eff")
|
||||||
item_url: HttpUrl | None = None
|
item_url: HttpUrl | None = Field(default=None, description="商品页 URL;与 item_id 二选一")
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_item_target(self) -> RakumaItemDetailRequest:
|
def check_item_target(self) -> RakumaItemDetailRequest:
|
||||||
@@ -493,10 +507,12 @@ class RakumaItemDetailRequest(BaseModel):
|
|||||||
class RakumaShopDetailRequest(BaseModel):
|
class RakumaShopDetailRequest(BaseModel):
|
||||||
"""ラクマ 卖家详情请求参数:传 shop_id(店铺 URL 的最后一段),或直接传店铺页 URL"""
|
"""ラクマ 卖家详情请求参数:传 shop_id(店铺 URL 的最后一段),或直接传店铺页 URL"""
|
||||||
|
|
||||||
shop_id: str | None = None # 店铺页 hash,如 422750cb7921557bc8dba2416915d968
|
shop_id: str | None = Field(default=None, description="店铺页 hash,如 422750cb7921557bc8dba2416915d968")
|
||||||
shop_url: HttpUrl | None = None
|
shop_url: HttpUrl | None = Field(default=None, description="店铺页 URL;与 shop_id 二选一")
|
||||||
# 评价明细在单独的 /review 页上,需要多打一次请求,默认不取
|
# 评价明细在单独的 /review 页上,需要多打一次请求,默认不取
|
||||||
include_reviews: bool = False
|
include_reviews: bool = Field(
|
||||||
|
default=False, description="是否返回评价明细;评价在单独的 /review 页,会多打一次请求"
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_shop_target(self) -> RakumaShopDetailRequest:
|
def check_shop_target(self) -> RakumaShopDetailRequest:
|
||||||
@@ -512,9 +528,9 @@ class RakumaShopItemsRequest(BaseModel):
|
|||||||
排序与筛选参数,因此这里只有页码。
|
排序与筛选参数,因此这里只有页码。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
shop_id: str | None = None
|
shop_id: str | None = Field(default=None, description="店铺页 hash")
|
||||||
shop_url: HttpUrl | None = None
|
shop_url: HttpUrl | None = Field(default=None, description="店铺页 URL;与 shop_id 二选一")
|
||||||
page: int = Field(default=1, ge=1)
|
page: int = Field(default=1, ge=1, description="页码")
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_shop_target(self) -> RakumaShopItemsRequest:
|
def check_shop_target(self) -> RakumaShopItemsRequest:
|
||||||
@@ -531,10 +547,14 @@ class RakumaCategoryRequest(BaseModel):
|
|||||||
组织方式,不会多打请求。
|
组织方式,不会多打请求。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
category_id: str | None = None
|
category_id: str | None = Field(
|
||||||
# 返回该分类下的完整子树(不止直接子级)。分类树共三层,
|
default=None,
|
||||||
# 顶层分类的子树可达上百条。
|
description="分类 ID。不传返回 14 个顶层分类;传入返回该分类的名称、祖先路径与直接子分类",
|
||||||
include_descendants: bool = False
|
)
|
||||||
|
include_descendants: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="返回该分类下的完整子树(不止直接子级);分类树共三层,顶层分类的子树可达上百条",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RakumaCategoryNode(BaseModel):
|
class RakumaCategoryNode(BaseModel):
|
||||||
@@ -544,73 +564,81 @@ class RakumaCategoryNode(BaseModel):
|
|||||||
商品数要逐个分类去抓 `/category/{id}` 页面,成本过高,本接口不提供。
|
商品数要逐个分类去抓 `/category/{id}` 页面,成本过高,本接口不提供。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
category_id: str = ""
|
category_id: str = Field(default="", description="分类 ID")
|
||||||
name: str = ""
|
name: str = Field(default="", description="分类名")
|
||||||
parent_id: str = "" # "0" 表示顶层分类
|
parent_id: str = Field(default="", description="父分类 ID,「0」表示顶层分类")
|
||||||
is_leaf: bool = False # 叶子分类,没有下级
|
is_leaf: bool = Field(default=False, description="叶子分类,没有下级")
|
||||||
url: str = "" # 分类页地址
|
url: str = Field(default="", description="分类页地址")
|
||||||
# include_descendants=true 时填充下级分类,否则始终为空
|
children: list[RakumaCategoryNode] = Field(
|
||||||
children: list[RakumaCategoryNode] = Field(default_factory=list)
|
default_factory=list, description="下级分类;include_descendants=true 时填充,否则始终为空"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RakumaCategoryData(BaseModel):
|
class RakumaCategoryData(BaseModel):
|
||||||
"""ラクマ 分类查询结果"""
|
"""ラクマ 分类查询结果"""
|
||||||
|
|
||||||
category_id: str = "" # 空串表示顶层
|
category_id: str = Field(default="", description="分类 ID,空串表示顶层")
|
||||||
name: str = ""
|
name: str = Field(default="", description="分类名")
|
||||||
full_name: str = "" # 从顶层拼到自身的路径名,如「エンタメ/ホビー / ゲームソフト/ゲーム機本体 / 家庭用ゲームソフト」
|
full_name: str = Field(
|
||||||
is_leaf: bool = False
|
default="",
|
||||||
url: str = ""
|
description="从顶层拼到自身的路径名,如「エンタメ/ホビー / ゲームソフト/ゲーム機本体 / 家庭用ゲームソフト」",
|
||||||
total_count: int = 0 # 站点分类树的节点总数,用于确认取到的是全量树
|
)
|
||||||
ancestors: list[RakumaCategoryNode] = Field(default_factory=list) # 从顶层到父级,不含自身
|
is_leaf: bool = Field(default=False, description="叶子分类,没有下级")
|
||||||
children: list[RakumaCategoryNode] = Field(default_factory=list) # 直接子分类;include_descendants=true 时带子树
|
url: str = Field(default="", description="分类页地址")
|
||||||
|
total_count: int = Field(default=0, description="站点分类树的节点总数,用于确认取到的是全量树")
|
||||||
|
ancestors: list[RakumaCategoryNode] = Field(
|
||||||
|
default_factory=list, description="祖先分类路径,从顶层到父级,不含自身"
|
||||||
|
)
|
||||||
|
children: list[RakumaCategoryNode] = Field(
|
||||||
|
default_factory=list, description="直接子分类;include_descendants=true 时带子树"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RakumaSeller(BaseModel):
|
class RakumaSeller(BaseModel):
|
||||||
"""ラクマ 卖家(出品者)摘要"""
|
"""ラクマ 卖家(出品者)摘要"""
|
||||||
|
|
||||||
shop_id: str = "" # 店铺页 hash,可直接用于 /api/rakuma/shop_detail
|
shop_id: str = Field(default="", description="店铺页 hash,可直接用于 /api/rakuma/shop_detail")
|
||||||
user_id: str = "" # 站点内部数值用户 ID
|
user_id: str = Field(default="", description="站点内部数值用户 ID")
|
||||||
shop_name: str = "" # 店铺名,卖家可自定义
|
shop_name: str = Field(default="", description="店铺名,卖家可自定义")
|
||||||
user_name: str = "" # 用户昵称
|
user_name: str = Field(default="", description="用户昵称")
|
||||||
shop_url: str = ""
|
shop_url: str = Field(default="", description="店铺页地址")
|
||||||
icon_url: str = ""
|
icon_url: str = Field(default="", description="头像地址")
|
||||||
seller_type: str = "" # 站点原值,如 一般 / 事業者
|
seller_type: str = Field(default="", description="卖家类型(站点原值),如 一般 / 事業者")
|
||||||
review_score: float = 0.0
|
review_score: float = Field(default=0.0, description="综合评分")
|
||||||
review_count: int = 0
|
review_count: int = Field(default=0, description="评价数")
|
||||||
is_verified: bool = False # 本人確認済
|
is_verified: bool = Field(default=False, description="本人確認済")
|
||||||
|
|
||||||
|
|
||||||
class RakumaSearchItem(BaseModel):
|
class RakumaSearchItem(BaseModel):
|
||||||
"""ラクマ 搜索结果中的单个商品"""
|
"""ラクマ 搜索结果中的单个商品"""
|
||||||
|
|
||||||
item_id: str = "" # 商品页 hash,调详情接口用
|
item_id: str = Field(default="", description="商品页 hash,调详情接口用")
|
||||||
item_number: str = "" # 站点内部数值商品 ID
|
item_number: str = Field(default="", description="站点内部数值商品 ID")
|
||||||
item_name: str = ""
|
item_name: str = Field(default="", description="商品名")
|
||||||
item_url: str = ""
|
item_url: str = Field(default="", description="商品页地址")
|
||||||
price: int = 0
|
price: int = Field(default=0, description="价格(日元)")
|
||||||
image_url: str = ""
|
image_url: str = Field(default="", description="主图地址")
|
||||||
is_sold_out: bool = False
|
is_sold_out: bool = Field(default=False, description="已售出")
|
||||||
brand_id: str = ""
|
brand_id: str = Field(default="", description="品牌 ID")
|
||||||
brand_name: str = ""
|
brand_name: str = Field(default="", description="品牌名")
|
||||||
category_id: str = ""
|
category_id: str = Field(default="", description="分类 ID")
|
||||||
category_names: list[str] = Field(default_factory=list)
|
category_names: list[str] = Field(default_factory=list, description="分类名列表")
|
||||||
seller_user_id: str = "" # 卖家数值 ID;店铺 hash 需从详情页取
|
seller_user_id: str = Field(default="", description="卖家数值 ID;店铺 hash 需从详情页取")
|
||||||
seller_type: str = ""
|
seller_type: str = Field(default="", description="卖家类型(站点原值),如 一般 / 事業者")
|
||||||
|
|
||||||
|
|
||||||
class RakumaSearchResultData(BaseModel):
|
class RakumaSearchResultData(BaseModel):
|
||||||
"""ラクマ 搜索结果数据"""
|
"""ラクマ 搜索结果数据"""
|
||||||
|
|
||||||
keyword: str = ""
|
keyword: str = Field(default="", description="搜索关键词")
|
||||||
page: int = 1
|
page: int = Field(default=1, description="当前页码")
|
||||||
page_size: int = 0
|
page_size: int = Field(default=0, description="每页条数")
|
||||||
# 站点声明的命中总数。页面上展示为「約1,190,000件」的四舍五入值,
|
# 站点声明的命中总数。页面上展示为「約1,190,000件」的四舍五入值,
|
||||||
# 这里取的是埋点属性里的精确值。
|
# 这里取的是埋点属性里的精确值。
|
||||||
total_count: int = 0
|
total_count: int = Field(default=0, description="站点声明的命中总数(埋点属性里的精确值)")
|
||||||
has_more: bool = False
|
has_more: bool = Field(default=False, description="是否还有下一页")
|
||||||
request_url: str = ""
|
request_url: str = Field(default="", description="实际抓取的页面地址,便于排查")
|
||||||
items: list[RakumaSearchItem] = Field(default_factory=list)
|
items: list[RakumaSearchItem] = Field(default_factory=list, description="本页商品列表")
|
||||||
|
|
||||||
|
|
||||||
class RakumaItemDetailData(BaseModel):
|
class RakumaItemDetailData(BaseModel):
|
||||||
@@ -620,78 +648,82 @@ class RakumaItemDetailData(BaseModel):
|
|||||||
「規格」在站点上只体现为一个可选的尺码字段。
|
「規格」在站点上只体现为一个可选的尺码字段。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item_id: str = ""
|
item_id: str = Field(default="", description="商品页 hash")
|
||||||
item_number: str = ""
|
item_number: str = Field(default="", description="站点内部数值商品 ID")
|
||||||
item_name: str = ""
|
item_name: str = Field(default="", description="商品名")
|
||||||
description: str = ""
|
description: str = Field(default="", description="商品说明(卖家自填)")
|
||||||
item_url: str = ""
|
item_url: str = Field(default="", description="商品页地址")
|
||||||
price: int = 0
|
price: int = Field(default=0, description="价格(日元)")
|
||||||
is_sold_out: bool = False
|
is_sold_out: bool = Field(default=False, description="已售出")
|
||||||
images: list[str] = Field(default_factory=list)
|
images: list[str] = Field(default_factory=list, description="商品图片地址列表")
|
||||||
condition: str = "" # 商品の状態,站点原文如「目立った傷や汚れなし」
|
condition: str = Field(default="", description="商品の状態,站点原文如「目立った傷や汚れなし」")
|
||||||
size: str = "" # サイズ,无尺码时为空
|
size: str = Field(default="", description="サイズ,无尺码时为空")
|
||||||
brand_id: str = ""
|
brand_id: str = Field(default="", description="品牌 ID")
|
||||||
brand_name: str = ""
|
brand_name: str = Field(default="", description="品牌名")
|
||||||
category_id: str = "" # 最具体的一级分类 ID
|
category_id: str = Field(default="", description="最具体的一级分类 ID")
|
||||||
breadcrumbs: list[Breadcrumb] = Field(default_factory=list)
|
breadcrumbs: list[Breadcrumb] = Field(default_factory=list, description="分类面包屑")
|
||||||
shipping_payer: str = "" # 配送料の負担,如「送料込」
|
shipping_payer: str = Field(default="", description="配送料の負担,如「送料込」")
|
||||||
shipping_method: str = "" # 配送方法
|
shipping_method: str = Field(default="", description="配送方法")
|
||||||
shipping_date_estimate: str = "" # 発送日の目安
|
shipping_date_estimate: str = Field(default="", description="発送日の目安")
|
||||||
shipping_from: str = "" # 発送元の地域
|
shipping_from: str = Field(default="", description="発送元の地域")
|
||||||
is_anonymous_shipping: bool = False # 匿名配送
|
is_anonymous_shipping: bool = Field(default=False, description="匿名配送")
|
||||||
like_count: int = 0 # いいね数
|
like_count: int = Field(default=0, description="いいね数")
|
||||||
comment_count: int = 0
|
comment_count: int = Field(default=0, description="评论数")
|
||||||
posted_at: str = "" # 站点展示的相对时间,如「約1時間前」
|
posted_at: str = Field(default="", description="站点展示的相对时间,如「約1時間前」")
|
||||||
seller: RakumaSeller = Field(default_factory=RakumaSeller)
|
seller: RakumaSeller = Field(default_factory=RakumaSeller, description="卖家(出品者)信息")
|
||||||
|
|
||||||
|
|
||||||
class RakumaReview(BaseModel):
|
class RakumaReview(BaseModel):
|
||||||
"""ラクマ 卖家的一条交易评价"""
|
"""ラクマ 卖家的一条交易评价"""
|
||||||
|
|
||||||
rating: str = "" # good / normal / bad
|
rating: str = Field(default="", description="评价档位:good / normal / bad")
|
||||||
title: str = "" # 站点原文,如「よい出品者です」
|
title: str = Field(default="", description="站点原文标题,如「よい出品者です」")
|
||||||
comment: str = ""
|
comment: str = Field(default="", description="评价内容")
|
||||||
reviewer_name: str = ""
|
reviewer_name: str = Field(default="", description="评价者昵称")
|
||||||
reviewed_at: str = "" # 站点展示的日期,如 2026/05/04
|
reviewed_at: str = Field(default="", description="站点展示的日期,如 2026/05/04")
|
||||||
|
|
||||||
|
|
||||||
class RakumaRatingBreakdown(BaseModel):
|
class RakumaRatingBreakdown(BaseModel):
|
||||||
"""评价数量分档"""
|
"""评价数量分档"""
|
||||||
|
|
||||||
good: int = 0
|
good: int = Field(default=0, description="好评数")
|
||||||
normal: int = 0
|
normal: int = Field(default=0, description="中评数")
|
||||||
bad: int = 0
|
bad: int = Field(default=0, description="差评数")
|
||||||
|
|
||||||
|
|
||||||
class RakumaShopDetailData(BaseModel):
|
class RakumaShopDetailData(BaseModel):
|
||||||
"""ラクマ 卖家详情数据"""
|
"""ラクマ 卖家详情数据"""
|
||||||
|
|
||||||
shop_id: str = ""
|
shop_id: str = Field(default="", description="店铺页 hash")
|
||||||
user_id: str = ""
|
user_id: str = Field(default="", description="站点内部数值用户 ID")
|
||||||
shop_name: str = ""
|
shop_name: str = Field(default="", description="店铺名,卖家可自定义")
|
||||||
user_name: str = ""
|
user_name: str = Field(default="", description="用户昵称")
|
||||||
shop_url: str = ""
|
shop_url: str = Field(default="", description="店铺页地址")
|
||||||
icon_url: str = ""
|
icon_url: str = Field(default="", description="头像地址")
|
||||||
cover_url: str = ""
|
cover_url: str = Field(default="", description="封面图地址")
|
||||||
introduction: str = "" # プロフィール文
|
introduction: str = Field(default="", description="プロフィール文(自我介绍)")
|
||||||
review_score: float = 0.0
|
review_score: float = Field(default=0.0, description="综合评分")
|
||||||
review_count: int = 0
|
review_count: int = Field(default=0, description="评价数")
|
||||||
is_verified: bool = False # 本人確認済
|
is_verified: bool = Field(default=False, description="本人確認済")
|
||||||
verification_label: str = "" # 站点原文,如「本人確認済」/「本人確認未完了」
|
verification_label: str = Field(default="", description="站点原文,如「本人確認済」/「本人確認未完了」")
|
||||||
item_count: int = 0 # 该卖家在售 + 已售商品总数
|
item_count: int = Field(default=0, description="该卖家在售 + 已售商品总数")
|
||||||
# 以下三项需 include_reviews=true 才会填充
|
# 以下三项需 include_reviews=true 才会填充
|
||||||
rating_breakdown: RakumaRatingBreakdown = Field(default_factory=RakumaRatingBreakdown)
|
rating_breakdown: RakumaRatingBreakdown = Field(
|
||||||
seller_rating_breakdown: RakumaRatingBreakdown = Field(default_factory=RakumaRatingBreakdown)
|
default_factory=RakumaRatingBreakdown, description="评价数量分档(全部评价)"
|
||||||
reviews: list[RakumaReview] = Field(default_factory=list)
|
)
|
||||||
|
seller_rating_breakdown: RakumaRatingBreakdown = Field(
|
||||||
|
default_factory=RakumaRatingBreakdown, description="评价数量分档(仅作为卖家收到的评价)"
|
||||||
|
)
|
||||||
|
reviews: list[RakumaReview] = Field(default_factory=list, description="评价明细列表")
|
||||||
|
|
||||||
|
|
||||||
class RakumaShopItemsData(BaseModel):
|
class RakumaShopItemsData(BaseModel):
|
||||||
"""ラクマ 卖家商品列表数据"""
|
"""ラクマ 卖家商品列表数据"""
|
||||||
|
|
||||||
shop_id: str = ""
|
shop_id: str = Field(default="", description="店铺页 hash")
|
||||||
shop_name: str = ""
|
shop_name: str = Field(default="", description="店铺名")
|
||||||
page: int = 1
|
page: int = Field(default=1, description="当前页码")
|
||||||
total_count: int = 0 # 该卖家的商品总数
|
total_count: int = Field(default=0, description="该卖家的商品总数")
|
||||||
has_more: bool = False
|
has_more: bool = Field(default=False, description="是否还有下一页")
|
||||||
request_url: str = ""
|
request_url: str = Field(default="", description="实际抓取的页面地址,便于排查")
|
||||||
items: list[RakumaSearchItem] = Field(default_factory=list)
|
items: list[RakumaSearchItem] = Field(default_factory=list, description="本页商品列表")
|
||||||
|
|||||||
+8
-5
@@ -16,7 +16,7 @@ from typing import Any, Generic, TypeVar
|
|||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
|
||||||
from app.shared.errors import AppError, AuthenticationError
|
from app.shared.errors import AppError, AuthenticationError
|
||||||
@@ -29,10 +29,13 @@ T = TypeVar("T")
|
|||||||
class ApiResponse(BaseModel, Generic[T]):
|
class ApiResponse(BaseModel, Generic[T]):
|
||||||
"""统一 API 响应格式"""
|
"""统一 API 响应格式"""
|
||||||
|
|
||||||
success: bool
|
success: bool = Field(description="请求是否成功")
|
||||||
msg: str
|
msg: str = Field(description="提示信息;失败时为错误原因")
|
||||||
data: T | None = None
|
data: T | None = Field(default=None, description="响应数据;无数据或失败时为 null")
|
||||||
code: int
|
code: int = Field(
|
||||||
|
description="错误码:0 表示成功,其余见统一错误码表(如 1001 鉴权失败、"
|
||||||
|
"1002 请求参数校验失败、5xxx 交易服务、6xxx 网关)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---- 依赖注入 ----
|
# ---- 依赖注入 ----
|
||||||
|
|||||||
+40
-33
@@ -26,43 +26,43 @@ class AuthSite(StrEnum):
|
|||||||
class AuthStatusRequest(BaseModel):
|
class AuthStatusRequest(BaseModel):
|
||||||
"""登录态查询请求"""
|
"""登录态查询请求"""
|
||||||
|
|
||||||
site: AuthSite | None = None # 不传则返回所有已配置站点
|
site: AuthSite | None = Field(default=None, description="站点标识;不传则返回所有已配置站点")
|
||||||
refresh: bool = True # 是否真实打一次请求探测;false 时只读缓存
|
refresh: bool = Field(default=True, description="是否真实打一次请求探测;false 时只读缓存")
|
||||||
|
|
||||||
|
|
||||||
class AuthSiteStatus(BaseModel):
|
class AuthSiteStatus(BaseModel):
|
||||||
"""单站登录态"""
|
"""单站登录态"""
|
||||||
|
|
||||||
site: str
|
site: str = Field(description="站点标识(rakuten)")
|
||||||
state_file_exists: bool # 是否已跑过 scripts/login.py
|
state_file_exists: bool = Field(description="是否已跑过 scripts/login.py(本地登录态文件存在)")
|
||||||
logged_in: bool | None # None 表示尚未探测
|
logged_in: bool | None = Field(description="是否已登录;null 表示尚未探测")
|
||||||
checked_age_seconds: float | None = None
|
checked_age_seconds: float | None = Field(default=None, description="上次探测距现在的秒数;未探测为 null")
|
||||||
detail: str = ""
|
detail: str = Field(default="", description="补充说明(如未登录原因)")
|
||||||
|
|
||||||
|
|
||||||
class AuthStatusData(BaseModel):
|
class AuthStatusData(BaseModel):
|
||||||
"""登录态查询响应"""
|
"""登录态查询响应"""
|
||||||
|
|
||||||
sites: list[AuthSiteStatus] = Field(default_factory=list)
|
sites: list[AuthSiteStatus] = Field(default_factory=list, description="各站点登录态列表")
|
||||||
|
|
||||||
|
|
||||||
class AuthReloadRequest(BaseModel):
|
class AuthReloadRequest(BaseModel):
|
||||||
"""重新加载登录态请求(人工登录完成后调用,免重启服务)"""
|
"""重新加载登录态请求(人工登录完成后调用,免重启服务)"""
|
||||||
|
|
||||||
site: AuthSite | None = None # 不传则重载所有已配置站点
|
site: AuthSite | None = Field(default=None, description="站点标识;不传则重载所有已配置站点")
|
||||||
|
|
||||||
|
|
||||||
class AuthReloadData(BaseModel):
|
class AuthReloadData(BaseModel):
|
||||||
"""重新加载登录态响应"""
|
"""重新加载登录态响应"""
|
||||||
|
|
||||||
reloaded: dict[str, int] = Field(default_factory=dict) # site -> cookie 条数
|
reloaded: dict[str, int] = Field(default_factory=dict, description="重载结果:站点 -> cookie 条数")
|
||||||
sites: list[AuthSiteStatus] = Field(default_factory=list)
|
sites: list[AuthSiteStatus] = Field(default_factory=list, description="重载后的各站点登录态")
|
||||||
|
|
||||||
|
|
||||||
class AuthLoginRequest(BaseModel):
|
class AuthLoginRequest(BaseModel):
|
||||||
"""触发自动登录请求"""
|
"""触发自动登录请求"""
|
||||||
|
|
||||||
site: AuthSite | None = None # 不传则对所有已配置站点各跑一次
|
site: AuthSite | None = Field(default=None, description="站点标识;不传则对所有已配置站点各跑一次")
|
||||||
|
|
||||||
|
|
||||||
class AuthLoginData(BaseModel):
|
class AuthLoginData(BaseModel):
|
||||||
@@ -73,9 +73,11 @@ class AuthLoginData(BaseModel):
|
|||||||
不会白起一次浏览器。
|
不会白起一次浏览器。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logged_in: bool
|
logged_in: bool = Field(description="本次动作的最终结论:所有涉及站点都登录着才为 true")
|
||||||
relogin_attempted: dict[str, bool] = Field(default_factory=dict)
|
relogin_attempted: dict[str, bool] = Field(
|
||||||
sites: list[AuthSiteStatus] = Field(default_factory=list)
|
default_factory=dict, description="各站点是否真的跑了登录流程;已登录的站点直接跳过"
|
||||||
|
)
|
||||||
|
sites: list[AuthSiteStatus] = Field(default_factory=list, description="各站点登录态")
|
||||||
|
|
||||||
|
|
||||||
class TradingHealthData(BaseModel):
|
class TradingHealthData(BaseModel):
|
||||||
@@ -85,8 +87,8 @@ class TradingHealthData(BaseModel):
|
|||||||
POST /api/auth/status。
|
POST /api/auth/status。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status: str
|
status: str = Field(description="服务状态,健康为 ok")
|
||||||
auth: dict[str, Any] = Field(default_factory=dict)
|
auth: dict[str, Any] = Field(default_factory=dict, description="缓存的各站点登录态(不触发网络探测)")
|
||||||
|
|
||||||
|
|
||||||
# ---- 购物车接口(/api/cart/*)----
|
# ---- 购物车接口(/api/cart/*)----
|
||||||
@@ -100,10 +102,15 @@ class CartAddRequest(BaseModel):
|
|||||||
choice 接受字符串("颜色:赤")或字符串列表(["颜色:赤", "サイズ:M"])。
|
choice 接受字符串("颜色:赤")或字符串列表(["颜色:赤", "サイズ:M"])。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item_url: str
|
item_url: str = Field(description="商品页 URL")
|
||||||
quantity: int = 1
|
quantity: int = Field(default=1, description="加购数量")
|
||||||
variant_id: str | None = None
|
variant_id: str | None = Field(
|
||||||
choice: str | list[str] | None = None
|
default=None, description="多规格商品的规格 ID;不传时自动选第一个非售罄规格"
|
||||||
|
)
|
||||||
|
choice: str | list[str] | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="商品选项,如 \"颜色:赤\";可传字符串或字符串列表,不传时必填选项自动选第一个候选值",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CartStatusRequest(BaseModel):
|
class CartStatusRequest(BaseModel):
|
||||||
@@ -117,35 +124,35 @@ class CartClearRequest(BaseModel):
|
|||||||
class CartRemoveRequest(BaseModel):
|
class CartRemoveRequest(BaseModel):
|
||||||
"""删除指定商品请求"""
|
"""删除指定商品请求"""
|
||||||
|
|
||||||
item_id: str
|
item_id: str = Field(description="要删除的商品 ID")
|
||||||
|
|
||||||
|
|
||||||
class CartAddData(BaseModel):
|
class CartAddData(BaseModel):
|
||||||
"""加购响应数据"""
|
"""加购响应数据"""
|
||||||
|
|
||||||
added: bool = True
|
added: bool = Field(default=True, description="是否加购成功")
|
||||||
item_id: str
|
item_id: str = Field(description="加购的商品 ID")
|
||||||
shop_bid: str
|
shop_bid: str = Field(description="店铺 bid")
|
||||||
cart_count: int # -1 表示加购成功但末尾 count 查询失败
|
cart_count: int = Field(description="加购后的购物车商品件数;-1 表示加购成功但末尾 count 查询失败")
|
||||||
|
|
||||||
|
|
||||||
class CartStatusData(BaseModel):
|
class CartStatusData(BaseModel):
|
||||||
"""购物车状态响应数据"""
|
"""购物车状态响应数据"""
|
||||||
|
|
||||||
logged_in: bool
|
logged_in: bool = Field(description="当前是否已登录")
|
||||||
count: int
|
count: int = Field(description="购物车商品件数(空车为 0)")
|
||||||
raw_status: str # 站点状态码字符串,"100" 表示正常
|
raw_status: str = Field(description="站点 cart count API 的状态码字符串,\"100\" 表示正常、\"101\" 表示空车")
|
||||||
|
|
||||||
|
|
||||||
class CartClearData(BaseModel):
|
class CartClearData(BaseModel):
|
||||||
"""清空购物车响应数据"""
|
"""清空购物车响应数据"""
|
||||||
|
|
||||||
removed_count: int # 实际点击「削除」按钮的次数
|
removed_count: int = Field(description="实际点击「削除」按钮的次数")
|
||||||
cart_count: int # -1 表示末尾 count 查询失败
|
cart_count: int = Field(description="清空后的购物车商品件数;-1 表示末尾 count 查询失败")
|
||||||
|
|
||||||
|
|
||||||
class CartRemoveData(BaseModel):
|
class CartRemoveData(BaseModel):
|
||||||
"""删除指定商品响应数据"""
|
"""删除指定商品响应数据"""
|
||||||
|
|
||||||
removed: bool # 末尾 cart HTML 已不含 item_id 时为 true
|
removed: bool = Field(description="末尾 cart HTML 已不含该 item_id 时为 true")
|
||||||
item_id: str
|
item_id: str = Field(description="被删除的商品 ID")
|
||||||
|
|||||||
Reference in New Issue
Block a user