Files
taiji-AI-PAD/plans/PayPal支付集成-前端实施文档.md
T
2026-03-12 02:32:23 +00:00

604 lines
15 KiB
Markdown
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.
# 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 <token>
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 <token>
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 (
<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)
```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<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 集成示例
```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 测试环境
```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=<Live 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)