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

15 KiB
Raw Blame History

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 生产环境切换

生产环境需要:

  1. 获取 Live Client ID

  2. 更新配置

    // 使用环境变量
    export const PAYPAL_CONFIG = {
      clientId: process.env.REACT_APP_PAYPAL_CLIENT_ID || "sandbox_client_id",
      currency: "USD",
      intent: "capture",
    };
    
  3. 环境变量文件

    # .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 错误处理示例

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. 参考资料