forked from xiaohei/taiji-AI-PAD
备份
This commit is contained in:
@@ -56,6 +56,13 @@ AUDIT_ACTIONS = {
|
||||
"billing.refund": "退款",
|
||||
"billing.adjust": "余额调整",
|
||||
|
||||
# PayPal 支付操作
|
||||
"payment.paypal.order_created": "PayPal订单创建",
|
||||
"payment.paypal.order_captured": "PayPal支付完成",
|
||||
"payment.paypal.order_failed": "PayPal支付失败",
|
||||
"payment.paypal.amount_mismatch": "PayPal金额不匹配",
|
||||
"payment.paypal.webhook_received": "PayPal Webhook接收",
|
||||
|
||||
# 供应商管理
|
||||
"provider.create": "创建供应商",
|
||||
"provider.update": "更新供应商",
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
PayPal 客户端封装
|
||||
|
||||
用于处理 PayPal 支付相关的 API 调用。
|
||||
支持创建订单、捕获支付、查询订单状态等操作。
|
||||
|
||||
重要:这是客户支付的真金白银,所有操作必须有完整的日志记录。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from paypalserversdk.http.auth.o_auth_2 import ClientCredentialsAuthCredentials
|
||||
from paypalserversdk.logging.configuration.api_logging_configuration import (
|
||||
LoggingConfiguration,
|
||||
RequestLoggingConfiguration,
|
||||
ResponseLoggingConfiguration,
|
||||
)
|
||||
from paypalserversdk.paypal_serversdk_client import PaypalServersdkClient
|
||||
from paypalserversdk.controllers.orders_controller import OrdersController
|
||||
from paypalserversdk.models.order_request import OrderRequest
|
||||
from paypalserversdk.models.checkout_payment_intent import CheckoutPaymentIntent
|
||||
from paypalserversdk.models.purchase_unit_request import PurchaseUnitRequest
|
||||
from paypalserversdk.models.amount_with_breakdown import AmountWithBreakdown
|
||||
from paypalserversdk.api_helper import APIHelper
|
||||
from paypalserversdk.configuration import Environment
|
||||
|
||||
from config import settings
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class PayPalClient:
|
||||
"""PayPal 客户端封装"""
|
||||
|
||||
def __init__(self):
|
||||
self._client: Optional[PaypalServersdkClient] = None
|
||||
self._orders_controller: Optional[OrdersController] = None
|
||||
|
||||
def _get_environment(self) -> Environment:
|
||||
"""获取 PayPal 环境配置"""
|
||||
if settings.paypal_environment == "production":
|
||||
return Environment.PRODUCTION
|
||||
return Environment.SANDBOX
|
||||
|
||||
def _get_client(self) -> PaypalServersdkClient:
|
||||
"""获取 PayPal SDK 客户端(懒加载)"""
|
||||
if self._client is None:
|
||||
self._client = PaypalServersdkClient(
|
||||
client_credentials_auth_credentials=ClientCredentialsAuthCredentials(
|
||||
o_auth_client_id=settings.paypal_client_id,
|
||||
o_auth_client_secret=settings.paypal_client_secret,
|
||||
),
|
||||
environment=self._get_environment(),
|
||||
logging_configuration=LoggingConfiguration(
|
||||
log_level=logging.INFO,
|
||||
mask_sensitive_headers=True,
|
||||
request_logging_config=RequestLoggingConfiguration(
|
||||
log_body=True
|
||||
),
|
||||
response_logging_config=ResponseLoggingConfiguration(
|
||||
log_body=True
|
||||
),
|
||||
),
|
||||
)
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def orders_controller(self) -> OrdersController:
|
||||
"""获取订单控制器"""
|
||||
if self._orders_controller is None:
|
||||
self._orders_controller = self._get_client().orders
|
||||
return self._orders_controller
|
||||
|
||||
async def create_order(self, amount: float, currency: str = "USD") -> dict:
|
||||
"""
|
||||
创建 PayPal 订单
|
||||
|
||||
Args:
|
||||
amount: 支付金额
|
||||
currency: 货币类型,默认 USD
|
||||
|
||||
Returns:
|
||||
包含 order_id 和状态的字典
|
||||
|
||||
Raises:
|
||||
Exception: PayPal API 调用失败
|
||||
"""
|
||||
order_request = OrderRequest(
|
||||
intent=CheckoutPaymentIntent.CAPTURE,
|
||||
purchase_units=[
|
||||
PurchaseUnitRequest(
|
||||
amount=AmountWithBreakdown(
|
||||
currency_code=currency,
|
||||
value=f"{amount:.2f}",
|
||||
),
|
||||
description="Taiji AI Platform - EU Balance Recharge",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.orders_controller.create_order({"body": order_request})
|
||||
|
||||
if response.body:
|
||||
order = response.body
|
||||
logger.info(
|
||||
"PayPal 订单创建成功",
|
||||
order_id=order.id,
|
||||
status=order.status,
|
||||
amount=amount
|
||||
)
|
||||
return {
|
||||
"order_id": order.id,
|
||||
"status": order.status,
|
||||
"links": [{"rel": link.rel, "href": link.href} for link in order.links] if order.links else []
|
||||
}
|
||||
else:
|
||||
raise Exception("PayPal 返回空响应")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("PayPal 订单创建失败", error=str(e))
|
||||
raise
|
||||
|
||||
async def capture_order(self, order_id: str) -> dict:
|
||||
"""
|
||||
捕获 PayPal 订单(完成支付)
|
||||
|
||||
Args:
|
||||
order_id: PayPal 订单 ID
|
||||
|
||||
Returns:
|
||||
包含支付详情的字典
|
||||
|
||||
Raises:
|
||||
Exception: PayPal API 调用失败
|
||||
"""
|
||||
try:
|
||||
response = self.orders_controller.capture_order({"id": order_id})
|
||||
|
||||
if response.body:
|
||||
order = response.body
|
||||
|
||||
# 提取支付信息
|
||||
capture_info = {}
|
||||
if order.purchase_units and len(order.purchase_units) > 0:
|
||||
pu = order.purchase_units[0]
|
||||
if pu.payments and pu.payments.captures and len(pu.payments.captures) > 0:
|
||||
capture = pu.payments.captures[0]
|
||||
capture_info = {
|
||||
"capture_id": capture.id,
|
||||
"amount": float(capture.amount.value) if capture.amount else 0,
|
||||
"currency": capture.amount.currency_code if capture.amount else "USD",
|
||||
}
|
||||
|
||||
# 提取付款人信息
|
||||
payer_info = {}
|
||||
if order.payer:
|
||||
payer_info = {
|
||||
"payer_id": order.payer.payer_id,
|
||||
"email": order.payer.email_address,
|
||||
"name": f"{order.payer.name.given_name} {order.payer.name.surname}" if order.payer.name else None,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"PayPal 订单捕获成功",
|
||||
order_id=order.id,
|
||||
status=order.status,
|
||||
capture_id=capture_info.get("capture_id")
|
||||
)
|
||||
|
||||
return {
|
||||
"order_id": order.id,
|
||||
"status": order.status,
|
||||
**capture_info,
|
||||
**payer_info,
|
||||
"raw_response": APIHelper.json_serialize(order),
|
||||
}
|
||||
else:
|
||||
raise Exception("PayPal 返回空响应")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("PayPal 订单捕获失败", order_id=order_id, error=str(e))
|
||||
raise
|
||||
|
||||
async def get_order(self, order_id: str) -> dict:
|
||||
"""
|
||||
查询 PayPal 订单状态
|
||||
|
||||
Args:
|
||||
order_id: PayPal 订单 ID
|
||||
|
||||
Returns:
|
||||
订单详情
|
||||
|
||||
Raises:
|
||||
Exception: PayPal API 调用失败
|
||||
"""
|
||||
try:
|
||||
response = self.orders_controller.get_order({"id": order_id})
|
||||
|
||||
if response.body:
|
||||
order = response.body
|
||||
return {
|
||||
"order_id": order.id,
|
||||
"status": order.status,
|
||||
"create_time": order.create_time,
|
||||
"update_time": order.update_time,
|
||||
}
|
||||
else:
|
||||
raise Exception("PayPal 返回空响应")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("PayPal 订单查询失败", order_id=order_id, error=str(e))
|
||||
raise
|
||||
|
||||
|
||||
# 单例
|
||||
_paypal_client: Optional[PayPalClient] = None
|
||||
|
||||
|
||||
def get_paypal_client() -> PayPalClient:
|
||||
"""获取 PayPal 客户端单例"""
|
||||
global _paypal_client
|
||||
if _paypal_client is None:
|
||||
_paypal_client = PayPalClient()
|
||||
return _paypal_client
|
||||
@@ -12,6 +12,7 @@ from . import (
|
||||
platform_agent_quota, # 平台 Agent 配额管理
|
||||
billing_webhook, # LiteLLM Token计费webhook
|
||||
external_tools, # 外部数据工具管理
|
||||
paypal, # PayPal 支付集成
|
||||
)
|
||||
|
||||
|
||||
@@ -45,5 +46,8 @@ def register_routes(app: FastAPI) -> None:
|
||||
platform_agent_quota.channel_router, # 渠道平台 Agent 配额管理
|
||||
platform_agent_quota.admin_router, # 管理员平台 Agent 配额管理
|
||||
platform_agent_quota.user_router, # 用户平台 Agent 配额管理
|
||||
# PayPal 支付路由
|
||||
paypal.router, # PayPal 用户支付路由(需要认证)
|
||||
paypal.webhook_router, # PayPal Webhook 回调路由(无需认证,在 whitelist 路径下)
|
||||
):
|
||||
app.include_router(router)
|
||||
|
||||
@@ -545,9 +545,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
status="active",
|
||||
is_active=True,
|
||||
is_admin=False,
|
||||
balance=0,
|
||||
credit_limit=0,
|
||||
eu_balance=0,
|
||||
total_eu_consumed=0,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
"""
|
||||
PayPal 支付路由
|
||||
用户在线充值功能
|
||||
|
||||
重要:这是客户支付的真金白银,所有操作必须:
|
||||
1. 有完整的审计日志
|
||||
2. 有事务保护
|
||||
3. 有金额验证
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from database import get_db
|
||||
from models import User, Balance, RechargeRecord
|
||||
from app.auth import require_auth
|
||||
from app.schemas import SuccessResponse
|
||||
from app.paypal_client import get_paypal_client
|
||||
from app.billing import add_balance
|
||||
from app.audit import log_audit_event
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
router = APIRouter(prefix="/api/user/billing/paypal", tags=["PayPal支付"])
|
||||
webhook_router = APIRouter(tags=["PayPal Webhook"])
|
||||
|
||||
|
||||
# ============= 请求/响应模型 =============
|
||||
|
||||
class CreatePayPalOrderRequest(BaseModel):
|
||||
"""创建 PayPal 订单请求"""
|
||||
amount: float = Field(..., gt=0, le=10000, description="充值金额 (USD)")
|
||||
currency: str = Field("USD", pattern="^USD$", description="货币类型,目前仅支持 USD")
|
||||
|
||||
|
||||
class CapturePayPalOrderRequest(BaseModel):
|
||||
"""捕获 PayPal 订单请求"""
|
||||
orderId: str = Field(..., min_length=1, description="PayPal 订单 ID")
|
||||
|
||||
|
||||
# ============= 用户路由 =============
|
||||
|
||||
@router.post("/create-order", response_model=SuccessResponse)
|
||||
async def create_paypal_order(
|
||||
req: CreatePayPalOrderRequest,
|
||||
request: Request,
|
||||
principal: dict = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
创建 PayPal 订单
|
||||
|
||||
用户发起充值请求,后端创建 PayPal 订单并返回订单 ID。
|
||||
前端使用订单 ID 调用 PayPal JS SDK 弹出支付窗口。
|
||||
|
||||
金额说明:
|
||||
- 1 USD = 1 EU
|
||||
- 最小充值金额:大于 0 USD
|
||||
- 最大充值金额:10000 USD
|
||||
"""
|
||||
user_id = principal.get("user_id")
|
||||
order_id = None
|
||||
|
||||
# 验证用户存在
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
try:
|
||||
# 创建 PayPal 订单
|
||||
paypal_client = get_paypal_client()
|
||||
order_result = await paypal_client.create_order(
|
||||
amount=req.amount,
|
||||
currency=req.currency
|
||||
)
|
||||
order_id = order_result["order_id"]
|
||||
|
||||
# 创建本地订单记录
|
||||
recharge_record = RechargeRecord(
|
||||
user_id=user_id,
|
||||
amount=Decimal(str(req.amount)),
|
||||
payment_method="paypal",
|
||||
order_id=order_id,
|
||||
status="pending",
|
||||
)
|
||||
db.add(recharge_record)
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"PayPal 订单创建成功",
|
||||
user_id=str(user_id),
|
||||
order_id=order_id,
|
||||
amount=req.amount
|
||||
)
|
||||
|
||||
# ========== 审计日志:订单创建成功 ==========
|
||||
await log_audit_event(
|
||||
action="payment.paypal.order_created",
|
||||
resource_type="paypal_order",
|
||||
resource_id=order_id,
|
||||
user_id=str(user_id),
|
||||
success=True,
|
||||
details={
|
||||
"amount": req.amount,
|
||||
"currency": req.currency,
|
||||
"eu_amount": req.amount,
|
||||
"paypal_status": order_result["status"],
|
||||
},
|
||||
request=request,
|
||||
db=db
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
data={
|
||||
"orderId": order_id,
|
||||
"status": order_result["status"],
|
||||
"amount": req.amount,
|
||||
"currency": req.currency,
|
||||
"euAmount": req.amount,
|
||||
},
|
||||
message="订单创建成功,请完成支付"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("创建 PayPal 订单失败", user_id=str(user_id), error=str(e))
|
||||
|
||||
# ========== 审计日志:订单创建失败 ==========
|
||||
await log_audit_event(
|
||||
action="payment.paypal.order_created",
|
||||
resource_type="paypal_order",
|
||||
resource_id=order_id,
|
||||
user_id=str(user_id),
|
||||
success=False,
|
||||
details={"amount": req.amount, "currency": req.currency},
|
||||
error_message=str(e),
|
||||
request=request,
|
||||
db=db
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"创建订单失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/capture-order", response_model=SuccessResponse)
|
||||
async def capture_paypal_order(
|
||||
req: CapturePayPalOrderRequest,
|
||||
request: Request,
|
||||
principal: dict = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
捕获 PayPal 订单(完成支付)
|
||||
|
||||
用户在 PayPal 完成支付后,前端调用此接口验证支付并完成充值。
|
||||
|
||||
流程:
|
||||
1. 验证订单属于当前用户
|
||||
2. 调用 PayPal API 捕获支付
|
||||
3. 验证支付金额
|
||||
4. 增加用户 EU 余额
|
||||
5. 更新订单状态
|
||||
"""
|
||||
user_id = principal.get("user_id")
|
||||
|
||||
# 查找本地订单记录
|
||||
result = await db.execute(
|
||||
select(RechargeRecord).where(
|
||||
RechargeRecord.order_id == req.orderId,
|
||||
RechargeRecord.user_id == user_id,
|
||||
RechargeRecord.payment_method == "paypal"
|
||||
)
|
||||
)
|
||||
recharge_record = result.scalar_one_or_none()
|
||||
|
||||
if not recharge_record:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="订单不存在或不属于当前用户"
|
||||
)
|
||||
|
||||
if recharge_record.status == "success":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="订单已完成,请勿重复操作"
|
||||
)
|
||||
|
||||
try:
|
||||
# 捕获 PayPal 订单
|
||||
paypal_client = get_paypal_client()
|
||||
capture_result = await paypal_client.capture_order(req.orderId)
|
||||
|
||||
if capture_result["status"] != "COMPLETED":
|
||||
recharge_record.status = "failed"
|
||||
await db.commit()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"支付未完成,状态: {capture_result['status']}"
|
||||
)
|
||||
|
||||
# 验证支付金额(严格模式:金额必须完全匹配)
|
||||
captured_amount = Decimal(str(capture_result.get("amount", 0)))
|
||||
expected_amount = recharge_record.amount
|
||||
|
||||
if captured_amount != expected_amount:
|
||||
logger.error(
|
||||
"支付金额不匹配,拒绝充值",
|
||||
order_id=req.orderId,
|
||||
expected=float(expected_amount),
|
||||
captured=float(captured_amount),
|
||||
user_id=str(user_id)
|
||||
)
|
||||
recharge_record.status = "amount_mismatch"
|
||||
await db.commit()
|
||||
|
||||
# ========== 审计日志:金额不匹配 ==========
|
||||
await log_audit_event(
|
||||
action="payment.paypal.amount_mismatch",
|
||||
resource_type="paypal_order",
|
||||
resource_id=req.orderId,
|
||||
user_id=str(user_id),
|
||||
success=False,
|
||||
details={
|
||||
"expected_amount": float(expected_amount),
|
||||
"captured_amount": float(captured_amount),
|
||||
},
|
||||
error_message=f"支付金额不匹配: 期望 ${expected_amount}, 实际 ${captured_amount}",
|
||||
request=request,
|
||||
db=db
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"支付金额不匹配: 期望 ${expected_amount}, 实际 ${captured_amount}"
|
||||
)
|
||||
|
||||
# 增加用户余额
|
||||
eu_amount = captured_amount # 1 USD = 1 EU
|
||||
success, message = await add_balance(
|
||||
user_id=str(user_id),
|
||||
amount=eu_amount,
|
||||
db=db,
|
||||
description=f"PayPal 充值 (订单: {req.orderId})",
|
||||
auto_commit=False # 不自动提交,等待订单状态更新后一起提交
|
||||
)
|
||||
|
||||
if not success:
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"余额更新失败: {message}"
|
||||
)
|
||||
|
||||
# 更新订单状态
|
||||
recharge_record.status = "success"
|
||||
recharge_record.completed_at = datetime.utcnow()
|
||||
|
||||
# 统一提交事务
|
||||
await db.commit()
|
||||
|
||||
# 获取新余额
|
||||
balance_result = await db.execute(
|
||||
select(Balance).where(Balance.user_id == user_id)
|
||||
)
|
||||
balance = balance_result.scalar_one_or_none()
|
||||
new_balance = float(balance.eu_balance) if balance else 0
|
||||
|
||||
logger.info(
|
||||
"PayPal 充值成功",
|
||||
user_id=str(user_id),
|
||||
order_id=req.orderId,
|
||||
amount=float(captured_amount),
|
||||
new_balance=new_balance
|
||||
)
|
||||
|
||||
# ========== 审计日志:支付成功 ==========
|
||||
await log_audit_event(
|
||||
action="payment.paypal.order_captured",
|
||||
resource_type="paypal_order",
|
||||
resource_id=req.orderId,
|
||||
user_id=str(user_id),
|
||||
success=True,
|
||||
details={
|
||||
"amount": float(captured_amount),
|
||||
"eu_amount": float(eu_amount),
|
||||
"new_balance": new_balance,
|
||||
"capture_id": capture_result.get("capture_id"),
|
||||
"payer_email": capture_result.get("email"),
|
||||
"payer_id": capture_result.get("payer_id"),
|
||||
},
|
||||
request=request,
|
||||
db=db
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
data={
|
||||
"orderId": req.orderId,
|
||||
"status": "COMPLETED",
|
||||
"amount": float(captured_amount),
|
||||
"euAmount": float(eu_amount),
|
||||
"newBalance": new_balance,
|
||||
"captureId": capture_result.get("capture_id"),
|
||||
"payerEmail": capture_result.get("email"),
|
||||
},
|
||||
message=f"充值成功,已增加 {eu_amount} EU"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
# HTTPException 已经处理过,直接抛出
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(
|
||||
"捕获 PayPal 订单失败",
|
||||
order_id=req.orderId,
|
||||
user_id=str(user_id),
|
||||
error=str(e),
|
||||
error_type=type(e).__name__
|
||||
)
|
||||
|
||||
# ========== 审计日志:系统异常 ==========
|
||||
await log_audit_event(
|
||||
action="payment.paypal.order_failed",
|
||||
resource_type="paypal_order",
|
||||
resource_id=req.orderId,
|
||||
user_id=str(user_id),
|
||||
success=False,
|
||||
details={"error_type": type(e).__name__},
|
||||
error_message=str(e),
|
||||
request=request,
|
||||
db=db
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"支付验证失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/order/{order_id}", response_model=SuccessResponse)
|
||||
async def get_paypal_order(
|
||||
order_id: str,
|
||||
principal: dict = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
查询 PayPal 订单状态
|
||||
"""
|
||||
user_id = principal.get("user_id")
|
||||
|
||||
# 查找本地订单记录
|
||||
result = await db.execute(
|
||||
select(RechargeRecord).where(
|
||||
RechargeRecord.order_id == order_id,
|
||||
RechargeRecord.user_id == user_id,
|
||||
RechargeRecord.payment_method == "paypal"
|
||||
)
|
||||
)
|
||||
recharge_record = result.scalar_one_or_none()
|
||||
|
||||
if not recharge_record:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="订单不存在"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
data={
|
||||
"orderId": order_id,
|
||||
"status": recharge_record.status,
|
||||
"amount": float(recharge_record.amount),
|
||||
"euAmount": float(recharge_record.amount),
|
||||
"createdAt": recharge_record.created_at.isoformat() if recharge_record.created_at else None,
|
||||
"completedAt": recharge_record.completed_at.isoformat() if recharge_record.completed_at else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============= Webhook 路由 =============
|
||||
|
||||
@webhook_router.post("/whitelist/payment/callback/paypal")
|
||||
async def paypal_webhook(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
PayPal Webhook 回调
|
||||
|
||||
用于接收 PayPal 异步通知,处理边缘情况(如用户关闭页面但支付已完成)。
|
||||
|
||||
支持的事件类型:
|
||||
- PAYMENT.CAPTURE.COMPLETED: 支付捕获完成
|
||||
- PAYMENT.CAPTURE.DENIED: 支付被拒绝
|
||||
- PAYMENT.CAPTURE.REFUNDED: 支付已退款
|
||||
"""
|
||||
try:
|
||||
# 获取请求体
|
||||
body = await request.json()
|
||||
|
||||
# 获取 PayPal 签名头(用于验证)
|
||||
headers = {
|
||||
"PayPal-Transmission-Id": request.headers.get("PayPal-Transmission-Id"),
|
||||
"PayPal-Transmission-Time": request.headers.get("PayPal-Transmission-Time"),
|
||||
"PayPal-Transmission-Sig": request.headers.get("PayPal-Transmission-Sig"),
|
||||
"PayPal-Cert-Url": request.headers.get("PayPal-Cert-Url"),
|
||||
"PayPal-Auth-Algo": request.headers.get("PayPal-Auth-Algo"),
|
||||
}
|
||||
|
||||
# TODO: 验证 Webhook 签名
|
||||
# 生产环境必须验证签名,防止伪造请求
|
||||
# 使用 PayPal Webhook Signature Verification API
|
||||
|
||||
event_type = body.get("event_type")
|
||||
resource = body.get("resource", {})
|
||||
|
||||
logger.info(
|
||||
"收到 PayPal Webhook",
|
||||
event_type=event_type,
|
||||
resource_id=resource.get("id")
|
||||
)
|
||||
|
||||
# ========== 审计日志:Webhook 接收 ==========
|
||||
await log_audit_event(
|
||||
action="payment.paypal.webhook_received",
|
||||
resource_type="paypal_webhook",
|
||||
resource_id=body.get("id"),
|
||||
user_id=None,
|
||||
success=True,
|
||||
details={
|
||||
"event_type": event_type,
|
||||
"resource_id": resource.get("id"),
|
||||
},
|
||||
request=request,
|
||||
db=db
|
||||
)
|
||||
|
||||
if event_type == "PAYMENT.CAPTURE.COMPLETED":
|
||||
# 支付完成
|
||||
capture_id = resource.get("id")
|
||||
# 根据 capture_id 查找并更新订单
|
||||
# 这里作为备份机制,主要逻辑在 capture-order 接口
|
||||
logger.info("PayPal 支付完成 Webhook", capture_id=capture_id)
|
||||
|
||||
elif event_type == "PAYMENT.CAPTURE.DENIED":
|
||||
# 支付被拒绝
|
||||
capture_id = resource.get("id")
|
||||
logger.warning("PayPal 支付被拒绝", capture_id=capture_id)
|
||||
|
||||
elif event_type == "PAYMENT.CAPTURE.REFUNDED":
|
||||
# 支付已退款
|
||||
capture_id = resource.get("id")
|
||||
logger.info("PayPal 支付已退款", capture_id=capture_id)
|
||||
# TODO: 处理退款逻辑,扣减用户余额
|
||||
|
||||
return {"success": True}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("处理 PayPal Webhook 失败", error=str(e))
|
||||
# 返回 200 避免 PayPal 重试
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -106,6 +106,19 @@ class Settings(BaseSettings):
|
||||
alipay_app_id: str = os.getenv("ALIPAY_APP_ID", "")
|
||||
wechat_pay_app_id: str = os.getenv("WECHAT_PAY_APP_ID", "")
|
||||
|
||||
# PayPal 支付配置
|
||||
paypal_client_id: str = os.getenv("PAYPAL_CLIENT_ID", "")
|
||||
paypal_client_secret: str = os.getenv("PAYPAL_CLIENT_SECRET", "")
|
||||
paypal_environment: str = os.getenv("PAYPAL_ENVIRONMENT", "sandbox") # sandbox 或 production
|
||||
paypal_webhook_id: str = os.getenv("PAYPAL_WEBHOOK_ID", "") # 用于验证 Webhook 签名
|
||||
|
||||
@property
|
||||
def paypal_api_base(self) -> str:
|
||||
"""获取 PayPal API 基础 URL"""
|
||||
if self.paypal_environment == "production":
|
||||
return "https://api-m.paypal.com"
|
||||
return "https://api-m.sandbox.paypal.com"
|
||||
|
||||
# 云存储设置(Azure Blob Storage)
|
||||
azure_storage_connection_string: str = os.getenv("AZURE_STORAGE_CONNECTION_STRING", "")
|
||||
s3_bucket: str = os.getenv("S3_BUCKET", "taiji-ai-exports")
|
||||
|
||||
@@ -43,4 +43,7 @@ psutil==5.9.8
|
||||
# 其他
|
||||
Jinja2==3.1.3
|
||||
MarkupSafe==2.1.3
|
||||
pydantic[email]
|
||||
pydantic[email]
|
||||
|
||||
# PayPal 支付
|
||||
paypal-server-sdk>=0.5.0
|
||||
Reference in New Issue
Block a user