×

🧾《京东订单API(jd.order.detail.get)对接ERP:企业认证+OAuth授权避坑指南》(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-07-03 15:48:18 浏览20 评论0

抢沙发发表评论

🧾《京东订单API(jd.order.detail.get)对接ERP:企业认证+OAuth授权避坑指南》(附Python源码)

直接说结论先:
京东订单类接口(jingdong.pop.order.search/ jd.order.detail.get/ jingdong.etms.waybill.send个人开发者应用无权限,必须:
  1. 企业支付宝/企业对公认证 创建「商家自用型应用」

  2. 申请订单相关接口权限(需填场景说明)

  3. 店铺卖家账号OAuth 2.0授权获取 access_token(session参数)

个人应用调会返回 403 no permission / invalid method,属正常限制。

一、现象对照表

你做的
返回
原因
个人应用 + jd.order.detail.get
403 no permission/ invalid method
个人号无订单接口权限
企业应用未申请接口
同上 403
控制台→API权限→申请 jingdong.pop.order.search/ jd.order.detail.get
传了买家 AccessToken
空/403
必须是店铺卖家 OAuth 换的 token
沙箱调订单
返回 mock/空
沙箱不支持真实订单,仅验签名
session 过期
Invalid access_token
refresh_token刷新

二、企业认证 + OAuth授权流程(关键!)

  1. 企业认证
    JOS控制台 → 账户管理 → 企业实名(营业执照 + 企业对公/企业支付宝)

  2. 创建应用
    应用类型选 「商家自用型应用」(ISV需额外软服中心入驻)

  3. 申请接口权限
    应用→接口权限→申请:
    📌 场景说明示例:"ERP系统同步本店铺已付款订单生成内部销售单,并回写发货物流,仅访问授权店铺数据"

    • jingdong.pop.order.search(订单列表)

    • jd.order.detail.get(订单明细)

    • jingdong.etms.waybill.send(发货回填运单)

    • jingdong.etms.trace.get(物流轨迹)

  4. 卖家OAuth授权换取 AccessToken
    ① 引导卖家访问:
    https://auth.jd.com/oauth2/toLogin.action
     ?response_type=code
     &client_id=YOUR_APP_KEY
     &redirect_uri=URLENCODE(你在应用配置的回调地址)
     &state=erp_jd
    
    ② 回调 → redirect_uri?code=xxx
    
    ③ POST https://auth.jd.com/oauth2/accessToken
       grant_type=authorization_code
       client_id=APP_KEY
       client_secret=APP_SECRET
       code=xxx
       redirect_uri=同上
    → {access_token, refresh_token, expires_in, user_nick}
    access_token= JOS接口中的 access_token(session) 参数


三、Python:订单列表 + 明细调用封装(含权限提示)

# jd_order_sync.py
"""
京东订单同步 Demo(企业应用 + 卖家AccessToken)
jingdong.pop.order.search  →  jd.order.detail.get
依赖: requests  (pip install requests)
"""
import hashlib
import json
import requests
import time
from datetime import datetime, timedelta
from typing import Dict, List

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

    def __init__(self, app_key: str, app_secret: str):
        self.ak = app_key
        self.as_ = app_secret

    # ─── JOS MD5签名(秒级timestamp)───
    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.as_}{qs}{self.as_}".encode()
                          ).hexdigest().upper()

    def _call(self, method: str, biz: Dict, access_token: str):
        api_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=(',', ':')),
            "access_token": access_token
        }
        api_p["sign"] = self._sign(api_p)

        r = requests.post(self.GW, data=api_p, timeout=15)
        r.raise_for_status()
        d = r.json()

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

        # 权限/业务错误检测
        if isinstance(data, dict):
            err = data.get("error_response") or d.get("error_response")
            if err:
                code = str(err.get("code", ""))
                zh = err.get("zh_desc") or err.get("en_desc")
                if "no permission" in zh or "invalid method" in zh:
                    raise PermissionError(
                        "❌ 【无权限】订单接口需:\n"
                        "  1) 企业实名商家应用\n"
                        "  2) 已申请 jingdong.pop.order.search / jd.order.detail.get\n"
                        "  3) access_token 须是【卖家】OAuth授权所得(非买家token)\n"
                        f"  原始: [{code}] {zh}"
                    )
                raise Exception(f"JOS [{code}]: {zh} sub:{err.get('sub_code')}")
        return data

    # ─── 增量拉取订单列表 ───
    def list_orders(self, access_token: str,
                    minutes_back: int = 30,
                    order_state: str = "WAIT_SELLER_STOCK_OUT",
                    page: int = 1,
                    page_size: int = 50) -> Dict:
        now = datetime.now()
        start = (now - timedelta(minutes=minutes_back)).strftime("%Y-%m-%d %H:%M:%S")
        end = now.strftime("%Y-%m-%d %H:%M:%S")
        return self._call(
            "jingdong.pop.order.search",
            {
                "start_modified": start,
                "end_modified": end,
                "order_state": order_state,     # WAIT_SELLER_STOCK_OUT=已付待发
                "page": page,
                "page_size": min(page_size, 100)
            },
            access_token
        ).get("popOrderSearch", {}).get("orderSearch", {})

    # ─── 订单明细 ───
    def get_detail(self, access_token: str, order_id: str) -> Dict:
        return self._call(
            "jd.order.detail.get",
            {"orderId": order_id},
            access_token
        ).get("orderDetail", {}).get("orderInfo", {})


# =========================================================
# 使用示例
# =========================================================
if __name__ == "__main__":
    client = JdOrderClient(
        app_key="YOUR_JD_ENTERPRISE_APP_KEY",
        app_secret="YOUR_JD_APP_SECRET"
    )
    SELLER_TOKEN = "SELLER_ACCESS_TOKEN"   # ← OAuth2 换取的卖家 token

    try:
        result = client.list_orders(SELLER_TOKEN, minutes_back=30)
        orders = result.get("orderInfoList", []) or []
        total = result.get("orderTotal", 0)
        print(f"✅ 近30分钟变更订单: {len(orders)} / 共计{total}")

        for o in orders[:3]:
            detail = client.get_detail(SELLER_TOKEN, str(o.get("orderId")))
            print(f"  单 {detail.get('orderId')} {detail.get('orderState')} "
                  f"¥{detail.get('orderPrice')}")

    except PermissionError as pe:
        print(pe)
        print("\n➡  解决:企业实名→创建自用型应用→申请订单权限→卖家OAuth授权→填入SELLER_TOKEN")
    except Exception as e:
        print("❌", e)

四、OAuth Token 交换最简片段(补全用)

def jd_exchange_token(app_key, app_secret, code, redirect_uri):
    r = requests.post("https://auth.jd.com/oauth2/accessToken", data={
        "grant_type": "authorization_code",
        "client_id": app_key,
        "client_secret": app_secret,
        "code": code,
        "redirect_uri": redirect_uri
    }, timeout=15)
    r.raise_for_status()
    return r.json()   # access_token / refresh_token / expires_in / user_nick

五、避坑清单(京东订单对接必看)

现象
解决
个人应用调订单
403 no permission
企业实名商家自用应用
接口未申请
同上 403
应用→API权限→申请订单接口
传买家 token
空/403
必须用店铺卖家 OAuth 换的 AccessToken
token 过期
Invalid access_token
refresh_token提前刷新(建议过期前7天)
沙箱返回空订单
正常
沙箱只验签,用生产网关
ISV应用403
未入驻软服/未绑定店铺
完成 ISV 入驻并绑定授权店铺
timestamp 毫秒
Invalid Timestamp
JOS用秒级 int(time.time())

六、面试/方案一句话

京东订单API(jingdong.pop.order.search/ jd.order.detail.get)须企业实名商家应用 + 申请订单权限 + 卖家OAuth AccessToken(session参数);增量按 start_modified/end_modified时间窗拉取防超量,遇403先确认以上三点,沙箱仅验签名不返回真实订单。
需要我补 APScheduler 定时增量订单同步(断点续跑+Token自动刷新)京东发货回填 jingdong.etms.waybill.send完整参数 吗?


群贤毕至

访客