🔧 避坑实录:京东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值是紧凑JSON(separators=(',',':'),中文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刷新) 吗?