forked from xiaohei/taiji-AI-PAD
计费备份
This commit is contained in:
@@ -0,0 +1,603 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user