×

🔧 避坑实录:京东API接入最常见的6个错误(签名/授权/限流/企业认证)(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-07-08 09:35:34 浏览25 评论0

抢沙发发表评论

🔧 避坑实录:京东API接入最常见的6个错误(签名/授权/限流/企业认证)(附Python源码)

直接给结论:京东JOS接入 90%的坑集中在签名格式、秒级时间戳、订单接口需企业+卖家Token、QPS超限、字段未传、沙箱误判。下面逐条给现象→原因→解决→可运行自检代码。

一、六大常见错误速查表

#
现象
真实原因
解决
Invalid Signature/ sign fail
签名错:毫秒时间戳、空值参入、360buy_param_json含空格、未剔除sign
见签名自检
403 no permission/ invalid method调订单
个人应用查订单 / 接口未申请
企业实名自用型应用+申请jingdong.pop.order.search+传卖家AccessToken
code=16/ ISP_FLOW_CONTROL_LIMIT/ HTTP 429
QPS超免费上限(个人≈1~2/s 企业≈5~10/s)
令牌桶限速 QPS≤免费上限×0.8 + 退避重试
skus_json空 / stockNum=0查自己店铺商品
公开查询不返SKU库存/商家编码
卖家 AccessToken(OAuth授权)再查自己店铺
timestamp invalid/ IllegalParam
毫秒(1688习惯) 错
JOS用秒级 int(time.time())
沙箱返回空订单/ mock商品 → 以为代码错
沙箱只验签不返真数据
切生产网关验证;沙箱能通=签名OK

二、Python:签名自检 + 全错误分类捕获 Client

# jd_troubleshoot.py
"""
京东JOS 六大坑 自检工具
1. sign_debug() → 验证签名算法(坑①)
2. Top level call → 捕获 权限/限流/token/sandbox 提示(坑②③④⑤⑥)
"""
import hashlib
import json
import requests
import time
from typing import Dict, Optional

封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# ─────────────── ① 签名自检 ───────────────
def sign_debug(params: Dict, app_secret: str):
    """
    打印参与签名的KV和最终sign,与你代码中生成的sign比对
    params: 不含sign的所有API参数(含 timestamp/app_key/method/360buy_param_json)
    """
    filtered = {k: v for k, v in params.items()
                if v is not None and str(v).strip() != '' and k != 'sign'}
    sorted_items = sorted(filtered.items(), key=lambda x: x[0])
    qs = ''.join(f"{k}{v}" for k, v in sorted_items)
    sign_str = f"{app_secret}{qs}{app_secret}"
    sign = hashlib.md5(sign_str.encode()).hexdigest().upper()

    print("=" * 52)
    print("🔍 JOS Sign 调试")
    print("=" * 52)
    for k, v in sorted_items:
        print(f"  {k} = {v[:80] if len(str(v))<80 else v[:30]+'...'}")
    print(f"\n待签名串(len={len(sign_str)}) 首尾已拼AppSecret")
    print(f"✅ 计算 sign = {sign}")
    print("=" * 52)
    return sign


# ─────────────── JOS Client(带错误分类) ───────────────
class JdTroubleClient:
    GW = "https://api.jd.com/routerjson"

    def __init__(self, ak, ask, sandbox=False):
        self.ak, self.ask = ak, ask
        self.gw = ("https://api.sandbox.jd.com/routerjson"
                    if sandbox else self.GW)

    def _sign(self, p: Dict) -> str:
        filt = sorted((k, v) for k, v in p.items()
                       if v is not None and str(v).strip() != '' and k != 'sign')
        qs = ''.join(f"{k}{v}" for k, v in filt)
        return hashlib.md5(f"{self.ask}{qs}{self.ask}".encode()
                          ).hexdigest().upper()

    def _call(self, method, biz, token=None):
        p = {
            "app_key": self.ak, "method": method,
            "timestamp": str(int(time.time())),   # ← 坑⑤ 秒级!
            "format": "json", "v": "2.0", "sign_method": "md5",
            "360buy_param_json": json.dumps(biz, ensure_ascii=False,
                                             separators=(',', ':'))
        }
        if token:
            p["access_token"] = token
        p["sign"] = self._sign(p)

        r = requests.post(self.gw, data=p, timeout=15)
        r.raise_for_status()
        d = r.json()

        if "error_response" in d:
            err = d["error_response"]
            code = str(err.get("code", ""))
            sub = err.get("sub_code", "")
            msg = err.get("zh_desc") or err.get("en_desc", "")

            # ── 分类提示 ──
            if "no permission" in msg or "invalid method" in msg:
                hint = ("❌ 【坑② 无权限】\n"
                        "   • 订单/库存接口 → 须企业实名应用+申请权限\n"
                        "   • session须是【卖家】OAuth AccessToken(非买家)\n"
                        "   • 商品公开字段可不传token")
            elif self._is_flow(err):
                hint = ("⚠️  【坑③ QPS超限】降低QPS(令牌桶)后重试,或买资源包\n"
                       f"   原始: {code} {msg}")
            elif "sign" in msg.lower() or "invalid sign" in msg.lower():
                hint = ("❌ 【坑① 签名错误】运行 sign_debug() 对比\n"
                       "   ✓ 秒级timestamp ✓ 空值剔除 ✓ 360buy_param_json紧凑JSON ✓ sign不参入")
            elif "session" in msg.lower() or "token" in msg.lower():
                hint = ("❌ 【坑④ 缺/过期 session】订单类须传卖家AccessToken\n"
                       "   用 OAuth2 刷新 token(expire前7天)")
            elif "limited-by-app-access-count" in sub:
                hint = ("🚨 【日免费额度耗尽】超量按基础API计费\n"
                       "   → 增量同步(fields过滤) + 申请提额 + 断点续跑")
            elif "timestamp" in msg.lower():
                hint = ("❌ 【坑⑤ timestamp格式】JOS用秒级10位 int(time.time())")
            else:
                hint = f"❌ JOS [{code}] {msg} sub={sub}"
            raise Exception(hint)

        resp_key = method.replace(".", "_") + "_response"
        if resp_key not in d:
            for k in d:
                if k.endswith("_response"):
                    resp_key = k
                    break
        return d.get(resp_key, d)

    @staticmethod
    def _is_flow(err_dict):
        s = str(err_dict)
        return any(k in s for k in ["ISP_FLOW_CONTROL", "FLOW_CONTROL",
                                     "code=16", "429"])

    # ─── 示例:商品详情 ───
    def test_ware(self, ware_id, token=None):
        return self._call(
            "jingdong.ware.read.get",
            {"wareId": ware_id,
             "fields": "ware_id,title,price,jd_price,stock_num,skus_json"},
            token=token
        ).get("ware", {})

    # ─── 示例:订单列表(需token) ───
    def test_orders(self, token, minutes=30):
        from datetime import datetime, timedelta
        now = datetime.now()
        s = (now - timedelta(minutes=minutes)).strftime("%Y-%m-%d %H:%M:%S")
        e = now.strftime("%Y-%m-%d %H:%M:%S")
        return self._call(
            "jingdong.pop.order.search",
            {"start_modified": s, "end_modified": e,
             "order_state": "WAIT_SELLER_STOCK_OUT",
             "page": 1, "page_size": 10},
            token=token
        )

封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# ======================= 运行自检 =======================
if __name__ == "__main__":
    AK = "YOUR_JD_APP_KEY"
    ASK = "YOUR_JD_APP_SECRET"

    # ---- ① 签名调试(粘真实参数看sign)----
    print("\n>>> [自检①] 签名算法验证")
    sign_debug(
        {
            "method": "jingdong.ware.read.get",
            "app_key": AK,
            "timestamp": str(int(time.time())),
            "format": "json",
            "v": "2.0",
            "sign_method": "md5",
            "360buy_param_json": json.dumps(
                {"wareId": "100012345678",
                 "fields": "ware_id,title,price,jd_price"},
                ensure_ascii=False, separators=(',', ':'))
        },
        ASK
    )

    # ---- ② 接口连通性 ----
    print("\n>>> [自检②] 商品详情(不需token,个人/企业均可)")
    cli = JdTroubleClient(AK, ASK, sandbox=False)
    try:
        ware = cli.test_ware("100012345678")   # ← 替换真实 wareId
        print(f"✅ 连通成功!标题: {ware.get('title')} ¥{ware.get('jd_price')}")
    except Exception as e:
        print(e)

    # ---- ③ 订单测试(需填真实卖家 token)----
    # SELLER_TOKEN = "SELLER_ACCESS_TOKEN"
    # try:
    #     cli.test_orders(SELLER_TOKEN)
    #     print("✅ 订单接口正常(企业+授权OK)")
    # except Exception as e:
    #     print(e)

三、六个坑详细说明

❌ 坑① 签名错误(Invalid Signature)

  • 必须:参数按 key ASCII 升序 → 拼 key+value→ 首尾 AppSecret→ MD5 → 大写

  • 360buy_param_json值是紧凑JSONseparators=(',',':'),中文 ensure_ascii=False

  • 剔除 sign本身、值为 None/""

  • 用上面 sign_debug()直接对比

❌ 坑② 403 no permission(订单接口)

  • jingdong.pop.order.search/ jd.order.detail.get/ jingdong.stock.get
    必须企业实名应用 + 接口已申请 + 传卖家 OAuth AccessToken

  • 商品公开字段(jingdong.ware.read.get不传 token)个人企业都能查

❌ 坑③ QPS超限(code=16 / 429)

  • 个人默认≈1~2/s,企业≈5~10/s

  • Client 加令牌桶 QPS=4(免费5时)或 QPS=8(免费10时),遇限流指数退避重试(代码已分类打印)

❌ 坑④ skus_json 空 / stockNum=0

  • 别人店铺商品:公开API不返回库存/商家编码 → 正常

  • 自己店铺商品:必须传 access_token=卖家AccessToken,否则同样被裁减

❌ 坑⑤ timestamp invalid

# ✅ 正确
"timestamp": str(int(time.time()))      # 秒级 10位
# ❌ 错误(1688习惯毫秒会报 Invalid Timestamp)
"timestamp": str(int(time.time()*1000))

❌ 坑⑥ 沙箱返回空/ mock 以为代码错

  • 沙箱仅验签名,不返回真实订单/真实商品库存

  • 商品接口沙箱会把 wareId原样回显 title 为 mock → 签名通=OK,切生产看真数据


四、面试/排错一句话

JOS接入六坑——签名字典排序+秒级时间戳+紧凑JSON+空值剔除;订单类需企业应用+卖家OAuth token;QPS超限令牌桶退避;skus空确认是否传了session且是自己店铺;沙箱空数据是正常Mock不是Bug。
需要我补 京东OAuth2授权码→AccessToken完整Python代码APScheduler订单增量同步(断点续跑+Token刷新) 吗?


群贤毕至

访客