feat: 新增后端支付模块,支持微信和支付宝

- 新增api/payment/支付服务(微信、支付宝)
- 新增api/routers/payment.py支付路由
- 更新database/models.py支付相关模型
- 新增数据库迁移文件(订单表、用户订阅字段)
- 更新main.py、requirements.txt、.env.production

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
iammm0
2026-02-10 14:23:29 +08:00
parent 6526c08c3a
commit e39fd97e06
14 changed files with 1351 additions and 5 deletions

23
api/payment/__init__.py Normal file
View File

@@ -0,0 +1,23 @@
"""
独立可复用的支付模块
支持微信支付 APP 支付和支付宝 APP 支付
"""
from .service import PaymentService
from .config import PaymentConfig
from .exceptions import (
PaymentError,
PaymentConfigError,
PaymentCreateError,
PaymentNotifyError,
PaymentQueryError,
)
__all__ = [
"PaymentService",
"PaymentConfig",
"PaymentError",
"PaymentConfigError",
"PaymentCreateError",
"PaymentNotifyError",
"PaymentQueryError",
]

222
api/payment/alipay_pay.py Normal file
View File

@@ -0,0 +1,222 @@
"""
支付宝 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)

96
api/payment/config.py Normal file
View File

@@ -0,0 +1,96 @@
"""
支付模块配置
从环境变量读取微信支付和支付宝的商户密钥等配置
"""
import os
import logging
from dataclasses import dataclass, field
from typing import Optional
logger = logging.getLogger(__name__)
@dataclass
class WeChatPayConfig:
"""微信支付配置"""
app_id: str = "" # 应用 APPID
mch_id: str = "" # 商户号
api_v3_key: str = "" # APIv3 密钥
private_key_path: str = "" # 商户 API 私钥文件路径(与 private_key 二选一)
private_key: str = "" # 商户 API 私钥内容PEM 字符串,可放环境变量,与 private_key_path 二选一)
cert_serial_no: str = "" # 商户证书序列号
notify_url: str = "" # 支付结果回调地址
@property
def is_configured(self) -> bool:
has_key = bool(self.private_key.strip()) or bool(self.private_key_path.strip())
return all([
self.app_id, self.mch_id, self.api_v3_key,
has_key, self.cert_serial_no, self.notify_url
])
@dataclass
class AlipayConfig:
"""支付宝配置"""
app_id: str = "" # 应用 APPID
private_key: str = "" # 应用私钥(字符串内容)
alipay_public_key: str = "" # 支付宝公钥(字符串内容)
notify_url: str = "" # 支付结果回调地址
sign_type: str = "RSA2" # 签名类型,默认 RSA2
@property
def is_configured(self) -> bool:
return all([
self.app_id, self.private_key,
self.alipay_public_key, self.notify_url
])
@dataclass
class PaymentConfig:
"""支付模块总配置"""
wechat: WeChatPayConfig = field(default_factory=WeChatPayConfig)
alipay: AlipayConfig = field(default_factory=AlipayConfig)
# 支付宝是否视为「开发中」不可用(默认 True上线时设 ALIPAY_UNDER_DEVELOPMENT=false
alipay_under_development: bool = True
@classmethod
def from_env(cls) -> "PaymentConfig":
"""从环境变量加载配置"""
# 微信私钥:优先使用 WECHAT_PAY_PRIVATE_KEYPEM 内容),否则用 WECHAT_PAY_PRIVATE_KEY_PATH文件路径
wechat_private_key = os.getenv("WECHAT_PAY_PRIVATE_KEY", "").strip()
if wechat_private_key and "\\n" in wechat_private_key:
wechat_private_key = wechat_private_key.replace("\\n", "\n")
config = cls(
wechat=WeChatPayConfig(
app_id=os.getenv("WECHAT_PAY_APP_ID", ""),
mch_id=os.getenv("WECHAT_PAY_MCH_ID", ""),
api_v3_key=os.getenv("WECHAT_PAY_API_V3_KEY", ""),
private_key_path=os.getenv("WECHAT_PAY_PRIVATE_KEY_PATH", ""),
private_key=wechat_private_key,
cert_serial_no=os.getenv("WECHAT_PAY_CERT_SERIAL_NO", ""),
notify_url=os.getenv("WECHAT_PAY_NOTIFY_URL", ""),
),
alipay=AlipayConfig(
app_id=os.getenv("ALIPAY_APP_ID", ""),
private_key=os.getenv("ALIPAY_PRIVATE_KEY", ""),
alipay_public_key=os.getenv("ALIPAY_PUBLIC_KEY", ""),
notify_url=os.getenv("ALIPAY_NOTIFY_URL", ""),
sign_type=os.getenv("ALIPAY_SIGN_TYPE", "RSA2"),
),
alipay_under_development=os.getenv("ALIPAY_UNDER_DEVELOPMENT", "true").lower() in ("true", "1", "yes"),
)
# 日志输出配置状态
if config.wechat.is_configured:
logger.info(f"微信支付配置已加载: APP_ID={config.wechat.app_id}, MCH_ID={config.wechat.mch_id}")
else:
logger.warning("微信支付配置不完整,微信支付将不可用")
if config.alipay.is_configured:
logger.info(f"支付宝配置已加载: APP_ID={config.alipay.app_id}")
else:
logger.warning("支付宝配置不完整,支付宝支付将不可用")
return config

40
api/payment/exceptions.py Normal file
View File

@@ -0,0 +1,40 @@
"""
支付模块异常定义
"""
class PaymentError(Exception):
"""支付基础异常"""
def __init__(self, message: str = "支付异常", code: str = "PAYMENT_ERROR"):
self.message = message
self.code = code
super().__init__(self.message)
class PaymentConfigError(PaymentError):
"""支付配置异常"""
def __init__(self, message: str = "支付配置错误"):
super().__init__(message=message, code="PAYMENT_CONFIG_ERROR")
class PaymentCreateError(PaymentError):
"""创建支付订单异常"""
def __init__(self, message: str = "创建支付订单失败"):
super().__init__(message=message, code="PAYMENT_CREATE_ERROR")
class PaymentNotifyError(PaymentError):
"""支付回调通知处理异常"""
def __init__(self, message: str = "支付回调处理失败"):
super().__init__(message=message, code="PAYMENT_NOTIFY_ERROR")
class PaymentQueryError(PaymentError):
"""查询支付状态异常"""
def __init__(self, message: str = "查询支付状态失败"):
super().__init__(message=message, code="PAYMENT_QUERY_ERROR")

85
api/payment/schemas.py Normal file
View File

@@ -0,0 +1,85 @@
"""
支付模块 Pydantic 模型定义
"""
from pydantic import BaseModel
from typing import Optional, Dict, Any
class PaymentResult(BaseModel):
"""创建支付订单的返回结果"""
success: bool
payment_method: str # wechat / alipay
out_trade_no: str # 内部订单号
# 微信支付参数APP 调起支付所需)
wechat_params: Optional[Dict[str, str]] = None
# 支付宝 order_stringAPP 调起支付所需)
alipay_order_string: Optional[str] = None
error_message: Optional[str] = None
class NotifyResult(BaseModel):
"""支付回调通知处理结果"""
success: bool
out_trade_no: Optional[str] = None # 内部订单号
trade_no: Optional[str] = None # 第三方交易号
total_amount: Optional[int] = None # 金额(分)
trade_status: Optional[str] = None # 交易状态
error_message: Optional[str] = None
class PaymentStatus(BaseModel):
"""查询支付状态的返回结果"""
success: bool
out_trade_no: str
trade_no: Optional[str] = None
trade_status: str # SUCCESS / NOTPAY / CLOSED / REFUND / PAYERROR
total_amount: Optional[int] = None
error_message: Optional[str] = None
# === API 请求/响应模型 ===
class CreateOrderRequest(BaseModel):
"""创建订单请求"""
plan_id: str # 套餐 ID
payment_method: str # wechat / alipay
class CreateOrderResponse(BaseModel):
"""创建订单响应"""
order_id: str # 内部订单号
payment_method: str # 支付方式
# 微信支付调起参数
wechat_params: Optional[Dict[str, str]] = None
# 支付宝 order string
alipay_order_string: Optional[str] = None
class OrderStatusResponse(BaseModel):
"""订单状态查询响应"""
order_id: str
plan_id: str
plan_name: str
amount: int # 金额(分)
currency: str
payment_method: str
status: str # pending / paid / failed / cancelled / refunded
trade_no: Optional[str] = None
created_at: str
paid_at: Optional[str] = None
class OrderListResponse(BaseModel):
"""订单列表项"""
id: str
plan_id: str
plan_name: str
amount: int
currency: str
status: str
payment_method: str
created_at: str
paid_at: Optional[str] = None

173
api/payment/service.py Normal file
View File

@@ -0,0 +1,173 @@
"""
统一支付服务门面
提供统一的支付接口,屏蔽微信/支付宝的底层差异
"""
import logging
from typing import Dict, Optional
from .config import PaymentConfig
from .wechat_pay import WeChatPayClient
from .alipay_pay import AlipayClient
from .schemas import PaymentResult, NotifyResult, PaymentStatus
from .exceptions import PaymentError, PaymentConfigError
logger = logging.getLogger(__name__)
# 支持的支付方式
PAYMENT_METHOD_WECHAT = "wechat"
PAYMENT_METHOD_ALIPAY = "alipay"
SUPPORTED_METHODS = {PAYMENT_METHOD_WECHAT, PAYMENT_METHOD_ALIPAY}
class PaymentService:
"""
统一支付服务
使用方式:
config = PaymentConfig.from_env()
payment_service = PaymentService(config)
# 创建支付订单
result = payment_service.create_payment("wechat", order_no, 9900, "高级版套餐")
# 处理回调
notify = payment_service.handle_wechat_notify(headers, body)
# 查询状态
status = payment_service.query_payment("wechat", order_no)
"""
def __init__(self, config: PaymentConfig):
self._config = config
self._wechat_client: Optional[WeChatPayClient] = None
self._alipay_client: Optional[AlipayClient] = None
@property
def wechat_client(self) -> WeChatPayClient:
"""获取微信支付客户端(懒加载)"""
if self._wechat_client is None:
self._wechat_client = WeChatPayClient(self._config.wechat)
return self._wechat_client
@property
def alipay_client(self) -> AlipayClient:
"""获取支付宝客户端(懒加载)"""
if self._alipay_client is None:
self._alipay_client = AlipayClient(self._config.alipay)
return self._alipay_client
def create_payment(
self,
method: str,
out_trade_no: str,
total_amount: int,
description: str,
) -> PaymentResult:
"""
创建支付订单
Args:
method: 支付方式 (wechat / alipay)
out_trade_no: 商户订单号
total_amount: 金额(单位:分)
description: 商品描述
Returns:
PaymentResult
"""
self._validate_method(method)
if method == PAYMENT_METHOD_WECHAT:
return self.wechat_client.create_app_order(
out_trade_no=out_trade_no,
total_amount=total_amount,
description=description,
)
else:
return self.alipay_client.create_app_order(
out_trade_no=out_trade_no,
total_amount=total_amount,
subject=description,
)
def handle_wechat_notify(
self, headers: Dict[str, str], body: str
) -> NotifyResult:
"""
处理微信支付异步回调通知
Args:
headers: 请求头
body: 请求体
Returns:
NotifyResult
"""
return self.wechat_client.verify_notify(headers=headers, body=body)
def handle_alipay_notify(self, params: Dict[str, str]) -> NotifyResult:
"""
处理支付宝异步回调通知
Args:
params: 回调参数
Returns:
NotifyResult
"""
return self.alipay_client.verify_notify(params=params)
def query_payment(self, method: str, out_trade_no: str) -> PaymentStatus:
"""
查询支付订单状态
Args:
method: 支付方式 (wechat / alipay)
out_trade_no: 商户订单号
Returns:
PaymentStatus
"""
self._validate_method(method)
if method == PAYMENT_METHOD_WECHAT:
return self.wechat_client.query_order(out_trade_no=out_trade_no)
else:
return self.alipay_client.query_order(out_trade_no=out_trade_no)
def close_payment(self, method: str, out_trade_no: str) -> bool:
"""
关闭支付订单
Args:
method: 支付方式 (wechat / alipay)
out_trade_no: 商户订单号
Returns:
是否关闭成功
"""
self._validate_method(method)
if method == PAYMENT_METHOD_WECHAT:
return self.wechat_client.close_order(out_trade_no=out_trade_no)
else:
return self.alipay_client.close_order(out_trade_no=out_trade_no)
def is_method_available(self, method: str) -> bool:
"""检查指定支付方式是否可用"""
if method == PAYMENT_METHOD_WECHAT:
return self._config.wechat.is_configured
elif method == PAYMENT_METHOD_ALIPAY:
if getattr(self._config, "alipay_under_development", True):
return False
return self._config.alipay.is_configured
return False
@staticmethod
def _validate_method(method: str):
"""验证支付方式"""
if method not in SUPPORTED_METHODS:
raise PaymentError(
f"不支持的支付方式: {method},支持的方式: {', '.join(SUPPORTED_METHODS)}",
code="UNSUPPORTED_METHOD",
)

270
api/payment/wechat_pay.py Normal file
View File

@@ -0,0 +1,270 @@
"""
微信支付 API v3 封装
使用 wechatpayv3 SDK 实现 APP 支付
"""
import json
import logging
import os
import time
from typing import Optional, Dict, Any
from .config import WeChatPayConfig
from .schemas import PaymentResult, NotifyResult, PaymentStatus
from .exceptions import PaymentConfigError, PaymentCreateError, PaymentNotifyError, PaymentQueryError
logger = logging.getLogger(__name__)
def _normalize_pem_key(key: str) -> str:
"""
规范化 PEM 私钥字符串,便于 wechatpayv3 正确解析。
- 环境变量中常将换行写成字面量 \\n需转为真实换行
- 统一为 \\n 换行,去除 \\r
"""
if not key or not key.strip():
return key
key = key.strip()
key = key.replace("\\n", "\n")
key = key.replace("\r\n", "\n").replace("\r", "\n")
return key
class WeChatPayClient:
"""微信支付客户端"""
def __init__(self, config: WeChatPayConfig):
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 wechatpayv3 import WeChatPay, WeChatPayType
# 商户私钥:优先用配置中的 PEM 字符串,否则从文件读取
if self._config.private_key and self._config.private_key.strip():
private_key = self._config.private_key.strip()
else:
key_path = self._config.private_key_path.strip()
if not key_path:
raise PaymentConfigError("未配置微信支付商户私钥:请设置 WECHAT_PAY_PRIVATE_KEY 或 WECHAT_PAY_PRIVATE_KEY_PATH")
# 若为相对路径,相对于当前工作目录解析(建议使用绝对路径或与 main.py 同级的路径)
if not os.path.isabs(key_path):
key_path = os.path.abspath(key_path)
with open(key_path, "r", encoding="utf-8") as f:
private_key = f.read()
# 统一 PEM 格式:换行符规范化、字面量 \n 转真实换行(环境变量或部分编辑器会写成 \n
private_key = _normalize_pem_key(private_key)
if not private_key or "-----BEGIN" not in private_key:
raise PaymentConfigError(
"微信支付商户私钥格式错误:需为 PEM 格式(以 -----BEGIN PRIVATE KEY----- 开头)。"
"若使用 WECHAT_PAY_PRIVATE_KEY_PATH请确认文件内容正确若用 WECHAT_PAY_PRIVATE_KEY请确认环境变量为完整 PEM。"
)
self._client = WeChatPay(
wechatpay_type=WeChatPayType.APP,
mchid=self._config.mch_id,
private_key=private_key,
cert_serial_no=self._config.cert_serial_no,
appid=self._config.app_id,
apiv3_key=self._config.api_v3_key,
notify_url=self._config.notify_url,
)
logger.info("微信支付客户端初始化成功")
except FileNotFoundError:
raise PaymentConfigError(
f"微信支付商户私钥文件不存在: {self._config.private_key_path}(当前工作目录: {os.getcwd()}"
)
except PaymentConfigError:
raise
except Exception as e:
raise PaymentConfigError(
f"微信支付客户端初始化失败: {e}"
"若使用文件路径,请确认文件为 PEM 格式且路径正确;或改用 WECHAT_PAY_PRIVATE_KEY 直接配置私钥内容。"
)
def create_app_order(
self,
out_trade_no: str,
total_amount: int,
description: str,
) -> PaymentResult:
"""
创建 APP 支付预订单
Args:
out_trade_no: 商户订单号
total_amount: 金额(单位:分)
description: 商品描述
Returns:
PaymentResult 包含调起支付所需的参数
"""
self._ensure_client()
try:
code, message = self._client.pay(
description=description,
out_trade_no=out_trade_no,
amount={"total": total_amount, "currency": "CNY"},
pay_type=self._get_pay_type(),
)
result = json.loads(message)
if code in range(200, 300):
prepay_id = result.get("prepay_id", "")
# 生成 APP 调起支付所需的参数
timestamp = str(int(time.time()))
nonce_str = out_trade_no # 使用订单号作为随机字符串的基础
# 使用 SDK 的签名方法生成 APP 支付参数
pay_params = self._client.sign(
data=[self._config.app_id, timestamp, nonce_str, prepay_id]
)
wechat_params = {
"appId": self._config.app_id,
"partnerId": self._config.mch_id,
"prepayId": prepay_id,
"nonceStr": nonce_str,
"timeStamp": timestamp,
"sign": pay_params,
"packageValue": "Sign=WXPay",
}
logger.info(f"微信支付预订单创建成功: {out_trade_no}")
return PaymentResult(
success=True,
payment_method="wechat",
out_trade_no=out_trade_no,
wechat_params=wechat_params,
)
else:
error_msg = result.get("message", "未知错误")
logger.error(f"微信支付预订单创建失败: {code} - {error_msg}")
raise PaymentCreateError(f"微信支付下单失败: {error_msg}")
except PaymentCreateError:
raise
except Exception as e:
logger.error(f"微信支付预订单创建异常: {e}")
raise PaymentCreateError(f"微信支付下单异常: {e}")
def verify_notify(self, headers: Dict[str, str], body: str) -> NotifyResult:
"""
验证并解密微信支付回调通知
Args:
headers: 请求头(包含签名等信息)
body: 请求体(加密的通知数据)
Returns:
NotifyResult 包含解密后的交易信息
"""
self._ensure_client()
try:
result = self._client.callback(headers=headers, body=body)
if result:
event_data = result if isinstance(result, dict) else json.loads(result)
trade_state = event_data.get("trade_state", "")
out_trade_no = event_data.get("out_trade_no", "")
transaction_id = event_data.get("transaction_id", "")
amount_info = event_data.get("amount", {})
total_amount = amount_info.get("total", 0)
logger.info(
f"微信支付回调验证成功: out_trade_no={out_trade_no}, "
f"trade_state={trade_state}, amount={total_amount}"
)
return NotifyResult(
success=True,
out_trade_no=out_trade_no,
trade_no=transaction_id,
total_amount=total_amount,
trade_status=trade_state,
)
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:
code, message = self._client.query(out_trade_no=out_trade_no)
result = json.loads(message)
if code in range(200, 300):
trade_state = result.get("trade_state", "NOTPAY")
transaction_id = result.get("transaction_id", "")
amount_info = result.get("amount", {})
total_amount = amount_info.get("total", 0)
return PaymentStatus(
success=True,
out_trade_no=out_trade_no,
trade_no=transaction_id,
trade_status=trade_state,
total_amount=total_amount,
)
else:
error_msg = result.get("message", "未知错误")
raise PaymentQueryError(f"查询微信支付订单失败: {error_msg}")
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:
code, message = self._client.close(out_trade_no=out_trade_no)
if code in range(200, 300):
logger.info(f"微信支付订单已关闭: {out_trade_no}")
return True
else:
logger.warning(f"关闭微信支付订单失败: {code} - {message}")
return False
except Exception as e:
logger.error(f"关闭微信支付订单异常: {e}")
return False
def _get_pay_type(self):
"""获取支付类型枚举"""
from wechatpayv3 import WeChatPayType
return WeChatPayType.APP