docs(models): 模型字段行尾注释迁移为 Field(description),补全 OpenAPI 字段说明

This commit is contained in:
2026-08-17 09:43:07 +08:00
parent f31e127ba7
commit 6b6b243132
4 changed files with 422 additions and 362 deletions
+48 -30
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")