222 lines
8.0 KiB
Python
222 lines
8.0 KiB
Python
import jingrow
|
||
import traceback
|
||
import urllib.parse
|
||
import json
|
||
import requests
|
||
from datetime import datetime
|
||
|
||
class PayPalAPI:
|
||
def __init__(self):
|
||
# 初始化时不立即加载设置
|
||
self.client_id = None
|
||
self.client_secret = None
|
||
self.server_url = None
|
||
self.notify_url = None
|
||
self.sandbox = False
|
||
self.access_token = None
|
||
self.token_expires_at = None
|
||
|
||
def _initialize(self):
|
||
# 每次都重新加载配置,确保使用最新设置
|
||
# 从 Jcloud Settings 获取配置
|
||
settings = jingrow.get_single("Jcloud Settings")
|
||
|
||
# 配置PayPal客户端
|
||
self.client_id = settings.paypal_client_id
|
||
self.client_secret = settings.paypal_client_secret
|
||
self.server_url = settings.paypal_server_url
|
||
self.notify_url = settings.paypal_notify_url
|
||
self.sandbox = settings.paypal_sandbox
|
||
|
||
# 重置访问令牌
|
||
self.access_token = None
|
||
self.token_expires_at = None
|
||
|
||
def _get_access_token(self):
|
||
"""获取PayPal API访问令牌"""
|
||
# 检查令牌是否有效
|
||
if self.access_token and self.token_expires_at and datetime.now() < self.token_expires_at:
|
||
return self.access_token
|
||
|
||
# 构建获取令牌的URL
|
||
token_url = f"{self.server_url}/v1/oauth2/token"
|
||
|
||
# 准备请求数据
|
||
data = {
|
||
"grant_type": "client_credentials"
|
||
}
|
||
|
||
# 发送请求
|
||
try:
|
||
response = requests.post(
|
||
token_url,
|
||
data=data,
|
||
auth=(self.client_id, self.client_secret),
|
||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||
)
|
||
|
||
response.raise_for_status()
|
||
token_data = response.json()
|
||
|
||
# 保存访问令牌和过期时间
|
||
self.access_token = token_data["access_token"]
|
||
|
||
# 计算过期时间(提前5分钟过期)
|
||
expires_in = token_data["expires_in"] - 300
|
||
self.token_expires_at = datetime.now() + jingrow.utils.relativedelta(seconds=expires_in)
|
||
|
||
return self.access_token
|
||
|
||
except Exception as e:
|
||
error_message = (
|
||
"获取PayPal访问令牌失败:\n"
|
||
f"异常信息: {str(e)}\n"
|
||
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||
)
|
||
jingrow.log_error(error_message, "PayPal Token Error")
|
||
raise
|
||
|
||
def generate_payment_url(self, order_id, amount, subject, team_name, return_url=None, cancel_url=None):
|
||
self._initialize()
|
||
|
||
# 获取访问令牌
|
||
access_token = self._get_access_token()
|
||
|
||
# 构建创建订单的URL
|
||
create_order_url = f"{self.server_url}/v2/checkout/orders"
|
||
|
||
# 构建请求内容
|
||
order_data = {
|
||
"intent": "CAPTURE",
|
||
"purchase_units": [{
|
||
"amount": {
|
||
"currency_code": "USD", # 目前只支持USD
|
||
"value": str(amount)
|
||
},
|
||
"description": subject
|
||
}],
|
||
"application_context": {
|
||
"return_url": return_url or f"{jingrow.utils.get_url()}/api/action/jcloud.api.billing.handle_paypal_return",
|
||
"cancel_url": cancel_url or f"{jingrow.utils.get_url()}/api/action/jcloud.api.billing.handle_paypal_cancel",
|
||
"brand_name": "Jingrow",
|
||
"user_action": "PAY_NOW",
|
||
"shipping_preference": "NO_SHIPPING"
|
||
}
|
||
}
|
||
|
||
# 发送创建订单请求
|
||
try:
|
||
response = requests.post(
|
||
create_order_url,
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"Authorization": f"Bearer {access_token}"
|
||
},
|
||
json=order_data
|
||
)
|
||
|
||
response.raise_for_status()
|
||
order_response = response.json()
|
||
|
||
# 提取支付链接
|
||
approval_url = None
|
||
for link in order_response.get("links", []):
|
||
if link["rel"] == "approve":
|
||
approval_url = link["href"]
|
||
break
|
||
|
||
if not approval_url:
|
||
raise Exception("未找到PayPal支付链接")
|
||
|
||
return approval_url
|
||
|
||
except Exception as e:
|
||
error_message = (
|
||
"创建PayPal订单失败:\n"
|
||
f"异常信息: {str(e)}\n"
|
||
f"请求数据: {json.dumps(order_data)}\n"
|
||
f"响应内容: {response.text if 'response' in locals() else '无响应'}\n"
|
||
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||
)
|
||
jingrow.log_error(error_message, "PayPal Order Error")
|
||
|
||
# 如果API调用失败,返回模拟链接用于测试
|
||
jingrow.log_error("使用模拟PayPal支付链接", "PayPal Fallback")
|
||
return f"https://www.paypal.com/checkoutnow?token={order_id}"
|
||
|
||
def verify_webhook(self, headers, body):
|
||
"""验证PayPal webhook通知"""
|
||
self._initialize()
|
||
|
||
try:
|
||
# 获取签名和证书
|
||
transmission_id = headers.get("PAYPAL-TRANSMISSION-ID")
|
||
transmission_time = headers.get("PAYPAL-TRANSMISSION-TIME")
|
||
cert_url = headers.get("PAYPAL-CERT-URL")
|
||
auth_algo = headers.get("PAYPAL-AUTH-ALGO")
|
||
transmission_sig = headers.get("PAYPAL-TRANSMISSION-SIG")
|
||
|
||
# 验证必要头信息
|
||
if not all([transmission_id, transmission_time, cert_url, auth_algo, transmission_sig]):
|
||
return False
|
||
|
||
# 获取PayPal公钥证书
|
||
response = requests.get(cert_url)
|
||
response.raise_for_status()
|
||
public_key = response.text
|
||
|
||
# 构建用于验证的字符串
|
||
webhook_id = jingrow.db.get_single_value("Jcloud Settings", "paypal_webhook_id") or ""
|
||
body_bytes = body if isinstance(body, bytes) else body.encode("utf-8")
|
||
|
||
# 注意:完整的验证实现需要使用加密库验证签名
|
||
# 这里简化处理,实际项目中需要实现完整的签名验证
|
||
jingrow.log_error(f"PayPal webhook验证: {transmission_id}", "PayPal Webhook")
|
||
|
||
# 返回True表示验证成功(简化处理)
|
||
return True
|
||
|
||
except Exception as e:
|
||
error_message = (
|
||
"验证PayPal webhook失败:\n"
|
||
f"异常信息: {str(e)}\n"
|
||
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||
)
|
||
jingrow.log_error(error_message, "PayPal Webhook Error")
|
||
return False
|
||
|
||
def capture_payment(self, order_id):
|
||
"""捕获PayPal支付金额"""
|
||
self._initialize()
|
||
|
||
# 获取访问令牌
|
||
access_token = self._get_access_token()
|
||
|
||
# 构建捕获支付的URL
|
||
capture_url = f"{self.server_url}/v2/checkout/orders/{order_id}/capture"
|
||
|
||
try:
|
||
response = requests.post(
|
||
capture_url,
|
||
headers={
|
||
"Content-Type": "application/json",
|
||
"Authorization": f"Bearer {access_token}"
|
||
}
|
||
)
|
||
|
||
response.raise_for_status()
|
||
capture_data = response.json()
|
||
|
||
return capture_data
|
||
|
||
except Exception as e:
|
||
error_message = (
|
||
f"捕获PayPal支付失败 (Order ID: {order_id}):\n"
|
||
f"异常信息: {str(e)}\n"
|
||
f"响应内容: {response.text if 'response' in locals() else '无响应'}\n"
|
||
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||
)
|
||
jingrow.log_error(error_message, "PayPal Capture Error")
|
||
raise
|
||
|