v1.6: Platform Gateway — Amazon/JD/Taobao/PDD/TikTok 5平台API集成 + start_provision_v2.sh
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""TikTok/Douyin (抖音) open platform adapter.
|
||||
|
||||
Endpoint: https://open.douyin.com/ (multiple paths)
|
||||
Auth: client_key + client_secret + access_token (OAuth 2.0)
|
||||
Sign: HMAC-SHA256 (different from MD5 platforms)
|
||||
Method: /goods/detail (by goods_id) -- requires video/goods scope
|
||||
|
||||
cred shape:
|
||||
{"client_key": "...", "client_secret": "...", "access_token": "..."}
|
||||
|
||||
query shape:
|
||||
{"goods_id": "12345"} -> /goods/detail
|
||||
{"sku": "12345"} -> alias
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode, quote
|
||||
|
||||
import httpx
|
||||
|
||||
from .base import UnifiedResult
|
||||
|
||||
log = logging.getLogger("chathub.gateway.tiktok")
|
||||
|
||||
OAUTH_TOKEN_URL = "https://open.douyin.com/oauth/access_token/"
|
||||
GOODS_DETAIL_URL = "https://open.douyin.com/goods/detail"
|
||||
|
||||
|
||||
def _hmac_sign(secret: str, message: str) -> str:
|
||||
"""Douyin HMAC-SHA256 hex (lowercase)."""
|
||||
return hmac.new(secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
async def fetch(creds: dict, query: dict) -> UnifiedResult:
|
||||
goods_id = query.get("goods_id") or query.get("sku")
|
||||
|
||||
client_key = creds.get("client_key") or creds.get("app_id") or creds.get("app_key")
|
||||
client_secret = creds.get("client_secret") or creds.get("app_secret")
|
||||
access_token = creds.get("access_token") or creds.get("refresh_token")
|
||||
|
||||
if not client_key or not client_secret:
|
||||
return UnifiedResult(
|
||||
status="no_creds",
|
||||
error="missing client_key/client_secret (set them via channelAuth)",
|
||||
channel="tiktok",
|
||||
)
|
||||
if not access_token:
|
||||
return UnifiedResult(
|
||||
status="no_creds",
|
||||
error="missing access_token (obtain via 抖音 OAuth authorization; 2hr TTL, refresh via refresh_token)",
|
||||
channel="tiktok",
|
||||
)
|
||||
if not goods_id:
|
||||
return UnifiedResult(status="error", error="missing goods_id", channel="tiktok")
|
||||
|
||||
params = {
|
||||
"access_token": access_token,
|
||||
"goods_id": str(goods_id),
|
||||
"app_id": client_key,
|
||||
}
|
||||
param_json = json.dumps({"goods_id": str(goods_id)}, ensure_ascii=False, separators=(",", ":"))
|
||||
base_string = f"app_id={client_key}&goods_id={goods_id}&access_token={access_token}"
|
||||
signature = _hmac_sign(client_secret, base_string)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8.0) as client:
|
||||
r = await client.post(
|
||||
GOODS_DETAIL_URL,
|
||||
params={"access_token": access_token, "app_id": client_key, "goods_id": str(goods_id), "sign": signature},
|
||||
)
|
||||
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="tiktok",
|
||||
)
|
||||
except Exception as e:
|
||||
return UnifiedResult(status="error", error=str(e)[:200], channel="tiktok")
|
||||
|
||||
err_code = str(payload.get("err_no", payload.get("code", "")))
|
||||
if err_code not in ("", "0"):
|
||||
return UnifiedResult(
|
||||
status="error",
|
||||
error=f"Douyin err_no={err_code} msg={payload.get('message', payload.get('errmsg', ''))}",
|
||||
channel="tiktok",
|
||||
)
|
||||
|
||||
data = payload.get("data") or payload.get("goods_detail") or {}
|
||||
if not data:
|
||||
return UnifiedResult(status="error", error=f"goods_id {goods_id} not found", channel="tiktok")
|
||||
|
||||
price = data.get("price") or data.get("min_price")
|
||||
if price and isinstance(price, str):
|
||||
try:
|
||||
price = float(price) / 100
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return UnifiedResult(
|
||||
status="success",
|
||||
data={
|
||||
"title": data.get("title") or data.get("goods_name") or f"goods {goods_id}",
|
||||
"price": price,
|
||||
"currency": "CNY",
|
||||
"url": data.get("share_url") or data.get("detail_url") or f"https://haohuo.jinritemai.com/GoodsDetail?goods_id={goods_id}",
|
||||
"image": (data.get("cover") or {}).get("url") if isinstance(data.get("cover"), dict) else data.get("cover"),
|
||||
"in_stock": (data.get("stock") or 0) > 0,
|
||||
"sales": data.get("sales"),
|
||||
},
|
||||
channel="tiktok",
|
||||
)
|
||||
Reference in New Issue
Block a user