×

《京东商品API:从 item.jd.get 迁移到 jingdong.item.read.get 的字段映射与踩坑》(附 Python 源码)

万邦科技Lex 万邦科技Lex 发表于2026-10-13 09:36:34 浏览26 评论0

抢沙发发表评论

《京东商品API:从 item.jd.get 迁移到 jingdong.item.read.get 的字段映射与踩坑》(附 Python 源码)

先拍结论:
京东这波迁移不是重构业务,而是“宙斯 JOS → 京东商家开放平台”的命名/入口收敛。
网关还是 https://api.jd.com/routerjson,签名还是 secret + kv(ASCII) + secret,AccessToken 逻辑不变;
真正要改的是三件事:
  1. method 从 item.jd.get / jd.item.get / jingdong.ware.get 换成 jingdong.item.read.get

  2. 返回根节点从 item_get_response / jingdong_ware_get_response 换成 jingdong_item_read_get_response

  3. 旧接口“单 SKU 平铺”,新接口“SPU + skuList + priceInfo + stockInfo”嵌套化

老代码直接 item["title"] 的,迁移后必炸。

一、老 vs 新:先对齐接口身份

维度
老 item.jd.get / jd.item.get
新 jingdong.item.read.get
平台
宙斯 JOS(已融合到 open.jd.com)
京东商家开放平台
定位
商品详情只读(旧命名)
商品基础信息只读(标准化)
网关
api.jd.com/routerjson
同上
签名
MD5(secret+kv+secret)
同上(一行不改)
入参
sku_id / item_id + fields
sku_id / sku_ids + fields
批量
不支持或各版本不一
支持 sku_ids,最多 20 个
返回根
item_get_response
jingdong_item_read_get_response
结构
平铺多
goodsInfoList / priceInfo / imageInfo / skuList / stockInfo
新应用申请
维护期,不鼓励
官方推荐
经验口径:
网关/签名/鉴权零改动,解析层重写,批量能力顺手用上。

二、旧接口“脑内模型”(为什么会写错)

老 item.jd.get 很多人写成:
{
  "item_get_response": {
    "item": {
      "sku_id": 100012345678,
      "title": "XX 笔记本",
      "price": 4999.00,
      "stock": 120,
      "image": "https://img10.360buyimg.com/..."
    }
  }
}
于是老代码:
item = resp["item_get_response"]["item"]
title = item["title"]
price = float(item["price"])
stock = int(item["stock"])

三、新接口真实结构(简化但接近生产)

{
  "jingdong_item_read_get_response": {
    "code": "0",
    "message": "success",
    "result": {
      "goodsInfoList": [
        {
          "skuId": "100012345678",
          "title": "XX 笔记本",
          "shortTitle": "轻薄本",
          "saleState": 1,
          "priceInfo": {
            "marketPrice": "5999.00",
            "jdPrice": "4999.00",
            "promotionPrice": "4799.00",
            "currency": "CNY"
          },
          "imageInfo": {
            "mainImg": "https://img10.360buyimg.com/n1/...",
            "imgList": ["https://img10...", "https://img10..."]
          },
          "brand": { "brandId": 123, "brandName": "Lenovo" },
          "category": {
            "cid1": 1, "cid1Name": "电脑办公",
            "cid2": 12, "cid2Name": "笔记本",
            "cid3": 123, "cid3Name": "轻薄本"
          },
          "stockInfo": {
            "stockNum": 120,
            "stockStatus": "available"
          },
          "salesInfo": {
            "totalSold": 3200,
            "commentCount": 1450,
            "goodRate": "97.2"
          },
          "skuList": [
            {
              "skuId": "10001234567801",
              "specText": "16G+512G",
              "price": "4999.00",
              "stock": 60
            },
            {
              "skuId": "10001234567802",
              "specText": "32G+1T",
              "price": "5799.00",
              "stock": 60
            }
          ]
        }
      ]
    }
  }
}
⚠️ 不同权限包返回的字段名会有差异:
  • 有的返回 jdPrice,有的返回 salePrice

  • 有的返回 stockNum,有的返回 stock

  • 有的返回 goodsInfoList,联盟接口返回 data

  • 下架商品 saleState=0,价格/库存可能直接为空

所以迁移第一原则:别信字段名,先打原始 JSON 建映射表。

四、字段映射表(ERP 落地用)

老 item.jd.get
新 jingdong.item.read.get
说明
item.sku_id
goodsInfoList[].skuId
最小销售单元
item.item_id / ware_id
SPU 需另查 / 用 sku 反推
京东强 SKU 模型
item.title
goodsInfoList[].title

item.sub_title
shortTitle
不一定有
item.price
priceInfo.jdPrice
当前京东价
item.market_price
priceInfo.marketPrice
划线价
item.promotion_price
priceInfo.promotionPrice
活动价
item.stock
stockInfo.stockNum / skuList[].stock
区域库存才准
item.img_url
imageInfo.mainImg
360buy 图链有防盗链
item.images
imageInfo.imgList

item.brand
brand.brandName

item.cid
category.cid3
三级类目
item.sku_attrs
skuList[].specText
“颜色:黑;尺码:XL”
item.desc
新接口常不返回
详情 HTML 要别的权限/接口
item.seller_nick
shopInfo.shopName
商家接口才有
item.outer_id
商家后台字段,新只读接口未必返
自营运配编码要店铺商品接口
跨平台统一模型里:
京东主键用 jd_<skuId>,不是 item_id;
1688 是 offerId+skuId,淘宝是 num_iid+sku_id,京东是 skuId + skuList[].skuId。

五、Python:兼容老新的客户端

# jd/client.py
import hashlib
import time
import requests

# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class JDClient:
    GATEWAY = "https://api.jd.com/routerjson"

    def __init__(self, app_key, app_secret, access_token=None, timeout=5):
        self.app_key = app_key
        self.app_secret = app_secret
        self.access_token = access_token
        self.timeout = timeout

    def _sign(self, params: dict) -> str:
        flat = {k: v for k, v in params.items() if v is not None and k != "sign"}
        qs = "".join(f"{k}{v}" for k, v in sorted(flat.items()))
        raw = f"{self.app_secret}{qs}{self.app_secret}"
        return hashlib.md5(raw.encode("utf-8")).hexdigest().upper()

    def execute(self, method: str, biz: dict):
        params = {
            "app_key": self.app_key,
            "method": method,
            "timestamp": str(int(time.time() * 1000)),
            "format": "json",
            "v": "2.0",
            "sign_method": "md5",
        }
        if self.access_token:
            params["access_token"] = self.access_token
        params.update(biz)
        params["sign"] = self._sign(params)

        resp = requests.post(self.GATEWAY, data=params, timeout=self.timeout)
        return resp.json()

六、Python:旧→新适配层(重点)

# jd/item_adapter.py
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class JDItem:
    sku_id: str
    spu_hint: Optional[str] = None
    title: Optional[str] = None
    short_title: Optional[str] = None
    market_price: Optional[float] = None
    sale_price: Optional[float] = None
    promo_price: Optional[float] = None
    main_img: Optional[str] = None
    img_list: list[str] = field(default_factory=list)
    brand_name: Optional[str] = None
    cid3: Optional[str] = None
    cid3_name: Optional[str] = None
    total_stock: Optional[int] = None
    on_sale: bool = True
    skus: list[dict] = field(default_factory=list)
    raw: dict = field(default_factory=dict)


def _f(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return None


def _i(x):
    try:
        return int(x)
    except (TypeError, ValueError):
        return None


def parse_legacy_item(resp: dict) -> JDItem:
    """老 item.jd.get"""
    item = (resp.get("item_get_response") or {}).get("item") or {}
    return JDItem(
        sku_id=str(item.get("sku_id") or item.get("skuId")),
        title=item.get("title"),
        market_price=_f(item.get("market_price")),
        sale_price=_f(item.get("price")),
        promo_price=_f(item.get("promotion_price")),
        main_img=item.get("img_url") or item.get("image"),
        total_stock=_i(item.get("stock")),
        skus=[{
            "sku_id": str(item.get("sku_id")),
            "spec": "",
            "price": _f(item.get("price")),
            "stock": _i(item.get("stock")),
        }],
        raw=item,
    )

# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def parse_new_item(resp: dict) -> JDItem:
    """新 jingdong.item.read.get"""
    node = resp.get("jingdong_item_read_get_response") or {}
    if str(node.get("code")) not in ("0", "", None) and "result" not in node:
        raise RuntimeError(node.get("message") or "jd item read error")

    goods_list = (
        (node.get("result") or {}).get("goodsInfoList") or []
    )
    if not goods_list:
        raise RuntimeError("empty goodsInfoList")

    g = goods_list[0]
    price_info = g.get("priceInfo") or {}
    stock_info = g.get("stockInfo") or {}
    image_info = g.get("imageInfo") or {}
    category = g.get("category") or {}

    return JDItem(
        sku_id=str(g.get("skuId")),
        title=g.get("title"),
        short_title=g.get("shortTitle"),
        market_price=_f(price_info.get("marketPrice")),
        sale_price=_f(price_info.get("jdPrice") or price_info.get("salePrice")),
        promo_price=_f(price_info.get("promotionPrice")),
        main_img=image_info.get("mainImg"),
        img_list=image_info.get("imgList") or [],
        brand_name=(g.get("brand") or {}).get("brandName"),
        cid3=str(category.get("cid3") or ""),
        cid3_name=category.get("cid3Name"),
        total_stock=_i(stock_info.get("stockNum") or stock_info.get("stock")),
        on_sale=g.get("saleState", 1) == 1,
        skus=[{
            "sku_id": str(s.get("skuId")),
            "spec": s.get("specText") or s.get("spec") or "",
            "price": _f(s.get("price")),
            "stock": _i(s.get("stock")),
        } for s in (g.get("skuList") or [])],
        raw=g,
    )
调用侧:
client = JDClient(APP_KEY, APP_SECRET, ACCESS_TOKEN)

# 老
legacy = parse_legacy_item(client.execute("item.jd.get", {"sku_id": sku}))
# 新
new = parse_new_item(client.execute("jingdong.item.read.get", {
    "sku_id": sku,
    "fields": "skuId,title,shortTitle,priceInfo,imageInfo,brand,category,stockInfo,skuList,salesInfo"
}))

七、批量迁移红利:别再单 SKU 狂打

def batch_get(client, sku_ids: list[str]):
    if len(sku_ids) > 20:
        raise ValueError("jingdong.item.read.get 单次最多 20 个")
    resp = client.execute("jingdong.item.read.get", {
        "sku_ids": ",".join(sku_ids),
        "fields": "skuId,title,priceInfo,stockInfo,imageInfo,skuList"
    })
    node = resp.get("jingdong_item_read_get_response") or {}
    return (node.get("result") or {}).get("goodsInfoList") or []
限流口径(社区经验):
  • 基础 2 QPS

  • 企业 5~50 QPS

  • 批量 20 个 ≠ 20 次免费,仍按平台计费/限流模型走

BUCKET = TokenBucket(rate=8)   # 企业 10 的话留余量

def safe_batch(skus):
    for chunk in chunks(skus, 20):
        BUCKET.acquire()
        yield batch_get(client, chunk)

八、迁移期双跑校验(很重要)

def migrate_check(sku):
    old = safe_parse(parse_legacy_item(client.execute("item.jd.get", {"sku_id": sku})))
    new = safe_parse(parse_new_item(client.execute("jingdong.item.read.get", {"sku_id": sku})))

    diffs = []
    if old.title != new.title:
        diffs.append(("title", old.title, new.title))
    if abs((old.sale_price or 0) - (new.sale_price or 0)) > 0.01:
        diffs.append(("price", old.sale_price, new.sale_price))
    if (old.total_stock or 0) != (new.total_stock or 0):
        diffs.append(("stock", old.total_stock, new.total_stock))
    return diffs
规则:
  • 标题/价格不一致 → 先查 area 区域价、查促销时间窗

  • 库存不一致 → 京东库存是区域可售库存,不传 area 默认北京

  • 老接口有、新接口没有(desc / outer_id / cost_price)→ 不是丢了,是权限域不同


九、区域库存坑(京东特有)

client.execute("jingdong.item.read.get", {
    "sku_id": sku,
    "area": "19_1607_3157_0",   # 省_市_区_镇
    "fields": "skuId,stockInfo,priceInfo"
})
不传 area:
  • 价格用默认区域

  • 库存用平台默认仓配视角

  • 跨境/家电/冷链会明显失真

淘宝/1688 很少强调“区域库存”,京东 POP/自营必须传 area。

十、和前几篇收口

  • 1688:offerId+skuId 采购视角,amountOnSale=-1/999999 不可信

  • 淘宝:num_iid 卖家视角,item.get 公开 / item.seller.get 私有

  • 京东:skuId 是王,jingdong.item.read.get 是商家/POP 只读视图;联盟 jd.union.open.goods.detail.query 是 CPS 视角,别混用

  • 迁移本质:京东不是“接口变了”,是“返回从平铺商品变成 SPU/SKU 价格库存对象树”

一句话:
老京东商品接口教你怎么“取一个商品”;
新 jingdong.item.read.get 教你怎么“理解一个京东 SPU:哪些 SKU、各区多少钱、哪区有货”。
迁移别只改 method,要把“商品=一条记录”的脑模型,升级成“商品=SPU 聚合 + SKU 集合 + 区域库存”。



群贤毕至

访客