from __future__ import annotations import asyncio import json import logging import time from typing import Any import httpx from yuxi.channel.extensions.pinduoduo.signature import generate_sign from yuxi.channel.extensions.pinduoduo.token import PddTokenManager logger = logging.getLogger(__name__) # Alternative gateway: https://gw-api.pinduoduo.com/api/router PDD_API_URL = "https://open-api.pinduoduo.com/api/router" MAX_RETRY_COUNT = 3 RETRY_BACKOFF_BASE = 2.0 TOKEN_EXPIRED_ERROR_CODES = {10005, 10006, 10007} RATE_LIMIT_ERROR_CODE = 70001 class PddApiError(Exception): def __init__(self, error_code: int, error_msg: str, retryable: bool = False): self.error_code = error_code self.error_msg = error_msg self.retryable = retryable super().__init__(f"[{error_code}] {error_msg}") class PddApiClient: def __init__(self, token_manager: PddTokenManager, sign_method: str = "md5"): self._token_manager = token_manager self._sign_method = sign_method self._http_client: httpx.AsyncClient | None = None def _get_client(self) -> httpx.AsyncClient: if self._http_client is None: self._http_client = httpx.AsyncClient(timeout=httpx.Timeout(30.0)) return self._http_client async def call(self, api_type: str, params: dict | None = None) -> dict[str, Any]: token = await self._token_manager.get_token() client = self._get_client() base_params: dict[str, Any] = { "type": api_type, "client_id": self._token_manager.client_id, "access_token": token, "timestamp": int(time.time()), "data_type": "JSON", } if params: base_params.update(params) sign = generate_sign(base_params, self._token_manager.client_secret, self._sign_method) base_params["sign"] = sign last_error = None for attempt in range(MAX_RETRY_COUNT): try: resp = await client.post(PDD_API_URL, json=base_params) resp.raise_for_status() data = resp.json() if "error_response" in data: err = data["error_response"] err_code = err.get("error_code", -1) err_msg = err.get("error_msg", "unknown error") if err_code in TOKEN_EXPIRED_ERROR_CODES: logger.info("Token expired, refreshing...") await self._token_manager.invalidate_token() token = await self._token_manager.get_token() base_params["access_token"] = token base_params["sign"] = generate_sign(base_params, self._token_manager.client_secret, self._sign_method) continue if err_code == RATE_LIMIT_ERROR_CODE: wait_time = RETRY_BACKOFF_BASE**attempt logger.warning( "Rate limited, waiting %.1fs (attempt %d)", wait_time, attempt + 1, ) await asyncio.sleep(wait_time) last_error = PddApiError(err_code, err_msg, retryable=True) continue raise PddApiError(err_code, err_msg) return data except httpx.HTTPStatusError as e: raise PddApiError( e.response.status_code, f"HTTP {e.response.status_code}", retryable=e.response.status_code >= 500, ) from e except httpx.RequestError as e: last_error = PddApiError(-1, str(e), retryable=True) if attempt < MAX_RETRY_COUNT - 1: wait_time = RETRY_BACKOFF_BASE**attempt await asyncio.sleep(wait_time) continue raise last_error or PddApiError(-1, "Max retries exceeded") async def close(self) -> None: if self._http_client: await self._http_client.aclose() self._http_client = None class PddCSClient: def __init__(self, api_client: PddApiClient): self._api = api_client async def send_text(self, session_id: str, from_user: str, to_user: str, mall_id: str, content: str) -> dict: return await self._api.call( "pdd.logistics.cs.session.send", { "session_id": session_id, "from_user": from_user, "to_user": to_user, "mall_id": mall_id, "message_type": 0, "message_content": content, }, ) async def send_image(self, session_id: str, from_user: str, to_user: str, mall_id: str, image_url: str) -> dict: return await self._api.call( "pdd.logistics.cs.session.send", { "session_id": session_id, "from_user": from_user, "to_user": to_user, "mall_id": mall_id, "message_type": 1, "message_content": json.dumps({"url": image_url}), }, ) async def send_order_card(self, session_id: str, from_user: str, to_user: str, mall_id: str, order_sn: str) -> dict: return await self._api.call( "pdd.logistics.cs.session.send", { "session_id": session_id, "from_user": from_user, "to_user": to_user, "mall_id": mall_id, "message_type": 3, "message_content": json.dumps({"order_sn": order_sn}), }, ) async def poll_messages( self, mall_id: str, start_modified: str, end_modified: str, page_num: int = 1, page_size: int = 100, ) -> dict: return await self._api.call( "pdd.logistics.cs.message.get", { "mall_id": mall_id, "start_modified": start_modified, "end_modified": end_modified, "page_num": page_num, "page_size": page_size, }, ) class PddOrderClient: def __init__(self, api_client: PddApiClient): self._api = api_client async def get_order_detail(self, order_sn: str) -> dict: return await self._api.call( "pdd.order.information.get", {"order_sn": order_sn}, ) async def get_recent_orders( self, start_updated_at: str, end_updated_at: str, page: int = 1, page_size: int = 20 ) -> dict: return await self._api.call( "pdd.order.list.get", { "start_updated_at": start_updated_at, "end_updated_at": end_updated_at, "page": page, "page_size": page_size, }, ) class PddLogisticsClient: def __init__(self, api_client: PddApiClient): self._api = api_client async def get_trace(self, order_sn: str) -> dict: return await self._api.call( "pdd.logistics.trace.get", {"order_sn": order_sn}, ) class PddRefundClient: def __init__(self, api_client: PddApiClient): self._api = api_client async def get_refund_info(self, order_sn: str) -> dict: return await self._api.call( "pdd.refund.information.get", {"order_sn": order_sn}, ) async def get_refund_list( self, start_updated_at: str, end_updated_at: str, page: int = 1, page_size: int = 20 ) -> dict: return await self._api.call( "pdd.refund.list.get", { "start_updated_at": start_updated_at, "end_updated_at": end_updated_at, "page": page, "page_size": page_size, }, ) async def agree_refund(self, order_sn: str, remark: str = "") -> dict: return await self._api.call( "pdd.refund.status.update", { "order_sn": order_sn, "operate_type": 1, "remark": remark, }, ) async def reject_refund(self, order_sn: str, reject_reason: str, reject_remark: str = "") -> dict: return await self._api.call( "pdd.refund.status.update", { "order_sn": order_sn, "operate_type": 2, "reject_reason": reject_reason, "reject_remark": reject_remark, }, ) class PddSessionClient: def __init__(self, api_client: PddApiClient): self._api = api_client async def transfer_human(self, session_id: str, mall_id: str, buyer_id: str, reason: str = "") -> dict: params: dict = { "session_id": session_id, "mall_id": mall_id, "to_user": buyer_id, } if reason: params["transfer_reason"] = reason return await self._api.call( "pdd.logistics.cs.session.transfer.human", params, ) async def close_session(self, session_id: str, mall_id: str) -> dict: return await self._api.call( "pdd.logistics.cs.session.close", { "session_id": session_id, "mall_id": mall_id, }, ) class PddGoodsClient: def __init__(self, api_client: PddApiClient): self._api = api_client async def get_goods_detail(self, goods_id: int) -> dict: return await self._api.call( "pdd.goods.detail.get", {"goods_id": goods_id}, ) async def get_goods_list(self, page: int = 1, page_size: int = 20) -> dict: return await self._api.call( "pdd.goods.list.get", {"page": page, "page_size": page_size}, )