本文聚焦 实战落地,以「官方开放平台 API」为核心(合规优先),结合 Python 语言实现核心接口封装、功能模块集成,并提供完整的项目架构设计、调试技巧与上线优化方案。适用于企业服务商、第三方工具开发者快速搭建闲鱼生态应用(如批量商品管理系统、订单同步工具等)。
一、项目架构设计
1. 整体架构(分层设计)
为保证代码可维护性、可扩展性,采用「分层架构」设计,核心目录结构如下:
plaintext
xianyu-api-demo/
├── config/ # 配置文件目录
│ ├── __init__.py
│ └── settings.py # 全局配置(AppKey、AppSecret、接口地址等)
├── core/ # 核心封装目录
│ ├── __init__.py
│ ├── client.py # API 客户端(签名、请求发送、重试逻辑)
│ └── auth.py # 授权管理(AccessToken 获取/刷新)
├── api/ # 接口封装目录
│ ├── __init__.py
│ ├── item.py # 商品相关接口(发布、编辑、查询)
│ ├── order.py # 订单相关接口(查询、发货、售后)
│ └── upload.py # 上传相关接口(图片、视频)
├── service/ # 业务逻辑层
│ ├── __init__.py
│ ├── item_service.py # 商品业务(如批量发布、库存更新)
│ └── order_service.py # 订单业务(如订单同步、状态推送)
├── utils/ # 工具函数目录
│ ├── __init__.py
│ ├── sign.py # 签名工具(复用/扩展核心逻辑)
│ ├── logger.py # 日志工具
│ └── exception.py # 自定义异常
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── test_item.py
│ └── test_order.py
└── main.py # 入口文件(示例运行)
2. 核心依赖选型
| 依赖库 | 用途 | 版本建议 |
|---|---|---|
| requests | HTTP 请求发送 | 2.31.0+ |
| pycryptodome | 加密(HMAC-SHA256/MD5) | 3.20.0+ |
| python-dotenv | 环境变量管理 | 1.0.0+ |
| loguru | 日志记录 | 0.7.0+ |
| tenacity | 重试机制 | 8.2.3+ |
| pydantic | 数据校验(参数 / 响应) | 2.0.0+ |
二、核心封装实现
1. 全局配置(config/settings.py)
统一管理配置项,支持环境变量注入(避免硬编码敏感信息):
python
运行
import os
from dotenv import load_dotenv
# 加载 .env 文件(本地开发用)
load_dotenv()
class Settings:
# 阿里开放平台基础配置
APP_KEY = os.getenv("XIAOYU_APP_KEY")
APP_SECRET = os.getenv("XIAOYU_APP_SECRET")
GATEWAY_URL = "https://eco.taobao.com/router/rest" # 闲鱼官方 API 网关
TIMEOUT = 5 # 请求超时时间(秒)
# OAuth2.0 授权配置
AUTH_URL = "https://oauth.taobao.com/token" # AccessToken 获取地址
REFRESH_TOKEN = os.getenv("XIAOYU_REFRESH_TOKEN") # 刷新令牌(长期有效)
# 接口版本与签名配置
API_VERSION = "2.0"
SIGN_METHOD = "HMAC-SHA256" # 支持 MD5/HMAC-SHA256
MAX_RETRY = 3 # 请求重试次数
RETRY_DELAY = 1 # 重试延迟(秒)
# 单例模式导出配置
settings = Settings()
2. 签名工具(utils/sign.py)
基于上一篇的签名规则,实现通用签名函数:
python
运行
import hashlib
import hmac
from urllib.parse import urlencode
from typing import Dict
def generate_sign(params: Dict[str, str], app_secret: str, sign_method: str = "HMAC-SHA256") -> str:
"""
生成闲鱼 API 签名
:param params: 所有请求参数(系统参数 + 业务参数)
:param app_secret: AppSecret
:param sign_method: 签名方式(MD5/HMAC-SHA256)
:return: 签名结果(大写)
"""
# 1. 按参数名 ASCII 升序排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 2. 拼接为 key1value1key2value2... 格式(无需 URL 编码,requests 会自动处理)
sign_str = "".join([f"{k}{v}" for k, v in sorted_params])
# 3. 前后拼接 AppSecret
sign_str = f"{app_secret}{sign_str}{app_secret}"
# 4. 加密
if sign_method == "MD5":
md5 = hashlib.md5()
md5.update(sign_str.encode("utf-8"))
return md5.hexdigest().upper()
elif sign_method == "HMAC-SHA256":
hmac_obj = hmac.new(
key=app_secret.encode("utf-8"),
msg=sign_str.encode("utf-8"),
digestmod=hashlib.sha256
)
return hmac_obj.hexdigest().upper()
else:
raise ValueError(f"不支持的签名方式:{sign_method}")
3. 授权管理(core/auth.py)
实现 AccessToken 的获取与自动刷新(有效期 2 小时):
python
运行
import requests
from typing import Optional
from config.settings import settings
from utils.logger import logger
class AuthManager:
_access_token: Optional[str] = None
_expires_at: Optional[int] = None # token 过期时间(时间戳,秒)
@classmethod
def get_access_token(cls) -> str:
"""获取 AccessToken(自动刷新过期 token)"""
# 检查 token 是否存在且未过期(预留 60 秒缓冲)
if cls._access_token and cls._expires_at and (cls._expires_at - 60) > int(time.time()):
return cls._access_token
# 刷新 token
cls._refresh_access_token()
return cls._access_token
@classmethod
def _refresh_access_token(cls):
"""通过 RefreshToken 刷新 AccessToken"""
params = {
"grant_type": "refresh_token",
"appkey": settings.APP_KEY,
"refresh_token": settings.REFRESH_TOKEN,
"client_id": settings.APP_KEY,
"client_secret": settings.APP_SECRET
}
try:
response = requests.post(settings.AUTH_URL, params=params, timeout=settings.TIMEOUT)
response.raise_for_status()
result = response.json()
if "access_token" not in result:
raise ValueError(f"刷新 token 失败:{result.get('msg', '未知错误')}")
cls._access_token = result["access_token"]
# 计算过期时间(当前时间 + 有效期(秒))
cls._expires_at = int(time.time()) + result.get("expires_in", 7200)
logger.info(f"AccessToken 刷新成功,有效期至:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(cls._expires_at))}")
except Exception as e:
logger.error(f"刷新 AccessToken 失败:{str(e)}")
raise
4. API 客户端(core/client.py)
封装请求发送、签名生成、重试逻辑,作为所有接口的基础:
python
运行
import time
import requests
from typing import Dict, Optional, Any
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type
from config.settings import settings
from core.auth import AuthManager
from utils.sign import generate_sign
from utils.logger import logger
from utils.exception import ApiRequestError, ApiResponseError
class XianyuApiClient:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"User-Agent": "Xianyu-API-Client/1.0 (Python)"
})
@retry(
stop=stop_after_attempt(settings.MAX_RETRY),
wait=wait_fixed(settings.RETRY_DELAY),
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout))
)
def request(self, method: str, api_name: str, business_params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
通用请求方法
:param method: HTTP 方法(GET/POST)
:param api_name: 接口名(如 xiaoyuxianyu.item.publish)
:param business_params: 业务参数
:return: 接口响应数据(data 字段)
"""
business_params = business_params or {}
# 1. 构建系统参数
system_params = {
"app_key": settings.APP_KEY,
"method": api_name,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"format": "json",
"v": settings.API_VERSION,
"sign_method": settings.SIGN_METHOD,
"access_token": AuthManager.get_access_token()
}
# 2. 合并所有参数(系统参数 + 业务参数),并转换为字符串类型(避免签名错误)
all_params = {**system_params, **business_params}
all_params = {k: str(v) for k, v in all_params.items()}
# 3. 生成签名
sign = generate_sign(all_params, settings.APP_SECRET, settings.SIGN_METHOD)
all_params["sign"] = sign
# 4. 发送请求
logger.debug(f"发送 API 请求:method={method}, api={api_name}, params={all_params}")
try:
if method.upper() == "GET":
response = self.session.get(settings.GATEWAY_URL, params=all_params, timeout=settings.TIMEOUT)
else:
response = self.session.post(settings.GATEWAY_URL, data=all_params, timeout=settings.TIMEOUT)
response.raise_for_status() # 抛出 HTTP 错误(4xx/5xx)
result = response.json()
logger.debug(f"API 响应:{result}")
# 5. 解析响应(处理错误)
self._handle_response(result, api_name)
return result.get("data", {})
except requests.exceptions.RequestException as e:
logger.error(f"API 请求失败:{str(e)}")
raise ApiRequestError(f"请求异常:{str(e)}") from e
except ApiResponseError:
raise
except Exception as e:
logger.error(f"API 处理失败:{str(e)}")
raise ApiRequestError(f"处理异常:{str(e)}") from e
def _handle_response(self, result: Dict[str, Any], api_name: str):
"""处理响应结果,抛出业务错误"""
code = result.get("code", -1)
msg = result.get("msg", "未知错误")
request_id = result.get("request_id", "")
if code != 0:
logger.error(f"API 业务错误:api={api_name}, code={code}, msg={msg}, request_id={request_id}")
raise ApiResponseError(
api_name=api_name,
code=code,
msg=msg,
request_id=request_id
)
# 单例模式导出客户端
api_client = XianyuApiClient()
5. 自定义异常(utils/exception.py)
统一异常类型,方便业务层捕获处理:
python
运行
class XianyuApiException(Exception):
"""闲鱼 API 基础异常"""
pass
class ApiRequestError(XianyuApiException):
"""请求异常(网络、超时等)"""
pass
class ApiResponseError(XianyuApiException):
"""响应异常(业务错误、签名错误等)"""
def __init__(self, api_name: str, code: int, msg: str, request_id: str):
self.api_name = api_name
self.code = code
self.msg = msg
self.request_id = request_id
super().__init__(f"API[{api_name}] 错误:code={code}, msg={msg}, request_id={request_id}")
class ParameterError(XianyuApiException):
"""参数错误"""
pass
三、核心接口封装(api / 目录)
基于 api_client 封装高频接口,以「商品发布」「订单查询」「图片上传」为例:
1. 图片上传接口(api/upload.py)
商品图片需先通过官方上传接口获取合法 URL,再用于商品发布:
python
运行
from typing import List, Optional
from core.client import api_client
from utils.exception import ParameterError
def upload_image(image_paths: List[str]) -> List[str]:
"""
上传图片(支持多图)
:param image_paths: 本地图片路径列表(最多 9 张)
:return: 图片 URL 列表(逗号分隔,可直接用于商品发布)
"""
if not image_paths or len(image_paths) > 9:
raise ParameterError("图片数量必须为 1-9 张")
# 闲鱼图片上传接口:xiaoyuxianyu.media.upload
business_params = {
"type": "image", # 类型:image/video
"image_paths": ",".join(image_paths) # 本地路径逗号分隔
}
result = api_client.request(method="POST", api_name="xiaoyuxianyu.media.upload", business_params=business_params)
image_urls = result.get("image_urls", [])
if not image_urls:
raise ParameterError("图片上传失败,未返回有效 URL")
return image_urls
2. 商品接口(api/item.py)
封装商品发布、查询、编辑接口:
python
运行
from typing import Dict, Optional, List
from pydantic import BaseModel, Field, validate_model
from core.client import api_client
from utils.exception import ParameterError
# 商品发布参数校验模型(避免参数错误)
class ItemPublishParams(BaseModel):
title: str = Field(..., max_length=30, description="商品标题")
price: float = Field(..., gt=0, description="商品价格(元)")
category_id: int = Field(..., description="商品分类 ID")
description: str = Field(..., max_length=500, description="商品描述")
images: str = Field(..., description="图片 URL 列表(逗号分隔)")
location: str = Field(..., description="发货地址")
stock: int = Field(default=1, ge=1, le=999, description="库存")
item_type: int = Field(default=1, description="商品类型(1=二手,2=全新)")
def publish_item(params: Dict[str, Any]) -> Dict[str, str]:
"""
发布商品
:param params: 商品参数(需符合 ItemPublishParams 校验规则)
:return: 商品 ID 与发布时间
"""
# 参数校验
try:
validated_params = ItemPublishParams(**params)
except Exception as e:
raise ParameterError(f"商品参数错误:{str(e)}") from e
# 转换为字典(用于接口请求)
business_params = validated_params.model_dump()
# 调用商品发布接口:xiaoyuxianyu.item.publish
result = api_client.request(
method="POST",
api_name="xiaoyuxianyu.item.publish",
business_params=business_params
)
return {
"item_id": result.get("item_id", ""),
"publish_time": result.get("publish_time", ""),
"status": result.get("status", 0)
}
def get_item_detail(item_id: str) -> Dict[str, Any]:
"""
查询商品详情
:param item_id: 商品 ID
:return: 商品详情数据
"""
if not item_id:
raise ParameterError("商品 ID 不能为空")
business_params = {"item_id": item_id}
return api_client.request(
method="GET",
api_name="xiaoyuxianyu.item.get",
business_params=business_params
)
def update_item_price(item_id: str, new_price: float) -> bool:
"""
修改商品价格
:param item_id: 商品 ID
:param new_price: 新价格(元)
:return: 是否修改成功
"""
if not item_id or new_price <= 0:
raise ParameterError("商品 ID 不能为空且价格必须大于 0")
business_params = {
"item_id": item_id,
"price": new_price
}
result = api_client.request(
method="POST",
api_name="xiaoyuxianyu.item.update.price",
business_params=business_params
)
return result.get("success", False)
3. 订单接口(api/order.py)
封装订单查询、发货接口:
python
运行
from typing import Dict, Optional, List
from core.client import api_client
from utils.exception import ParameterError
def query_orders(
order_status: Optional[int] = None,
page: int = 1,
page_size: int = 20,
start_time: Optional[str] = None,
end_time: Optional[str] = None
) -> Dict[str, Any]:
"""
查询订单列表
:param order_status: 订单状态(1=待付款,2=待发货,3=待收货,4=已完成,5=已取消)
:param page: 页码(默认 1)
:param page_size: 每页条数(默认 20,最大 50)
:param start_time: 开始时间(格式:yyyy-MM-dd HH:mm:ss)
:param end_time: 结束时间(格式:yyyy-MM-dd HH:mm:ss)
:return: 订单列表与分页信息
"""
if page_size > 50:
raise ParameterError("每页条数最大为 50")
business_params = {
"page": page,
"page_size": page_size
}
if order_status is not None:
business_params["order_status"] = order_status
if start_time:
business_params["start_time"] = start_time
if end_time:
business_params["end_time"] = end_time
# 调用订单查询接口:xiaoyuxianyu.order.list
return api_client.request(
method="GET",
api_name="xiaoyuxianyu.order.list",
business_params=business_params
)
def ship_order(order_id: str, logistics_company: str, logistics_no: str) -> bool:
"""
订单发货
:param order_id: 订单 ID
:param logistics_company: 快递公司名称(需为闲鱼支持的快递公司)
:param logistics_no: 物流单号
:return: 是否发货成功
"""
if not (order_id and logistics_company and logistics_no):
raise ParameterError("订单 ID、快递公司、物流单号不能为空")
business_params = {
"order_id": order_id,
"logistics_company": logistics_company,
"logistics_no": logistics_no
}
result = api_client.request(
method="POST",
api_name="xiaoyuxianyu.order.ship",
business_params=business_params
)
return result.get("success", False)
四、业务功能集成示例
1. 批量商品发布(service/item_service.py)
集成「图片上传」与「商品发布」接口,实现批量发布功能:
python
运行
from typing import List, Dict
from api.upload import upload_image
from api.item import publish_item
from utils.logger import logger
def batch_publish_items(item_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
批量发布商品
:param item_list: 商品列表(每个元素包含商品参数 + local_image_paths 本地图片路径列表)
:return: 发布结果列表(含商品 ID 或错误信息)
"""
results = []
for idx, item in enumerate(item_list):
try:
# 1. 提取本地图片路径并上传
local_image_paths = item.pop("local_image_paths", [])
if not local_image_paths:
raise ValueError("商品必须包含至少 1 张图片")
logger.info(f"开始上传商品图片:第 {idx+1} 个商品,图片数量={len(local_image_paths)}")
image_urls = upload_image(local_image_paths)
item["images"] = ",".join(image_urls)
# 2. 发布商品
logger.info(f"开始发布商品:第 {idx+1} 个商品,标题={item['title']}")
publish_result = publish_item(item)
results.append({
"success": True,
"item_idx": idx,
"item_id": publish_result["item_id"],
"message": "发布成功"
})
except Exception as e:
logger.error(f"第 {idx+1} 个商品发布失败:{str(e)}")
results.append({
"success": False,
"item_idx": idx,
"item_id": "",
"message": str(e)
})
return results
2. 订单同步到本地数据库(service/order_service.py)
集成「订单查询」接口,实现订单同步功能(示例用伪代码表示数据库操作):
python
运行
from typing import Optional
from datetime import datetime, timedelta
from api.order import query_orders
from utils.logger import logger
# 模拟本地数据库操作(实际项目替换为 SQLAlchemy/MySQLdb 等)
class OrderDB:
@staticmethod
def save_order(order_data: Dict[str, Any]):
"""保存订单到本地数据库"""
# 实际逻辑:插入或更新订单表
logger.info(f"保存订单到数据库:order_id={order_data['order_id']}")
@staticmethod
def get_latest_order_time() -> Optional[str]:
"""获取本地最新订单的创建时间(用于增量同步)"""
# 实际逻辑:查询订单表最大 create_time
return (datetime.now() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S")
def sync_orders(order_status: Optional[int] = None) -> int:
"""
同步闲鱼订单到本地数据库(增量同步)
:param order_status: 订单状态(可选,默认同步所有状态)
:return: 同步成功的订单数量
"""
logger.info("开始同步闲鱼订单...")
db = OrderDB()
# 1. 获取本地最新订单时间(增量同步,避免重复)
latest_time = db.get_latest_order_time()
logger.info(f"增量同步起始时间:{latest_time}")
# 2. 分页查询订单
page = 1
total_sync = 0
while True:
try:
result = query_orders(
order_status=order_status,
page=page,
page_size=50,
start_time=latest_time
)
orders = result.get("orders", [])
total = result.get("total", 0)
logger.info(f"第 {page} 页订单:{len(orders)} 条,总订单数:{total}")
if not orders:
break
# 3. 保存订单到本地数据库
for order in orders:
db.save_order(order)
total_sync += 1
# 4. 分页判断
if page * 50 >= total:
break
page += 1
except Exception as e:
logger.error(f"订单同步失败(第 {page} 页):{str(e)}")
raise
logger.info(f"订单同步完成,共同步 {total_sync} 条订单")
return total_sync
五、调试与上线优化
1. 本地调试技巧
- 签名验证:使用阿里开放平台 签名测试工具 对比本地生成的
sign是否一致; - 日志调试:开启
DEBUG级别日志,查看请求参数、签名结果、响应数据(utils/logger.py中配置); - 接口测试:先通过
tests/目录下的单接口测试(如test_item.py)验证接口可用性,再集成业务逻辑; - 沙箱环境:阿里开放平台提供沙箱环境(申请地址),可先在沙箱测试避免影响线上数据。
2. 上线优化方案
- 限流控制:根据官方 API 的 QPS 限制(如商品发布 10 QPS / 应用),使用
ratelimit库控制请求频率:python
运行
from ratelimit import limits, sleep_and_retry # 限制 10 QPS @sleep_and_retry @limits(calls=10, period=1) def publish_item_with_rate_limit(params): return publish_item(params) - 异常重试策略:对「签名错误」「token 失效」等非网络错误,不重试直接抛出;对网络超时、连接错误,通过
tenacity重试; - 缓存优化:缓存商品分类 ID、快递公司列表等静态数据,避免重复调用接口;
- IP 池配置:若需高频请求,使用独立 IP 池(避免公共 IP 被限流),并保持 IP 稳定性;
- 监控告警:对接 Prometheus + Grafana 监控接口成功率、响应时间,通过钉钉 / 企业微信告警异常(如连续 5 次请求失败)。
3. 常见坑规避
- 时间戳格式:必须严格遵循
yyyy-MM-dd HH:mm:ss,且与阿里服务器时间差≤10 分钟(建议用 NTP 同步时间); - 参数类型:所有参数必须转换为字符串后参与签名(如
price=99.9不能传99.9浮点型,需转字符串str(99.9)); - 图片上传:必须使用闲鱼官方上传接口,直接传外部图片 URL 会被拒绝;
- RefreshToken 有效期:官方 RefreshToken 有效期通常为 30 天,需定期更新并存储(避免硬编码)。
六、完整运行示例(main.py)
python
运行
from service.item_service import batch_publish_items
from service.order_service import sync_orders
from utils.logger import logger
if __name__ == "__main__":
# 示例 1:批量发布商品
logger.info("=== 开始批量发布商品 ===")
test_items = [
{
"title": "二手 iPhone 13 128G 国行",
"price": 3999.99,
"category_id": 12345, # 需替换为真实分类 ID(通过 xiaoyuxianyu.category.list 获取)
"description": "95新,无划痕,电池健康 88%,送充电器",
"location": "广东省深圳市南山区",
"stock": 1,
"local_image_paths": ["./images/iphone13_1.jpg", "./images/iphone13_2.jpg"] # 本地图片路径
},
{
"title": "全新 AirPods Pro 2 国行未拆封",
"price": 1799.00,
"category_id": 67890, # 需替换为真实分类 ID
"description": "官网购入,未拆封,全国联保",
"location": "广东省深圳市福田区",
"stock": 2,
"local_image_paths": ["./images/airpods_1.jpg"]
}
]
publish_results = batch_publish_items(test_items)
logger.info(f"批量发布结果:{publish_results}")
# 示例 2:同步订单到本地数据库
logger.info("\n=== 开始同步订单 ===")
sync_count = sync_orders(order_status=2) # 同步待发货订单
logger.info(f"订单同步完成,共同步 {sync_count} 条")
七、合规与风险补充
- 接口权限申请:上线前需确保已申请目标接口的正式权限(沙箱权限≠正式权限),避免接口调用失败;
- 数据加密存储:
AppSecret、RefreshToken等敏感信息需加密存储(如用cryptography库加密),禁止明文存储在代码或配置文件中; - 用户授权合规:若需获取用户数据(如订单、收货地址),必须通过 OAuth2.0 授权流程,且明确告知用户数据用途,符合《个人信息保护法》;
- 接口变更适配:定期查看阿里开放平台公告,若接口参数、签名规则变更,需及时适配(建议在代码中预留版本兼容逻辑)。
通过以上封装与集成方案,可快速搭建稳定、合规的闲鱼 API 应用,同时兼顾代码的可维护性与扩展性。核心原则:基础封装复用、业务逻辑分离、异常处理完善、合规优先落地。


1756

被折叠的 条评论
为什么被折叠?



