加 /api/cart/* 接口;加购链路完全归 trading
trading 新增 4 条购物车接口(POST /api/cart/{add,status,clear,remove}),
全部 Bearer 鉴权、走 SiteInteractor(Playwright + storage_state)。同步把
SiteInteractor 从 gateway URL 解耦——lifespan 总是构造与启停,container
字段 worker_site → site,加 asyncio.Lock 让 HTTP 与 worker 共用同一把锁
(同账号串行硬约束)。clear/remove 用 UI 点击 button[aria-label="削除"],
探针回报这是稳定 selector;真账号实测前先用此路径。
抽 ichiba 加购字段解析到 app/shared/purchase_contract.py(常量 +
inventory_flag_for + basket_domain_of + base_form_fields),原本 scraping
与 trading 重复实现同一段 __INITIAL_STATE__.purchase 解析。进一步发现
README 写的「purchase 块是两服务契约」实际未落地——trading 必须 Playwright
开页(httpx 被 TLS 指纹拦死),本地抽比再调 /api/item_detail 更快更新鲜。
删除 scraping 端 PurchaseInfo/PurchaseOption/PurchaseOptionValue 模型、
各站 _purchase_info 函数、tests/test_purchase.py。ItemDetailData 保留
purchase_condition / is_sold_out / purchase_unit / sku 等商品状态字段。
README「加购与下单」段重写。
328 测试全绿(含架构测试守住三方互不 import)。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,6 @@ from app.shared.errors import ScrapeParseError
|
||||
from app.scraping.models.scrape import (
|
||||
Breadcrumb,
|
||||
ItemDetailData,
|
||||
PurchaseInfo,
|
||||
PurchaseOption,
|
||||
PurchaseOptionValue,
|
||||
ReviewSummary,
|
||||
ShippingInfo,
|
||||
ShopSummary,
|
||||
@@ -31,12 +28,7 @@ from app.scraping.utils.coerce import as_dict, as_float, as_int, as_list, as_str
|
||||
# purchase.sellType 下表示「可正常购买」的状态值
|
||||
_PURCHASABLE_CONDITION = "enabled"
|
||||
|
||||
# 普通购买的事件标识,站点前端构造加购表单时固定带上
|
||||
_NORMAL_PURCHASE_EVENT = "ES01_003_001"
|
||||
|
||||
# 库存类型 → 加购表单里的 inventory_flag
|
||||
_INVENTORY_FLAG = {"multiple": 2}
|
||||
_DEFAULT_INVENTORY_FLAG = 1
|
||||
# 库存类型 → 加购表单里的 inventory_flag(常量与基础字段构造在 app.shared.purchase_contract)
|
||||
|
||||
|
||||
def _parse_attributes(raw: Any) -> list[SkuAttribute]:
|
||||
@@ -91,72 +83,6 @@ def _parse_variants(raw: Any) -> list[SkuVariant]:
|
||||
return variants
|
||||
|
||||
|
||||
def parse_purchase_options(raw: Any) -> list[PurchaseOption]:
|
||||
"""解析商品选项(選択肢)
|
||||
|
||||
结构为 {id, name, type: select|check|text, isRequired, values:[{id, name}]};
|
||||
type=text 的选项没有候选值,由买家自由填写(如刻字内容)。
|
||||
"""
|
||||
options: list[PurchaseOption] = []
|
||||
for option in as_list(raw):
|
||||
if not isinstance(option, dict):
|
||||
continue
|
||||
options.append(
|
||||
PurchaseOption(
|
||||
option_id=str(as_int(option.get("id"))) if option.get("id") is not None else "",
|
||||
name=as_str(option.get("name")),
|
||||
type=as_str(option.get("type")),
|
||||
is_required=bool(option.get("isRequired")),
|
||||
values=[
|
||||
PurchaseOptionValue(
|
||||
value_id=str(as_int(value.get("id"))) if value.get("id") is not None else "",
|
||||
name=as_str(value.get("name")),
|
||||
)
|
||||
for value in as_list(option.get("values"))
|
||||
if isinstance(value, dict)
|
||||
],
|
||||
)
|
||||
)
|
||||
return options
|
||||
|
||||
|
||||
def _purchase_info(
|
||||
*,
|
||||
sell_type: dict[str, Any],
|
||||
raw_sku: dict[str, Any],
|
||||
information: dict[str, Any],
|
||||
shop_id: int,
|
||||
item_id: str,
|
||||
item_variant_id: str,
|
||||
) -> PurchaseInfo:
|
||||
"""组装加购所需的端点与字段
|
||||
|
||||
字段名与取值来自站点前端构造加购表单的逻辑(getPurchaseFormData)。
|
||||
basketDomain 逐商品不同(不同店铺落在不同的 basket 集群),不能写死。
|
||||
"""
|
||||
inventory_flag = _INVENTORY_FLAG.get(as_str(raw_sku.get("inventoryType")), _DEFAULT_INVENTORY_FLAG)
|
||||
form_fields = {
|
||||
"shop_bid": str(shop_id),
|
||||
"item_id": item_id,
|
||||
"inventory_flag": str(inventory_flag),
|
||||
"__event": _NORMAL_PURCHASE_EVENT,
|
||||
}
|
||||
# 单一库存商品的规格是固定的,直接填好,调用方无需再选
|
||||
if inventory_flag == _DEFAULT_INVENTORY_FLAG and item_variant_id:
|
||||
form_fields["variant_id"] = item_variant_id
|
||||
|
||||
options = parse_purchase_options(information.get("options"))
|
||||
return PurchaseInfo(
|
||||
cart_url=as_str(sell_type.get("basketDomain")),
|
||||
form_fields=form_fields,
|
||||
quantity_field="units",
|
||||
variant_field="variant_id",
|
||||
options_field="choice" if options else "",
|
||||
options=options,
|
||||
has_required_options=any(option.is_required for option in options),
|
||||
)
|
||||
|
||||
|
||||
def _pick_sell_type(sell_type: dict[str, Any]) -> dict[str, Any]:
|
||||
"""取售卖方式信息,优先普通购买,其次任意一种带价格的方式(如定期购)"""
|
||||
normal = as_dict(sell_type.get("normalPurchase"))
|
||||
@@ -270,12 +196,4 @@ def parse_item_detail(
|
||||
breadcrumbs=breadcrumbs,
|
||||
shipping=shipping,
|
||||
sku=sku,
|
||||
purchase=_purchase_info(
|
||||
sell_type=sell_type,
|
||||
raw_sku=raw_sku,
|
||||
information=as_dict(purchase.get("information")),
|
||||
shop_id=as_int(shop_information.get("shopId")),
|
||||
item_id=item_id,
|
||||
item_variant_id=as_str(item.get("variantId")),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user