import json import logging import urllib.parse import uuid from datetime import datetime import requests from app.config import settings from app.services.payment.base import PaymentGateway, PaymentOrder, PaymentCallback from app.services.payment.mock_gateway import MockGateway logger = logging.getLogger(__name__) ALIPAY_GATEWAY_URL = "https://openapi.alipay.com/gateway.do" class AlipayGateway(PaymentGateway): def __init__(self): self.app_id = settings.ALIPAY_APP_ID self.private_key_path = settings.ALIPAY_PRIVATE_KEY_PATH self.public_key_path = settings.ALIPAY_PUBLIC_KEY_PATH self.notify_url = settings.ALIPAY_NOTIFY_URL self._private_key = None self._alipay_public_key = None def _is_configured(self) -> bool: return bool(self.app_id and self.private_key_path and self.public_key_path) def _load_private_key(self): if self._private_key is not None: return self._private_key try: with open(self.private_key_path, "r") as f: key_data = f.read() from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.backends import default_backend self._private_key = load_pem_private_key( key_data.encode("utf-8"), password=None, backend=default_backend() ) return self._private_key except Exception as e: logger.error("[Alipay] 加载私钥失败: %s", e) return None def _load_alipay_public_key(self): if self._alipay_public_key is not None: return self._alipay_public_key try: with open(self.public_key_path, "r") as f: key_data = f.read() from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.hazmat.backends import default_backend self._alipay_public_key = load_pem_public_key( key_data.encode("utf-8"), backend=default_backend() ) return self._alipay_public_key except Exception as e: logger.error("[Alipay] 加载支付宝公钥失败: %s", e) return None def _build_sign_content(self, params: dict) -> str: sorted_params = sorted(params.items(), key=lambda x: x[0]) return "&".join(f"{k}={v}" for k, v in sorted_params if v) def _rsa2_sign(self, sign_content: str) -> str: private_key = self._load_private_key() if private_key is None: raise RuntimeError("Alipay private key not loaded") from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding signature = private_key.sign( sign_content.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256(), ) import base64 return base64.b64encode(signature).decode("utf-8") def _rsa2_verify(self, sign_content: str, sign: str) -> bool: public_key = self._load_alipay_public_key() if public_key is None: logger.warning("[Alipay] 支付宝公钥未加载,跳过签名验证") return False import base64 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding try: public_key.verify( base64.b64decode(sign), sign_content.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256(), ) return True except Exception: return False def _build_common_params(self, method: str) -> dict: return { "app_id": self.app_id, "method": method, "charset": "utf-8", "sign_type": "RSA2", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "notify_url": self.notify_url, } async def create_order( self, order_id: str, amount: float, description: str, user_id: str, plan: str ) -> PaymentOrder: if not self._is_configured(): logger.info("[Alipay] 未配置应用信息,降级为Mock支付") return await MockGateway().create_order(order_id, amount, description, user_id, plan) biz_content = json.dumps({ "out_trade_no": order_id, "total_amount": str(amount), "subject": description, "product_code": "QUICK_WAP_WAY", }) params = self._build_common_params("alipay.trade.wap.pay") params["biz_content"] = biz_content sign_content = self._build_sign_content(params) params["sign"] = self._rsa2_sign(sign_content) try: resp = requests.post( ALIPAY_GATEWAY_URL, data=params, timeout=10, ) resp.raise_for_status() if resp.headers.get("content-type", "").startswith("text/html"): pay_url = f"{ALIPAY_GATEWAY_URL}?{urllib.parse.urlencode(params)}" else: data = resp.json() resp_data = data.get("alipay_trade_wap_pay_response", {}) pay_url = resp_data.get("pay_url", f"{ALIPAY_GATEWAY_URL}?{urllib.parse.urlencode(params)}") return PaymentOrder( order_id=order_id, pay_url=pay_url, amount=amount, status="pending", ) except requests.RequestException as e: logger.error("[Alipay] 创建订单失败: %s", e) raise RuntimeError(f"Alipay create_order failed: {e}") from e async def verify_callback(self, request_data: dict) -> PaymentCallback: if not self._is_configured(): return await MockGateway().verify_callback(request_data) sign = request_data.get("sign", "") sign_type = request_data.get("sign_type", "RSA2") params = {k: v for k, v in request_data.items() if k not in ("sign", "sign_type") and v} sign_content = self._build_sign_content(params) if not self._rsa2_verify(sign_content, sign): logger.warning("[Alipay] 回调签名验证失败: order_id=%s", request_data.get("out_trade_no")) return PaymentCallback( order_id=request_data.get("out_trade_no", ""), payment_id=request_data.get("trade_no", ""), amount=float(request_data.get("total_amount", 0)), status="failed", raw_data=request_data, ) trade_status = request_data.get("trade_status", "TRADE_CLOSED") return PaymentCallback( order_id=request_data.get("out_trade_no", ""), payment_id=request_data.get("trade_no", ""), amount=float(request_data.get("total_amount", 0)), status="success" if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED") else "failed", raw_data=request_data, ) async def query_order(self, order_id: str) -> PaymentOrder: if not self._is_configured(): return await MockGateway().query_order(order_id) biz_content = json.dumps({"out_trade_no": order_id}) params = self._build_common_params("alipay.trade.query") params["biz_content"] = biz_content sign_content = self._build_sign_content(params) params["sign"] = self._rsa2_sign(sign_content) try: resp = requests.post(ALIPAY_GATEWAY_URL, data=params, timeout=10) resp.raise_for_status() data = resp.json() resp_data = data.get("alipay_trade_query_response", {}) trade_status = resp_data.get("trade_status", "WAIT_BUYER_PAY") total_amount = float(resp_data.get("total_amount", 0)) status_map = { "TRADE_SUCCESS": "completed", "TRADE_FINISHED": "completed", "WAIT_BUYER_PAY": "pending", "TRADE_CLOSED": "closed", } return PaymentOrder( order_id=order_id, pay_url="", amount=total_amount, status=status_map.get(trade_status, "pending"), ) except requests.RequestException as e: logger.error("[Alipay] 查询订单失败: %s", e) raise RuntimeError(f"Alipay query_order failed: {e}") from e async def refund(self, order_id: str, amount: float, reason: str = "") -> bool: if not self._is_configured(): return await MockGateway().refund(order_id, amount, reason) biz_content = json.dumps({ "out_trade_no": order_id, "refund_amount": str(amount), "refund_reason": reason or "用户申请退款", "out_request_no": f"R_{order_id}_{int(datetime.now().timestamp())}", }) params = self._build_common_params("alipay.trade.refund") params["biz_content"] = biz_content sign_content = self._build_sign_content(params) params["sign"] = self._rsa2_sign(sign_content) try: resp = requests.post(ALIPAY_GATEWAY_URL, data=params, timeout=10) resp.raise_for_status() data = resp.json() resp_data = data.get("alipay_trade_refund_response", {}) return resp_data.get("code") == "10000" except requests.RequestException as e: logger.error("[Alipay] 退款失败: %s", e) return False