# PayPal 支付集成 - 前端实施文档 ## 1. 概述 ### 1.1 需求背景 用户在使用平台 Agent 时,当 EU 余额不足时可以通过 PayPal 在线充值。前端需要集成 PayPal JS SDK,显示支付按钮并处理支付流程。 ### 1.2 核心概念 - **EU(执行单元)**:系统计费单位,1 EU = 1 USD - **PayPal JS SDK**:PayPal 官方前端 SDK,用于显示支付按钮和处理支付弹窗 ### 1.3 PayPal Client ID - **环境**:Sandbox(测试环境) - **Client ID**:`AWJcBVeccSgDDhcZcYEbf4SJKxq9Uk_qVNlvk9mCewzl9o1Cp0onPzOD-v26-Mye9F1cKF6SzipuTtQZ` > **注意**:Client ID 是公开的,可以安全地放在前端代码中。Secret 只在后端使用。 --- ## 2. 后端 API 接口 前端需要调用以下后端接口: ### 2.1 创建订单 **请求** ```http POST /api/user/billing/paypal/create-order Authorization: Bearer Content-Type: application/json { "amount": 10.00, "currency": "USD" } ``` **响应** ```json { "success": true, "data": { "orderId": "5O190127TN364715T", "status": "CREATED", "amount": 10.00, "currency": "USD", "euAmount": 10.00 } } ``` ### 2.2 捕获支付 **请求** ```http POST /api/user/billing/paypal/capture-order Authorization: Bearer Content-Type: application/json { "orderId": "5O190127TN364715T" } ``` **响应** ```json { "success": true, "data": { "orderId": "5O190127TN364715T", "status": "COMPLETED", "amount": 10.00, "euAmount": 10.00, "newBalance": 110.00, "captureId": "3C679366HH908993F", "payerEmail": "buyer@example.com" }, "message": "充值成功,已增加 10.00 EU" } ``` --- ## 3. 支付流程 ```mermaid sequenceDiagram participant U as 用户 participant F as 前端 participant B as 后端 participant P as PayPal U->>F: 1. 输入充值金额 U->>F: 2. 点击 PayPal 按钮 F->>B: 3. POST /create-order B-->>F: 4. 返回 orderId F->>P: 5. PayPal SDK 弹出支付窗口 U->>P: 6. 登录 PayPal 并确认支付 P-->>F: 7. 支付成功回调 F->>B: 8. POST /capture-order B-->>F: 9. 返回充值结果 F-->>U: 10. 显示成功,更新余额 ``` --- ## 4. 实施步骤 ### 4.1 安装依赖 ```bash # React 项目 npm install @paypal/react-paypal-js # 或 Vue 项目 npm install @paypal/paypal-js ``` ### 4.2 创建 PayPal 充值组件 (React) ```tsx // components/PayPalRecharge.tsx import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js"; import { useState } from "react"; import { message } from "antd"; // Sandbox Client ID(生产环境需要替换为 Live Client ID) const PAYPAL_CLIENT_ID = "AWJcBVeccSgDDhcZcYEbf4SJKxq9Uk_qVNlvk9mCewzl9o1Cp0onPzOD-v26-Mye9F1cKF6SzipuTtQZ"; interface PayPalRechargeProps { amount: number; onSuccess: (data: { orderId: string; amount: number; euAmount: number; newBalance: number; }) => void; onError: (error: Error) => void; } export function PayPalRecharge({ amount, onSuccess, onError }: PayPalRechargeProps) { const [loading, setLoading] = useState(false); // 获取 token(根据你的项目实际情况调整) const getToken = () => { return localStorage.getItem("token") || sessionStorage.getItem("token"); }; return ( { setLoading(true); try { const response = await fetch("/api/user/billing/paypal/create-order", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${getToken()}`, }, body: JSON.stringify({ amount: amount, currency: "USD", }), }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.message || data.detail || "创建订单失败"); } return data.data.orderId; } catch (error) { setLoading(false); onError(error as Error); throw error; } }} // 支付成功后捕获订单 onApprove={async (data) => { try { const response = await fetch("/api/user/billing/paypal/capture-order", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${getToken()}`, }, body: JSON.stringify({ orderId: data.orderID, }), }); const result = await response.json(); setLoading(false); if (!response.ok || !result.success) { throw new Error(result.message || result.detail || "支付验证失败"); } message.success(`充值成功!已增加 ${result.data.euAmount} EU`); onSuccess(result.data); } catch (error) { setLoading(false); onError(error as Error); } }} // 支付错误 onError={(error) => { setLoading(false); message.error("支付失败,请重试"); onError(new Error(String(error))); }} // 用户取消支付 onCancel={() => { setLoading(false); message.info("支付已取消"); }} /> ); } ``` ### 4.3 创建充值页面 (React) ```tsx // pages/Recharge.tsx import { useState, useEffect } from "react"; import { Card, InputNumber, Button, Space, Typography, Statistic, Spin } from "antd"; import { PayPalRecharge } from "../components/PayPalRecharge"; const { Title, Text } = Typography; // 预设金额选项 const PRESET_AMOUNTS = [10, 50, 100, 500]; export function RechargePage() { const [amount, setAmount] = useState(10); const [balance, setBalance] = useState(0); const [loading, setLoading] = useState(true); // 获取当前余额 useEffect(() => { fetchBalance(); }, []); const fetchBalance = async () => { try { const response = await fetch("/api/user/billing/balance", { headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, }); const data = await response.json(); if (data.success) { setBalance(data.data.euBalance || 0); } } catch (error) { console.error("获取余额失败:", error); } finally { setLoading(false); } }; const handleSuccess = (data: { newBalance: number }) => { // 更新余额显示 setBalance(data.newBalance); }; const handleError = (error: Error) => { console.error("支付错误:", error); }; if (loading) { return (
); } return (
账户充值 {/* 当前余额 */} {/* 预设金额选择 */}
选择充值金额 (USD) {PRESET_AMOUNTS.map((preset) => ( ))}
{/* 自定义金额输入 */}
或输入自定义金额 setAmount(value || 0)} prefix="$" precision={2} placeholder="输入充值金额" />
{/* 充值说明 */}
充值 ${amount.toFixed(2)} USD = {amount.toFixed(2)} EU
{/* PayPal 支付按钮 */} {/* 说明文字 */}
· 1 USD = 1 EU(执行单元)
· 最小充值金额:$1.00
· 最大充值金额:$10,000.00
· 支付完成后余额立即到账
); } ``` ### 4.4 Vue 集成示例 ```vue ``` --- ## 5. 环境配置 ### 5.1 Sandbox 测试环境 ```typescript // config/paypal.ts export const PAYPAL_CONFIG = { // Sandbox Client ID clientId: "AWJcBVeccSgDDhcZcYEbf4SJKxq9Uk_qVNlvk9mCewzl9o1Cp0onPzOD-v26-Mye9F1cKF6SzipuTtQZ", currency: "USD", intent: "capture", }; ``` ### 5.2 生产环境切换 生产环境需要: 1. **获取 Live Client ID** - 登录 https://developer.paypal.com - 进入 Dashboard → My Apps & Credentials - 切换到 "Live" 标签 - 获取 Live Client ID 2. **更新配置** ```typescript // 使用环境变量 export const PAYPAL_CONFIG = { clientId: process.env.REACT_APP_PAYPAL_CLIENT_ID || "sandbox_client_id", currency: "USD", intent: "capture", }; ``` 3. **环境变量文件** ```bash # .env.production REACT_APP_PAYPAL_CLIENT_ID= ``` --- ## 6. 测试指南 ### 6.1 Sandbox 测试账号 1. 登录 https://developer.paypal.com/dashboard/accounts 2. 使用 Personal 类型的测试账号进行支付 3. 默认测试账号密码通常是 `12345678` ### 6.2 测试流程 1. 启动前端开发服务器 2. 打开充值页面 3. 输入充值金额(如 $10) 4. 点击 PayPal 按钮 5. 在弹出窗口中使用 Sandbox 测试账号登录 6. 确认支付 7. 验证余额是否增加 ### 6.3 常见测试场景 | 场景 | 操作 | 预期结果 | |------|------|----------| | 正常支付 | 完成支付流程 | 余额增加,显示成功提示 | | 取消支付 | 在 PayPal 窗口点击取消 | 显示"支付已取消"提示 | | 金额为 0 | 输入 0 或负数 | PayPal 按钮禁用 | | 超过最大金额 | 输入超过 10000 | 输入框限制最大值 | | 网络错误 | 断开网络 | 显示错误提示 | --- ## 7. 错误处理 ### 7.1 常见错误码 | 错误 | 原因 | 处理方式 | |------|------|----------| | `INSTRUMENT_DECLINED` | 支付方式被拒绝 | 提示用户更换支付方式 | | `PAYER_ACTION_REQUIRED` | 需要用户操作 | 引导用户完成 PayPal 验证 | | `ORDER_NOT_APPROVED` | 订单未批准 | 提示用户重新支付 | | `INVALID_RESOURCE_ID` | 订单 ID 无效 | 刷新页面重试 | ### 7.2 错误处理示例 ```typescript const handleError = (error: Error) => { console.error("支付错误:", error); const errorMessage = error.message || String(error); if (errorMessage.includes("INSTRUMENT_DECLINED")) { message.error("支付方式被拒绝,请更换支付方式"); } else if (errorMessage.includes("ORDER_NOT_APPROVED")) { message.error("订单未批准,请重新支付"); } else if (errorMessage.includes("network")) { message.error("网络错误,请检查网络连接"); } else { message.error("支付失败,请重试"); } }; ``` --- ## 8. 任务清单 - [ ] 安装 `@paypal/react-paypal-js` 或 `@paypal/paypal-js` 依赖 - [ ] 创建 PayPal 配置文件 - [ ] 创建 `PayPalRecharge` 组件 - [ ] 创建充值页面 - [ ] 添加路由配置 - [ ] 处理支付成功/失败回调 - [ ] 更新余额显示 - [ ] 使用 Sandbox 账号测试 - [ ] 配置生产环境 Client ID --- ## 9. 参考资料 - [PayPal React SDK 文档](https://www.npmjs.com/package/@paypal/react-paypal-js) - [PayPal JS SDK 文档](https://developer.paypal.com/sdk/js/) - [PayPal Sandbox 测试指南](https://developer.paypal.com/tools/sandbox/) - [PayPal 按钮样式配置](https://developer.paypal.com/sdk/js/configuration/#link-style)