v1.6: Platform Gateway — Amazon/JD/Taobao/PDD/TikTok 5平台API集成 + start_provision_v2.sh
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""JD (jingdong.com) union open platform adapter.
|
||||
|
||||
Endpoint: https://api.jd.com/routerjson
|
||||
Auth: app_key + app_secret + access_token (LWC OAuth 2.0)
|
||||
Sign: MD5(app_secret + sorted(k1v1k2v2...) + app_secret) uppercased
|
||||
|
||||
Methods:
|
||||
jd.union.open.goods.promotiongoodsinfo.query by SKU ID
|
||||
jd.union.open.goods.query by keyword
|
||||
|
||||
cred shape:
|
||||
{"app_key": "...", "app_secret": "...", "access_token": "...", "site_id": "..."}
|
||||
|
||||
query shape:
|
||||
{"sku": "100012345678"} -> goods.promotiongoodsinfo.query
|
||||
{"keyword": "iPhone 15"} -> goods.query
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from .base import UnifiedResult
|
||||
|
||||
log = logging.getLogger("chathub.gateway.jd")
|
||||
|
||||
API_URL = "https://api.jd.com/routerjson"
|
||||
SKU_QUERY_METHOD = "jd.union.open.goods.promotiongoodsinfo.query"
|
||||
KEYWORD_QUERY_METHOD = "jd.union.open.goods.query"
|
||||
|
||||
|
||||
def _json_dumps(obj: Any) -> str:
|
||||
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def _sign(app_secret: str, params: dict[str, str]) -> str:
|
||||
"""JD sign: app_secret + sorted(k1v1k2v2...) + app_secret, MD5, uppercase."""
|
||||
pieces = "".join(f"{k}{params[k]}" for k in sorted(params.keys()))
|
||||
return hashlib.md5((app_secret + pieces + app_secret).encode("utf-8")).hexdigest().upper()
|
||||
|
||||
|
||||
def _parse_sku_response(payload: dict, sku: str) -> UnifiedResult:
|
||||
inner = payload.get("jd_union_open_goods_promotiongoodsinfo_query_response") or {}
|
||||
result_str = inner.get("result", "{}")
|
||||
try:
|
||||
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
||||
except Exception:
|
||||
result = {}
|
||||
data = result.get("data") or {}
|
||||
if not data:
|
||||
return UnifiedResult(status="error", error=f"sku {sku} not found", channel="jd")
|
||||
price_info = data.get("priceInfo") or {}
|
||||
img_info = data.get("imageInfo") or {}
|
||||
base = data.get("baseInfo") or {}
|
||||
return UnifiedResult(
|
||||
status="success",
|
||||
data={
|
||||
"title": base.get("name") or data.get("skuName") or f"SKU {sku}",
|
||||
"price": price_info.get("price") or price_info.get("lowestPrice"),
|
||||
"currency": "CNY",
|
||||
"url": data.get("url") or f"https://item.jd.com/{sku}.html",
|
||||
"image": (img_info.get("imageList") or [None])[0],
|
||||
"in_stock": (data.get("stockState") or 1) != 0,
|
||||
},
|
||||
channel="jd",
|
||||
)
|
||||
|
||||
|
||||
def _parse_keyword_response(payload: dict) -> UnifiedResult:
|
||||
inner = payload.get("jd_union_open_goods_query_response") or {}
|
||||
result_str = inner.get("result", "{}")
|
||||
try:
|
||||
result = json.loads(result_str) if isinstance(result_str, str) else result_str
|
||||
except Exception:
|
||||
result = {}
|
||||
items = result.get("data") or []
|
||||
if not items:
|
||||
return UnifiedResult(status="error", error="no items for keyword", channel="jd")
|
||||
out = []
|
||||
for it in items[:3]:
|
||||
price_info = it.get("priceInfo") or {}
|
||||
out.append({
|
||||
"title": it.get("skuName") or "(无标题)",
|
||||
"price": price_info.get("price"),
|
||||
"currency": "CNY",
|
||||
"url": it.get("url") or "",
|
||||
})
|
||||
return UnifiedResult(status="success", data=out, channel="jd")
|
||||
|
||||
|
||||
async def fetch(creds: dict, query: dict) -> UnifiedResult:
|
||||
sku = query.get("sku")
|
||||
keyword = query.get("keyword")
|
||||
|
||||
app_key = creds.get("app_key") or creds.get("app_id")
|
||||
app_secret = creds.get("app_secret")
|
||||
access_token = creds.get("access_token") or creds.get("refresh_token")
|
||||
|
||||
if not app_key or not app_secret:
|
||||
return UnifiedResult(
|
||||
status="no_creds",
|
||||
error="missing app_key/app_secret (set them via channelAuth)",
|
||||
channel="jd",
|
||||
)
|
||||
if not access_token:
|
||||
return UnifiedResult(
|
||||
status="no_creds",
|
||||
error="missing access_token (use refresh_token via LWC OAuth to obtain)",
|
||||
channel="jd",
|
||||
)
|
||||
|
||||
if sku:
|
||||
method = SKU_QUERY_METHOD
|
||||
biz = {"skuIds": [str(sku)]}
|
||||
elif keyword:
|
||||
method = KEYWORD_QUERY_METHOD
|
||||
biz = {"keyword": str(keyword), "pageSize": 3}
|
||||
else:
|
||||
return UnifiedResult(status="error", error="missing sku or keyword", channel="jd")
|
||||
|
||||
public_params = {
|
||||
"method": method,
|
||||
"app_key": app_key,
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"format": "json",
|
||||
"v": "2.0",
|
||||
"access_token": access_token,
|
||||
"param_json": _json_dumps(biz),
|
||||
}
|
||||
public_params["sign"] = _sign(app_secret, public_params)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8.0) as client:
|
||||
r = await client.post(
|
||||
API_URL,
|
||||
data=urlencode(public_params),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
r.raise_for_status()
|
||||
payload = r.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
return UnifiedResult(
|
||||
status="error",
|
||||
error=f"HTTP {e.response.status_code}: {e.response.text[:200]}",
|
||||
channel="jd",
|
||||
)
|
||||
except Exception as e:
|
||||
return UnifiedResult(status="error", error=str(e)[:200], channel="jd")
|
||||
|
||||
jd_code = str(payload.get("code", ""))
|
||||
if jd_code not in ("200", "0", ""):
|
||||
return UnifiedResult(
|
||||
status="error",
|
||||
error=f"JD code={jd_code} message={payload.get('message') or payload.get('error_response', '')}",
|
||||
channel="jd",
|
||||
)
|
||||
|
||||
if sku:
|
||||
return _parse_sku_response(payload, sku)
|
||||
return _parse_keyword_response(payload)
|
||||
Reference in New Issue
Block a user