计费备份

This commit is contained in:
zhanggangyong
2026-03-12 02:32:23 +00:00
parent a540e6d61a
commit 7ddac1bea7
19 changed files with 2690 additions and 4041 deletions
@@ -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)