""" 微信支付 API v3 封装(从 payment 迁入 app) """ import json from app.core.logging import get_logger import os import time from typing import Dict from app.features.payment.payment_config import WeChatPayConfig from app.features.payment.schemas import NotifyResult, PaymentResult, PaymentStatus from app.features.payment.payment_exceptions import ( PaymentConfigError, PaymentCreateError, PaymentNotifyError, PaymentQueryError, ) logger = get_logger(__name__) def _normalize_pem_key(key: str) -> str: if not key or not key.strip(): return key key = key.strip().lstrip("\ufeff").strip('"').strip("'") key = key.replace("\\n", "\n") key = key.replace("\r\n", "\n").replace("\r", "\n") return key def _resolve_key_path(key_path: str) -> str: if not key_path or not key_path.strip(): return key_path key_path = key_path.strip() if os.path.isabs(key_path): return key_path abs_cwd = os.path.abspath(key_path) if os.path.isfile(abs_cwd): return abs_cwd try: # app/features/payment/wechat_client.py -> api/ api_dir = os.path.dirname( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ) abs_api = os.path.normpath(os.path.join(api_dir, key_path)) if os.path.isfile(abs_api): return abs_api except Exception: pass return abs_cwd class WeChatPayClient: def __init__(self, config: WeChatPayConfig): self._config = config self._client = None def ensure_client(self): self._ensure_client() def _ensure_client(self): if not self._config.is_configured: raise PaymentConfigError("微信支付配置不完整,请检查环境变量") if self._client is None: try: from wechatpayv3 import WeChatPay, WeChatPayType if ( self._config.private_key_path and self._config.private_key_path.strip() ): key_path = _resolve_key_path(self._config.private_key_path) with open(key_path, "r", encoding="utf-8") as f: private_key = f.read() elif self._config.private_key and self._config.private_key.strip(): private_key = self._config.private_key.strip() else: raise PaymentConfigError( "未配置微信支付商户私钥:请设置 WECHAT_PAY_PRIVATE_KEY_PATH 或 WECHAT_PAY_PRIVATE_KEY" ) private_key = _normalize_pem_key(private_key) if not private_key or "-----BEGIN" not in private_key: raise PaymentConfigError( "微信支付商户私钥格式错误:需为 PEM 格式。" ) kwargs = dict( 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, ) if self._config.use_platform_public_key: if ( self._config.platform_public_key_path and self._config.platform_public_key_path.strip() ): key_path = _resolve_key_path( self._config.platform_public_key_path ) with open(key_path, "r", encoding="utf-8") as f: platform_pub_key = _normalize_pem_key(f.read()) elif ( self._config.platform_public_key and self._config.platform_public_key.strip() ): platform_pub_key = _normalize_pem_key( self._config.platform_public_key.strip() ) else: raise PaymentConfigError( "平台公钥模式需设置 WECHAT_PAY_PLATFORM_PUBLIC_KEY 或 WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH" ) if not platform_pub_key or "-----BEGIN" not in platform_pub_key: raise PaymentConfigError( "微信支付平台公钥格式错误,需为 PEM 格式" ) kwargs["public_key"] = platform_pub_key kwargs["public_key_id"] = ( self._config.platform_public_key_id.strip() ) else: kwargs["timeout"] = (10, 25) self._client = WeChatPay(**kwargs) logger.info("微信支付客户端初始化成功") except FileNotFoundError: key_path = ( _resolve_key_path(self._config.private_key_path) if self._config.private_key_path else self._config.private_key_path ) raise PaymentConfigError(f"微信支付商户私钥文件不存在: {key_path}") except PaymentConfigError: raise except Exception as e: msg = str(e).strip() if "platform certificate" in msg.lower(): raise PaymentConfigError( "无法获取微信支付平台证书。请改用平台公钥模式:设置 " "WECHAT_PAY_PLATFORM_PUBLIC_KEY 与 WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID。" ) raise PaymentConfigError(f"微信支付客户端初始化失败: {e}") def create_app_order( self, out_trade_no: str, total_amount: int, description: str, ) -> 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", "") timestamp = str(int(time.time())) nonce_str = out_trade_no 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("微信支付预订单创建成功: %s", 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", "未知错误") raise PaymentCreateError(f"微信支付下单失败: {error_msg}") except PaymentCreateError: raise except Exception as e: logger.error("微信支付预订单创建异常: %s", e) raise PaymentCreateError(f"微信支付下单异常: {e}") def verify_notify(self, headers: Dict[str, str], body: str) -> 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) return NotifyResult( success=True, out_trade_no=out_trade_no, trade_no=transaction_id, total_amount=total_amount, trade_status=trade_state, ) raise PaymentNotifyError("微信支付回调验签失败") except PaymentNotifyError: raise except Exception as e: logger.error("微信支付回调处理异常: %s", e) raise PaymentNotifyError(f"微信支付回调处理失败: {e}") def query_order(self, out_trade_no: str) -> 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, ) error_msg = result.get("message", "未知错误") raise PaymentQueryError(f"查询微信支付订单失败: {error_msg}") except PaymentQueryError: raise except Exception as e: logger.error("查询微信支付订单异常: %s", e) raise PaymentQueryError(f"查询微信支付订单异常: {e}") def close_order(self, out_trade_no: str) -> bool: self._ensure_client() try: code, message = self._client.close(out_trade_no=out_trade_no) if code in range(200, 300): logger.info("微信支付订单已关闭: %s", out_trade_no) return True logger.warning("关闭微信支付订单失败: %s - %s", code, message) return False except Exception as e: logger.error("关闭微信支付订单异常: %s", e) return False def _get_pay_type(self): from wechatpayv3 import WeChatPayType return WeChatPayType.APP