《1688商品API对接实录:alibaba.1688.item.* 与 offer.get 的边界与字段映射踩坑》(附Python源码)
1688 商品域最容易被搞混的是:“offer / item / product / sku” 不是同一样东西。老一代叫alibaba.offer.*(供货要约视角),新一代商品域叫alibaba.product.*/alibaba.item.*(商品主数据视角)。做 ERP 时别迷信“一个接口拿全量”:基础信息、价格、库存、SKU、供应商、分销价通常分散在不同接口,而且 B2B 的“价格/库存”天然不是一口价。
一、接口边界:offer / item / product 到底谁是谁
视角 | 典型 method | 返回重点 | 适合场景 |
|---|---|---|---|
Offer(供货要约) | alibaba.offer.get / alibaba.offer.search / alibaba.offer.price.get | 批发价、MOQ、混批、供货状态、店铺关系 | 老 B2B 采购/选品 |
Item / Product(商品主数据) | alibaba.product.get / alibaba.product.batch.get / alibaba.item.get(不同文档有混用) | 标题、类目、主图、详情、SKU 结构、规格属性 | 商品档案、跨境刊登、SKU 映射 |
Price(价格专用) | alibaba.offer.price.get / alibaba.product.price.get | 阶梯价、起批量、会员价、代发价 | 成本核算 |
Stock(库存专用) | alibaba.product.stock.get / 订单预览 | SKU 可售库存、占用库存 | 防超卖 |
Distribution(分销) | alibaba.cpsMedia.productInfo / 严选 flow | retailPrice、佣金、代发支持 | 一件代发 |
⚠️ 网上很多教程把1688.item_get/alibaba.item.get/alibaba.offer.get当成同一个接口,这是最大误解。不同权限包、国内站/国际站、老应用/新应用拿到的字段结构不一样。
二、ID 体系先钉死(不然映射全乱)
offerId / itemId / productId ≈ 1688 侧“这个供货商品”的主键 skuId / specId ≈ 某个规格组合(颜色+尺寸+材质) outerId / cargoNumber ≈ 商家自己写的货号,跨系统对账用 shopId / supplierId ≈ 供应商,不是商品
下单用 offerId + skuId,不是 title
映射用 offerId,别用短链/分享页 URL
outerId 不可信唯一:不同供应商可能写一样货号
SKU 没 spec 组合也要有默认 SKU,否则下游平台建 SKU 会炸
三、字段映射踩坑清单(生产级)
1️⃣ 价格不是一个数
price:展示/起批参考价(别直接上架)priceRanges/priceRange:阶梯价[{startQuantity:10,price:18}, {startQuantity:100,price:15}]consignPrice/retailPrice:代发/分销基准价会员价 / 活动价:游客态看不到
inner_price_min # 最低起批价 inner_price_max # 最高档单价 inner_moq # 最小起订量 inner_consign_price # 代发价 inner_price_tiers # 阶梯明细
2️⃣ 库存字段名会变
amountOnSale:可售库存(优先)sku_stock:展示库存(可能是快照)available_stock:部分接口才有999999/-1:模糊库存/不公开
有 sku.amountOnSale → 用 amountOnSale 无 → 标 unknown_stock -1 / 999999 → 不进“可售”计算
3️⃣ SKU spec 是“属性数组”,不是字符串
spec = "白色-L"
spec = [{"prop_name": "颜色", "value": "白色"},
{"prop_name": "尺码", "value": "L"}]fingerprint = "颜色:白色|尺码:L"
4️⃣ 单位/起订量会坑死零售上架
unit=件/箱/套/米moq=10但下游平台允许买 1 件不处理 → 代发下单失败 / 利润算反
sell_unit # 件/箱/套 moq # 1688 侧最小买几 downstream_min # 闲鱼/Mercari 侧最小卖几 lot_size # 1 箱=多少件
5️⃣ 图片有防盗链 + 域名会变
cbu01.alicdn.com/img.alicdn.com都可能跨境刊登要下载转存对象存储,别直链
详情 HTML 里
<img>也要抽出来重传
6️⃣ 下架/删除不抛错
返回空
data或
status=deleted/off_sale或价格字段全空但标题还在
published → off_sale → deleted
四、统一商品归一化模型(核心代码)
# ali1688/item_normalizer.py
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class InnerSku:
sku_id: str
spec_fp: str # 颜色:白|尺码:L
price: float
moq: int = 1
amount_on_sale: Optional[int] = None
consign_price: Optional[float] = None
outer_id: str = ""
@dataclass
class InnerProduct:
offer_id: str
title: str
category_id: str = ""
main_images: list[str] = field(default_factory=list)
detail_html: str = ""
moq: int = 1
sell_unit: str = "件"
lot_size: int = 1
price_min: float = 0.0
price_max: float = 0.0
consign_price: Optional[float] = None
price_tiers: list[dict] = field(default_factory=list)
skus: list[InnerSku] = field(default_factory=list)
supplier_id: str = ""
status: str = "published" # published/off_sale/deleted
support_drop_ship: bool = False
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class Ali1688ItemNormalizer:
@staticmethod
def _spec_fingerprint(spec_attrs: list[dict]) -> str:
parts = []
for p in spec_attrs:
name = p.get("prop_name") or p.get("name") or ""
val = p.get("value") or ""
if name or val:
parts.append(f"{name}:{val}")
return "|".join(sorted(parts))
@staticmethod
def _safe_float(v, default=0.0) -> float:
try:
return float(v)
except (TypeError, ValueError):
return default
@classmethod
def from_offer_raw(cls, raw: dict) -> InnerProduct:
# 老 offer.get 结构(示意,真实字段以权限包为准)
result = raw.get("result", raw)
item = result.get("item", result)
skus = []
for s in item.get("sku_list", item.get("skus", [])):
spec_attrs = s.get("sku_properties") or s.get("spec_attributes") or []
skus.append(InnerSku(
sku_id=str(s.get("sku_id") or s.get("skuId") or ""),
spec_fp=cls._spec_fingerprint(spec_attrs),
price=cls._safe_float(s.get("price")),
moq=cls._safe_float(s.get("min_order_quantity"), 1),
amount_on_sale=s.get("amount_on_sale") if s.get("amount_on_sale") not in (-1, 999999) else None,
consign_price=cls._safe_float(s.get("consign_price")) or None,
outer_id=s.get("outer_id") or s.get("cargoNumber") or "",
))
tiers = []
for t in item.get("price_ranges", item.get("priceRange", [])):
tiers.append({
"start_qty": int(t.get("startQuantity", t.get("start_quantity", 1))),
"price": cls._safe_float(t.get("price")),
})
prices = [s.price for s in skus if s.price] + [cls._safe_float(item.get("price"))]
return InnerProduct(
offer_id=str(item.get("offerId") or item.get("item_id") or item.get("productId")),
title=item.get("title", ""),
category_id=str(item.get("categoryId") or item.get("cat_id", "")),
main_images=item.get("pics") or item.get("main_images") or [],
detail_html=item.get("desc", ""),
moq=int(cls._safe_float(item.get("min_order_quantity"), 1)),
sell_unit=item.get("sell_unit") or item.get("unit") or "件",
lot_size=int(cls._safe_float(item.get("batchNumber"), 1)),
price_min=min(prices) if prices else 0.0,
price_max=max(prices) if prices else 0.0,
consign_price=cls._safe_float(item.get("consignPrice")) or None,
price_tiers=tiers,
skus=skus,
supplier_id=str(item.get("supplier_id") or item.get("shop_id", "")),
status="off_sale" if item.get("status") in ("off_sale", "deleted") else "published",
support_drop_ship=bool(item.get("supportOnlineTrade") or item.get("drop_ship")),
)五、item.* / offer.* 调用策略(别乱刷)
# 1. 列表/搜索:offer.search / product.search # 2. 单品主数据:product.get / item.get # 3. 价格变更:offer.price.get / product.price.get # 4. 库存:product.stock.get + 下单前 createOrder.preview # 5. 分销价:cpsMedia.productInfo(不是商品主数据接口)
商品搜索:~10 QPS
SKU/价格:更低,别全量轮询
用
gmtModified做增量本地建“商品主数据表 + SKU 池 + 价格快照表”
六、和前几篇串联
《1688 全链路》:本文是“采购前主数据”那一环
《统一采购适配层》:
offerId + skuId就是采购绑定的主键《两套接口边界》:1688 商品采集 ≠ 1688 代发下单;
item.get只读,fenxiaoOrder.create才写《六大坑》:这里新增——价格多层、库存快照、spec 数组、单位混用、下架不报错
七、上线前检查表
[ ] 不把
price当代发价/零售价[ ] SKU 用 spec 指纹,不用“白色-L”字符串
[ ] 库存优先
amountOnSale,屏蔽-1/999999[ ] 单位/起订量/箱规进内部模型
[ ] 图片转存对象存储,不直链 alicdn
[ ] 下架商品进状态机,不停采
[ ] offerId 做幂等主键,outerId 只做辅助
[ ] 商品/价格/库存/分销价分接口取,不指望一次拿全
八、一句话收口
1688 商品接口的真正难点,不是签名,而是:“它卖的是批发关系,不是零售商品。”你要把offer → sku → 阶梯价 → 可售库存 → 代发价 → 单位/箱规全部归一化后,闲鱼/Mercari 那边才能拿到“能卖、能赚、能下单、能售后”的商品。