- 新增api/payment/支付服务(微信、支付宝) - 新增api/routers/payment.py支付路由 - 更新database/models.py支付相关模型 - 新增数据库迁移文件(订单表、用户订阅字段) - 更新main.py、requirements.txt、.env.production Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
7.5 KiB
Python
223 lines
7.5 KiB
Python
"""
|
|
支付宝 OpenAPI 封装
|
|
使用 python-alipay-sdk 实现 APP 支付
|
|
"""
|
|
import logging
|
|
from typing import Dict, Optional
|
|
|
|
from .config import AlipayConfig
|
|
from .schemas import PaymentResult, NotifyResult, PaymentStatus
|
|
from .exceptions import PaymentConfigError, PaymentCreateError, PaymentNotifyError, PaymentQueryError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AlipayClient:
|
|
"""支付宝客户端"""
|
|
|
|
def __init__(self, config: AlipayConfig):
|
|
self._config = config
|
|
self._client = None
|
|
|
|
def _ensure_client(self):
|
|
"""确保支付宝客户端已初始化"""
|
|
if not self._config.is_configured:
|
|
raise PaymentConfigError("支付宝配置不完整,请检查环境变量")
|
|
|
|
if self._client is None:
|
|
try:
|
|
from alipay import AliPay
|
|
|
|
self._client = AliPay(
|
|
appid=self._config.app_id,
|
|
app_notify_url=self._config.notify_url,
|
|
app_private_key_string=self._config.private_key,
|
|
alipay_public_key_string=self._config.alipay_public_key,
|
|
sign_type=self._config.sign_type,
|
|
debug=False, # 生产环境
|
|
)
|
|
logger.info("支付宝客户端初始化成功")
|
|
except Exception as e:
|
|
raise PaymentConfigError(f"支付宝客户端初始化失败: {e}")
|
|
|
|
def create_app_order(
|
|
self,
|
|
out_trade_no: str,
|
|
total_amount: int,
|
|
subject: str,
|
|
) -> PaymentResult:
|
|
"""
|
|
创建 APP 支付订单
|
|
|
|
Args:
|
|
out_trade_no: 商户订单号
|
|
total_amount: 金额(单位:分)
|
|
subject: 商品标题
|
|
|
|
Returns:
|
|
PaymentResult 包含支付宝 order_string
|
|
"""
|
|
self._ensure_client()
|
|
|
|
try:
|
|
# 支付宝金额单位是元,需要从分转换
|
|
amount_yuan = f"{total_amount / 100:.2f}"
|
|
|
|
order_string = self._client.api_alipay_trade_app_pay(
|
|
out_trade_no=out_trade_no,
|
|
total_amount=amount_yuan,
|
|
subject=subject,
|
|
notify_url=self._config.notify_url,
|
|
)
|
|
|
|
if order_string:
|
|
logger.info(f"支付宝订单创建成功: {out_trade_no}")
|
|
return PaymentResult(
|
|
success=True,
|
|
payment_method="alipay",
|
|
out_trade_no=out_trade_no,
|
|
alipay_order_string=order_string,
|
|
)
|
|
else:
|
|
raise PaymentCreateError("支付宝下单失败: 返回空订单字符串")
|
|
|
|
except PaymentCreateError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"支付宝订单创建异常: {e}")
|
|
raise PaymentCreateError(f"支付宝下单异常: {e}")
|
|
|
|
def verify_notify(self, params: Dict[str, str]) -> NotifyResult:
|
|
"""
|
|
验证支付宝异步通知签名
|
|
|
|
Args:
|
|
params: 通知参数字典(从 POST form 解析)
|
|
|
|
Returns:
|
|
NotifyResult 包含交易信息
|
|
"""
|
|
self._ensure_client()
|
|
|
|
try:
|
|
# 提取签名和签名类型
|
|
sign = params.pop("sign", None)
|
|
sign_type = params.pop("sign_type", None)
|
|
|
|
if not sign:
|
|
raise PaymentNotifyError("支付宝回调缺少签名参数")
|
|
|
|
# 验证签名
|
|
success = self._client.verify(params, sign)
|
|
|
|
if success:
|
|
trade_status = params.get("trade_status", "")
|
|
out_trade_no = params.get("out_trade_no", "")
|
|
trade_no = params.get("trade_no", "")
|
|
total_amount_str = params.get("total_amount", "0")
|
|
|
|
# 将元转换为分
|
|
total_amount = int(float(total_amount_str) * 100)
|
|
|
|
logger.info(
|
|
f"支付宝回调验证成功: out_trade_no={out_trade_no}, "
|
|
f"trade_status={trade_status}, amount={total_amount}"
|
|
)
|
|
|
|
return NotifyResult(
|
|
success=True,
|
|
out_trade_no=out_trade_no,
|
|
trade_no=trade_no,
|
|
total_amount=total_amount,
|
|
trade_status=trade_status,
|
|
)
|
|
else:
|
|
raise PaymentNotifyError("支付宝回调验签失败")
|
|
|
|
except PaymentNotifyError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"支付宝回调处理异常: {e}")
|
|
raise PaymentNotifyError(f"支付宝回调处理失败: {e}")
|
|
|
|
def query_order(self, out_trade_no: str) -> PaymentStatus:
|
|
"""
|
|
主动查询支付宝订单状态
|
|
|
|
Args:
|
|
out_trade_no: 商户订单号
|
|
|
|
Returns:
|
|
PaymentStatus 订单状态
|
|
"""
|
|
self._ensure_client()
|
|
|
|
try:
|
|
result = self._client.api_alipay_trade_query(out_trade_no=out_trade_no)
|
|
|
|
if result:
|
|
code = result.get("code", "")
|
|
if code == "10000":
|
|
trade_status = result.get("trade_status", "")
|
|
trade_no = result.get("trade_no", "")
|
|
total_amount_str = result.get("total_amount", "0")
|
|
total_amount = int(float(total_amount_str) * 100)
|
|
|
|
# 将支付宝状态映射到统一状态
|
|
unified_status = self._map_trade_status(trade_status)
|
|
|
|
return PaymentStatus(
|
|
success=True,
|
|
out_trade_no=out_trade_no,
|
|
trade_no=trade_no,
|
|
trade_status=unified_status,
|
|
total_amount=total_amount,
|
|
)
|
|
else:
|
|
error_msg = result.get("sub_msg", result.get("msg", "未知错误"))
|
|
raise PaymentQueryError(f"查询支付宝订单失败: {error_msg}")
|
|
else:
|
|
raise PaymentQueryError("查询支付宝订单返回空结果")
|
|
|
|
except PaymentQueryError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"查询支付宝订单异常: {e}")
|
|
raise PaymentQueryError(f"查询支付宝订单异常: {e}")
|
|
|
|
def close_order(self, out_trade_no: str) -> bool:
|
|
"""
|
|
关闭支付宝订单
|
|
|
|
Args:
|
|
out_trade_no: 商户订单号
|
|
|
|
Returns:
|
|
是否关闭成功
|
|
"""
|
|
self._ensure_client()
|
|
|
|
try:
|
|
result = self._client.api_alipay_trade_close(out_trade_no=out_trade_no)
|
|
if result and result.get("code") == "10000":
|
|
logger.info(f"支付宝订单已关闭: {out_trade_no}")
|
|
return True
|
|
else:
|
|
error_msg = result.get("sub_msg", "未知错误") if result else "空结果"
|
|
logger.warning(f"关闭支付宝订单失败: {error_msg}")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"关闭支付宝订单异常: {e}")
|
|
return False
|
|
|
|
@staticmethod
|
|
def _map_trade_status(alipay_status: str) -> str:
|
|
"""将支付宝交易状态映射到统一状态"""
|
|
status_map = {
|
|
"WAIT_BUYER_PAY": "NOTPAY",
|
|
"TRADE_CLOSED": "CLOSED",
|
|
"TRADE_SUCCESS": "SUCCESS",
|
|
"TRADE_FINISHED": "SUCCESS",
|
|
}
|
|
return status_map.get(alipay_status, alipay_status)
|