- 配置 Docker Compose 多服务编排 - 实现 Chatwoot + Agent 集成 - 配置 Strapi MCP 知识库 - 支持 7 种语言的 FAQ 系统 - 实现 LangGraph AI 工作流 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
539 lines
14 KiB
Python
539 lines
14 KiB
Python
"""
|
|
Hyperf PHP API Client for B2B Shopping AI Assistant
|
|
"""
|
|
from typing import Any, Optional
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
import httpx
|
|
|
|
from config import settings
|
|
from utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class APIError(Exception):
|
|
"""API error with code and message"""
|
|
def __init__(self, code: int, message: str, data: Optional[Any] = None):
|
|
self.code = code
|
|
self.message = message
|
|
self.data = data
|
|
super().__init__(f"[{code}] {message}")
|
|
|
|
|
|
@dataclass
|
|
class APIResponse:
|
|
"""Standardized API response"""
|
|
code: int
|
|
message: str
|
|
data: Any
|
|
meta: Optional[dict[str, Any]] = None
|
|
|
|
@property
|
|
def success(self) -> bool:
|
|
return self.code == 0
|
|
|
|
|
|
class HyperfClient:
|
|
"""Hyperf PHP API Client"""
|
|
|
|
def __init__(
|
|
self,
|
|
api_url: Optional[str] = None,
|
|
api_token: Optional[str] = None
|
|
):
|
|
"""Initialize Hyperf client
|
|
|
|
Args:
|
|
api_url: Hyperf API base URL, defaults to settings
|
|
api_token: API access token, defaults to settings
|
|
"""
|
|
self.api_url = (api_url or settings.hyperf_api_url).rstrip("/")
|
|
self.api_token = api_token or settings.hyperf_api_token
|
|
self._client: Optional[httpx.AsyncClient] = None
|
|
|
|
logger.info("Hyperf client initialized", api_url=self.api_url)
|
|
|
|
async def _get_client(self) -> httpx.AsyncClient:
|
|
"""Get or create HTTP client"""
|
|
if self._client is None:
|
|
self._client = httpx.AsyncClient(
|
|
base_url=f"{self.api_url}/api/v1",
|
|
headers={
|
|
"Authorization": f"Bearer {self.api_token}",
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json"
|
|
},
|
|
timeout=30.0
|
|
)
|
|
return self._client
|
|
|
|
async def close(self) -> None:
|
|
"""Close HTTP client"""
|
|
if self._client:
|
|
await self._client.aclose()
|
|
self._client = None
|
|
|
|
async def _request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
params: Optional[dict[str, Any]] = None,
|
|
json: Optional[dict[str, Any]] = None,
|
|
headers: Optional[dict[str, str]] = None
|
|
) -> APIResponse:
|
|
"""Make API request
|
|
|
|
Args:
|
|
method: HTTP method
|
|
endpoint: API endpoint
|
|
params: Query parameters
|
|
json: JSON body
|
|
headers: Additional headers
|
|
|
|
Returns:
|
|
Parsed API response
|
|
|
|
Raises:
|
|
APIError: If API returns error
|
|
"""
|
|
client = await self._get_client()
|
|
|
|
# Merge headers
|
|
request_headers = {}
|
|
if headers:
|
|
request_headers.update(headers)
|
|
|
|
logger.debug(
|
|
"API request",
|
|
method=method,
|
|
endpoint=endpoint
|
|
)
|
|
|
|
try:
|
|
response = await client.request(
|
|
method=method,
|
|
url=endpoint,
|
|
params=params,
|
|
json=json,
|
|
headers=request_headers
|
|
)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
result = APIResponse(
|
|
code=data.get("code", 0),
|
|
message=data.get("message", "success"),
|
|
data=data.get("data"),
|
|
meta=data.get("meta")
|
|
)
|
|
|
|
if not result.success:
|
|
raise APIError(result.code, result.message, result.data)
|
|
|
|
logger.debug(
|
|
"API response",
|
|
endpoint=endpoint,
|
|
code=result.code
|
|
)
|
|
|
|
return result
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
logger.error(
|
|
"HTTP error",
|
|
endpoint=endpoint,
|
|
status_code=e.response.status_code
|
|
)
|
|
raise APIError(
|
|
e.response.status_code,
|
|
f"HTTP error: {e.response.status_code}"
|
|
)
|
|
except Exception as e:
|
|
logger.error("API request failed", endpoint=endpoint, error=str(e))
|
|
raise
|
|
|
|
async def get(
|
|
self,
|
|
endpoint: str,
|
|
params: Optional[dict[str, Any]] = None,
|
|
**kwargs: Any
|
|
) -> APIResponse:
|
|
"""GET request"""
|
|
return await self._request("GET", endpoint, params=params, **kwargs)
|
|
|
|
async def post(
|
|
self,
|
|
endpoint: str,
|
|
json: Optional[dict[str, Any]] = None,
|
|
**kwargs: Any
|
|
) -> APIResponse:
|
|
"""POST request"""
|
|
return await self._request("POST", endpoint, json=json, **kwargs)
|
|
|
|
async def put(
|
|
self,
|
|
endpoint: str,
|
|
json: Optional[dict[str, Any]] = None,
|
|
**kwargs: Any
|
|
) -> APIResponse:
|
|
"""PUT request"""
|
|
return await self._request("PUT", endpoint, json=json, **kwargs)
|
|
|
|
async def delete(
|
|
self,
|
|
endpoint: str,
|
|
**kwargs: Any
|
|
) -> APIResponse:
|
|
"""DELETE request"""
|
|
return await self._request("DELETE", endpoint, **kwargs)
|
|
|
|
# ============ Order APIs ============
|
|
|
|
async def query_orders(
|
|
self,
|
|
user_id: str,
|
|
account_id: str,
|
|
order_id: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
date_start: Optional[str] = None,
|
|
date_end: Optional[str] = None,
|
|
page: int = 1,
|
|
page_size: int = 20
|
|
) -> APIResponse:
|
|
"""Query orders
|
|
|
|
Args:
|
|
user_id: User ID
|
|
account_id: Account ID
|
|
order_id: Optional specific order ID
|
|
status: Optional order status filter
|
|
date_start: Optional start date (YYYY-MM-DD)
|
|
date_end: Optional end date (YYYY-MM-DD)
|
|
page: Page number
|
|
page_size: Items per page
|
|
|
|
Returns:
|
|
Orders list response
|
|
"""
|
|
payload = {
|
|
"user_id": user_id,
|
|
"account_id": account_id,
|
|
"page": page,
|
|
"page_size": page_size
|
|
}
|
|
|
|
if order_id:
|
|
payload["order_id"] = order_id
|
|
if status:
|
|
payload["status"] = status
|
|
if date_start:
|
|
payload["date_range"] = {"start": date_start}
|
|
if date_end:
|
|
payload.setdefault("date_range", {})["end"] = date_end
|
|
|
|
return await self.post("/orders/query", json=payload)
|
|
|
|
async def get_logistics(
|
|
self,
|
|
order_id: str,
|
|
tracking_number: Optional[str] = None
|
|
) -> APIResponse:
|
|
"""Get order logistics information
|
|
|
|
Args:
|
|
order_id: Order ID
|
|
tracking_number: Optional tracking number
|
|
|
|
Returns:
|
|
Logistics tracking response
|
|
"""
|
|
params = {}
|
|
if tracking_number:
|
|
params["tracking_number"] = tracking_number
|
|
|
|
return await self.get(f"/orders/{order_id}/logistics", params=params)
|
|
|
|
async def modify_order(
|
|
self,
|
|
order_id: str,
|
|
user_id: str,
|
|
modifications: dict[str, Any]
|
|
) -> APIResponse:
|
|
"""Modify order
|
|
|
|
Args:
|
|
order_id: Order ID
|
|
user_id: User ID for permission check
|
|
modifications: Changes to apply
|
|
|
|
Returns:
|
|
Modified order response
|
|
"""
|
|
return await self.put(
|
|
f"/orders/{order_id}/modify",
|
|
json={
|
|
"user_id": user_id,
|
|
"modifications": modifications
|
|
}
|
|
)
|
|
|
|
async def cancel_order(
|
|
self,
|
|
order_id: str,
|
|
user_id: str,
|
|
reason: str
|
|
) -> APIResponse:
|
|
"""Cancel order
|
|
|
|
Args:
|
|
order_id: Order ID
|
|
user_id: User ID for permission check
|
|
reason: Cancellation reason
|
|
|
|
Returns:
|
|
Cancellation result with refund info
|
|
"""
|
|
return await self.post(
|
|
f"/orders/{order_id}/cancel",
|
|
json={
|
|
"user_id": user_id,
|
|
"reason": reason
|
|
}
|
|
)
|
|
|
|
# ============ Product APIs ============
|
|
|
|
async def search_products(
|
|
self,
|
|
query: str,
|
|
filters: Optional[dict[str, Any]] = None,
|
|
sort: str = "relevance",
|
|
page: int = 1,
|
|
page_size: int = 20
|
|
) -> APIResponse:
|
|
"""Search products
|
|
|
|
Args:
|
|
query: Search query
|
|
filters: Optional filters (category, price_range, brand, etc.)
|
|
sort: Sort order
|
|
page: Page number
|
|
page_size: Items per page
|
|
|
|
Returns:
|
|
Products list response
|
|
"""
|
|
payload = {
|
|
"query": query,
|
|
"sort": sort,
|
|
"page": page,
|
|
"page_size": page_size
|
|
}
|
|
|
|
if filters:
|
|
payload["filters"] = filters
|
|
|
|
return await self.post("/products/search", json=payload)
|
|
|
|
async def get_product(self, product_id: str) -> APIResponse:
|
|
"""Get product details
|
|
|
|
Args:
|
|
product_id: Product ID
|
|
|
|
Returns:
|
|
Product details response
|
|
"""
|
|
return await self.get(f"/products/{product_id}")
|
|
|
|
async def get_recommendations(
|
|
self,
|
|
user_id: str,
|
|
account_id: str,
|
|
context: Optional[dict[str, Any]] = None,
|
|
limit: int = 10
|
|
) -> APIResponse:
|
|
"""Get product recommendations
|
|
|
|
Args:
|
|
user_id: User ID
|
|
account_id: Account ID
|
|
context: Optional context (recent views, current query)
|
|
limit: Number of recommendations
|
|
|
|
Returns:
|
|
Recommendations response
|
|
"""
|
|
payload = {
|
|
"user_id": user_id,
|
|
"account_id": account_id,
|
|
"limit": limit
|
|
}
|
|
|
|
if context:
|
|
payload["context"] = context
|
|
|
|
return await self.post("/products/recommend", json=payload)
|
|
|
|
async def get_quote(
|
|
self,
|
|
product_id: str,
|
|
quantity: int,
|
|
account_id: str,
|
|
delivery_address: Optional[dict[str, str]] = None
|
|
) -> APIResponse:
|
|
"""Get B2B price quote
|
|
|
|
Args:
|
|
product_id: Product ID
|
|
quantity: Quantity
|
|
account_id: Account ID for pricing tier
|
|
delivery_address: Optional delivery address
|
|
|
|
Returns:
|
|
Quote response with pricing details
|
|
"""
|
|
payload = {
|
|
"product_id": product_id,
|
|
"quantity": quantity,
|
|
"account_id": account_id
|
|
}
|
|
|
|
if delivery_address:
|
|
payload["delivery_address"] = delivery_address
|
|
|
|
return await self.post("/products/quote", json=payload)
|
|
|
|
# ============ Aftersale APIs ============
|
|
|
|
async def apply_return(
|
|
self,
|
|
order_id: str,
|
|
user_id: str,
|
|
items: list[dict[str, Any]],
|
|
description: str,
|
|
images: Optional[list[str]] = None
|
|
) -> APIResponse:
|
|
"""Apply for return
|
|
|
|
Args:
|
|
order_id: Order ID
|
|
user_id: User ID
|
|
items: Items to return with quantity and reason
|
|
description: Description of issue
|
|
images: Optional image URLs
|
|
|
|
Returns:
|
|
Return application response
|
|
"""
|
|
payload = {
|
|
"order_id": order_id,
|
|
"user_id": user_id,
|
|
"items": items,
|
|
"description": description
|
|
}
|
|
|
|
if images:
|
|
payload["images"] = images
|
|
|
|
return await self.post("/aftersales/return", json=payload)
|
|
|
|
async def apply_exchange(
|
|
self,
|
|
order_id: str,
|
|
user_id: str,
|
|
items: list[dict[str, Any]],
|
|
description: str
|
|
) -> APIResponse:
|
|
"""Apply for exchange
|
|
|
|
Args:
|
|
order_id: Order ID
|
|
user_id: User ID
|
|
items: Items to exchange with reason
|
|
description: Description of issue
|
|
|
|
Returns:
|
|
Exchange application response
|
|
"""
|
|
return await self.post(
|
|
"/aftersales/exchange",
|
|
json={
|
|
"order_id": order_id,
|
|
"user_id": user_id,
|
|
"items": items,
|
|
"description": description
|
|
}
|
|
)
|
|
|
|
async def create_complaint(
|
|
self,
|
|
user_id: str,
|
|
complaint_type: str,
|
|
title: str,
|
|
description: str,
|
|
related_order_id: Optional[str] = None,
|
|
attachments: Optional[list[str]] = None
|
|
) -> APIResponse:
|
|
"""Create complaint
|
|
|
|
Args:
|
|
user_id: User ID
|
|
complaint_type: Type of complaint
|
|
title: Complaint title
|
|
description: Detailed description
|
|
related_order_id: Optional related order
|
|
attachments: Optional attachment URLs
|
|
|
|
Returns:
|
|
Complaint creation response
|
|
"""
|
|
payload = {
|
|
"user_id": user_id,
|
|
"type": complaint_type,
|
|
"title": title,
|
|
"description": description
|
|
}
|
|
|
|
if related_order_id:
|
|
payload["related_order_id"] = related_order_id
|
|
if attachments:
|
|
payload["attachments"] = attachments
|
|
|
|
return await self.post("/aftersales/complaint", json=payload)
|
|
|
|
async def query_aftersales(
|
|
self,
|
|
user_id: str,
|
|
aftersale_id: Optional[str] = None
|
|
) -> APIResponse:
|
|
"""Query aftersale records
|
|
|
|
Args:
|
|
user_id: User ID
|
|
aftersale_id: Optional specific aftersale ID
|
|
|
|
Returns:
|
|
Aftersale records response
|
|
"""
|
|
params = {"user_id": user_id}
|
|
if aftersale_id:
|
|
params["aftersale_id"] = aftersale_id
|
|
|
|
return await self.get("/aftersales/query", params=params)
|
|
|
|
|
|
# Global Hyperf client instance
|
|
hyperf_client: Optional[HyperfClient] = None
|
|
|
|
|
|
def get_hyperf_client() -> HyperfClient:
|
|
"""Get or create global Hyperf client instance"""
|
|
global hyperf_client
|
|
if hyperf_client is None:
|
|
hyperf_client = HyperfClient()
|
|
return hyperf_client
|