×

《1688商品API对接实录:alibaba.1688.item.* 与 offer.get 的边界与字段映射踩坑》(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-09-29 09:55:55 浏览32 评论0

抢沙发发表评论

《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️⃣ 价格不是一个数

1688 至少有 4 层价:
  • 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
下游要自动:下架商品 → 停止采购 → 标记“仅售库存”或下架 listing

四、统一商品归一化模型(核心代码)

# 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 那边才能拿到“能卖、能赚、能下单、能售后”的商品。


群贤毕至

访客