geo/backend/app/services/payment/alipay.py

244 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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