forked from xiaohei/taiji-AI-PAD
15 KiB
15 KiB
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 创建订单
请求
POST /api/user/billing/paypal/create-order
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 10.00,
"currency": "USD"
}
响应
{
"success": true,
"data": {
"orderId": "5O190127TN364715T",
"status": "CREATED",
"amount": 10.00,
"currency": "USD",
"euAmount": 10.00
}
}
2.2 捕获支付
请求
POST /api/user/billing/paypal/capture-order
Authorization: Bearer <token>
Content-Type: application/json
{
"orderId": "5O190127TN364715T"
}
响应
{
"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. 支付流程
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 安装依赖
# React 项目
npm install @paypal/react-paypal-js
# 或 Vue 项目
npm install @paypal/paypal-js
4.2 创建 PayPal 充值组件 (React)
// 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 (
<PayPalScriptProvider
options={{
clientId: PAYPAL_CLIENT_ID,
currency: "USD",
intent: "capture",
}}
>
<PayPalButtons
style={{
layout: "vertical",
color: "blue",
shape: "rect",
label: "paypal",
}}
disabled={loading || amount <= 0}
// 创建订单
createOrder={async () => {
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("支付已取消");
}}
/>
</PayPalScriptProvider>
);
}
4.3 创建充值页面 (React)
// 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<number>(10);
const [balance, setBalance] = useState<number>(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 (
<div style={{ textAlign: "center", padding: 50 }}>
<Spin size="large" />
</div>
);
}
return (
<div style={{ maxWidth: 600, margin: "0 auto", padding: 24 }}>
<Card>
<Title level={3}>账户充值</Title>
{/* 当前余额 */}
<Statistic
title="当前余额"
value={balance}
suffix="EU"
precision={2}
style={{ marginBottom: 24 }}
/>
{/* 预设金额选择 */}
<div style={{ marginBottom: 24 }}>
<Text>选择充值金额 (USD)</Text>
<Space style={{ marginTop: 8, display: "flex", flexWrap: "wrap" }}>
{PRESET_AMOUNTS.map((preset) => (
<Button
key={preset}
type={amount === preset ? "primary" : "default"}
onClick={() => setAmount(preset)}
>
${preset}
</Button>
))}
</Space>
</div>
{/* 自定义金额输入 */}
<div style={{ marginBottom: 24 }}>
<Text>或输入自定义金额</Text>
<InputNumber
style={{ width: "100%", marginTop: 8 }}
min={1}
max={10000}
value={amount}
onChange={(value) => setAmount(value || 0)}
prefix="$"
precision={2}
placeholder="输入充值金额"
/>
</div>
{/* 充值说明 */}
<div style={{ marginBottom: 16, padding: 12, background: "#f5f5f5", borderRadius: 4 }}>
<Text type="secondary">
充值 <Text strong>${amount.toFixed(2)} USD</Text> = <Text strong>{amount.toFixed(2)} EU</Text>
</Text>
</div>
{/* PayPal 支付按钮 */}
<PayPalRecharge
amount={amount}
onSuccess={handleSuccess}
onError={handleError}
/>
{/* 说明文字 */}
<div style={{ marginTop: 16 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
· 1 USD = 1 EU(执行单元)<br />
· 最小充值金额:$1.00<br />
· 最大充值金额:$10,000.00<br />
· 支付完成后余额立即到账
</Text>
</div>
</Card>
</div>
);
}
4.4 Vue 集成示例
<!-- components/PayPalRecharge.vue -->
<template>
<div ref="paypalContainer" id="paypal-button-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { loadScript } from '@paypal/paypal-js';
import { message } from 'ant-design-vue';
const props = defineProps<{
amount: number;
}>();
const emit = defineEmits<{
(e: 'success', data: any): void;
(e: 'error', error: Error): void;
}>();
const paypalContainer = ref<HTMLElement | null>(null);
const PAYPAL_CLIENT_ID = 'AWJcBVeccSgDDhcZcYEbf4SJKxq9Uk_qVNlvk9mCewzl9o1Cp0onPzOD-v26-Mye9F1cKF6SzipuTtQZ';
const getToken = () => {
return localStorage.getItem('token') || '';
};
onMounted(async () => {
try {
const paypal = await loadScript({
clientId: PAYPAL_CLIENT_ID,
currency: 'USD',
});
if (paypal && paypal.Buttons) {
paypal.Buttons({
createOrder: async () => {
const response = await fetch('/api/user/billing/paypal/create-order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getToken()}`,
},
body: JSON.stringify({
amount: props.amount,
currency: 'USD',
}),
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || '创建订单失败');
}
return data.data.orderId;
},
onApprove: async (data: { orderID: string }) => {
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();
if (result.success) {
message.success(`充值成功!已增加 ${result.data.euAmount} EU`);
emit('success', result.data);
} else {
throw new Error(result.message || '支付验证失败');
}
},
onError: (err: any) => {
message.error('支付失败,请重试');
emit('error', new Error(String(err)));
},
onCancel: () => {
message.info('支付已取消');
},
}).render('#paypal-button-container');
}
} catch (error) {
console.error('加载 PayPal SDK 失败:', error);
}
});
</script>
5. 环境配置
5.1 Sandbox 测试环境
// config/paypal.ts
export const PAYPAL_CONFIG = {
// Sandbox Client ID
clientId: "AWJcBVeccSgDDhcZcYEbf4SJKxq9Uk_qVNlvk9mCewzl9o1Cp0onPzOD-v26-Mye9F1cKF6SzipuTtQZ",
currency: "USD",
intent: "capture",
};
5.2 生产环境切换
生产环境需要:
-
获取 Live Client ID
- 登录 https://developer.paypal.com
- 进入 Dashboard → My Apps & Credentials
- 切换到 "Live" 标签
- 获取 Live Client ID
-
更新配置
// 使用环境变量 export const PAYPAL_CONFIG = { clientId: process.env.REACT_APP_PAYPAL_CLIENT_ID || "sandbox_client_id", currency: "USD", intent: "capture", }; -
环境变量文件
# .env.production REACT_APP_PAYPAL_CLIENT_ID=<Live Client ID>
6. 测试指南
6.1 Sandbox 测试账号
- 登录 https://developer.paypal.com/dashboard/accounts
- 使用 Personal 类型的测试账号进行支付
- 默认测试账号密码通常是
12345678
6.2 测试流程
- 启动前端开发服务器
- 打开充值页面
- 输入充值金额(如 $10)
- 点击 PayPal 按钮
- 在弹出窗口中使用 Sandbox 测试账号登录
- 确认支付
- 验证余额是否增加
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 错误处理示例
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