You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flask集成PayPal REST API:支付后即时更新用户积分的问题

Flask + PayPal v2 API 实现支付后自动更新用户积分

核心方案

  • 采用PayPal官方最新的v2 REST API(替代过时的paypal-python-sdk)
  • 创建订单时,将用户ID存入purchase_units[0].custom_id字段,PayPal会在支付完成后返回该标识
  • 捕获订单时,读取custom_id定位用户,同时验证实际支付金额与后端预设金额一致,杜绝篡改风险
  • 无需依赖IPN,直接通过capture_order路由处理支付结果(PayPal前端会自动回调该接口)

前置准备

  1. 安装官方Python SDK:pip install paypalcheckoutsdk flask-jwt-extended flask-sqlalchemy
  2. 在PayPal开发者平台创建应用,获取Client ID和Secret(优先用沙箱环境测试)
  3. 确保你的用户模型包含id(用户唯一标识)和points(积分字段)

完整代码示例

基础配置与模型

from flask import Flask, request, jsonify
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment, LiveEnvironment
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity
import os

app = Flask(__name__)
# 核心配置
app.secret_key = os.getenv('FLASK_SECRET_KEY', 'your-strong-flask-secret')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'  # 替换为你的数据库链接
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# JWT配置(用于验证用户身份,防止前端篡改用户ID)
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'your-strong-jwt-secret')

db = SQLAlchemy(app)
jwt = JWTManager(app)

# 用户模型示例
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    points = db.Column(db.Integer, default=0)

# PayPal客户端初始化
PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', 'your-paypal-client-id')
PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', 'your-paypal-secret')
# 沙箱环境测试,上线时替换为LiveEnvironment
environment = SandboxEnvironment(client_id=PAYPAL_CLIENT_ID, client_secret=PAYPAL_CLIENT_SECRET)
paypal_client = PayPalHttpClient(environment)

创建订单路由(生成PayPal支付链接)

@app.route('/api/paypal/create-order', methods=['POST'])
@jwt_required()
def create_order():
    # 从JWT令牌获取用户ID,完全杜绝前端篡改
    user_id = get_jwt_identity()
    data = request.get_json()
    amount = data.get('amount')

    # 安全校验1:验证金额格式与合法性
    try:
        float(amount)
    except ValueError:
        return jsonify({'error': '无效的金额格式'}), 400

    # 安全校验2:后端预设金额与积分的对应关系,完全不依赖前端传递的积分值
    # 示例:1美元=10积分,可根据业务逻辑从数据库/配置文件读取
    points_to_add = int(float(amount) * 10)

    # 构建PayPal订单请求
    paypal_request = OrdersCreateRequest()
    paypal_request.prefer('return=representation')
    paypal_request.request_body({
        "intent": "CAPTURE",
        "purchase_units": [
            {
                "amount": {
                    "currency_code": "USD",
                    "value": amount
                },
                # 存储用户ID,PayPal支付完成后会原封不动返回该字段
                "custom_id": str(user_id)
            }
        ],
        "application_context": {
            "return_url": "https://your-domain.com/payment-success",  # 支付成功跳转前端页面
            "cancel_url": "https://your-domain.com/payment-cancel"    # 支付取消跳转页面
        }
    })

    try:
        response = paypal_client.execute(paypal_request)
        return jsonify({
            'order_id': response.result.id,
            'status': response.result.status,
            'points_to_add': points_to_add
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

捕获订单路由(支付成功后更新积分)

@app.route('/api/paypal/capture-order', methods=['POST'])
def capture_order():
    order_id = request.get_json().get('order_id')
    if not order_id:
        return jsonify({'error': '缺少订单ID'}), 400

    # 发起捕获订单请求
    paypal_request = OrdersCaptureRequest(order_id)
    try:
        response = paypal_client.execute(paypal_request)
        order_result = response.result

        # 安全校验1:确认支付状态为完成
        if order_result.status != 'COMPLETED':
            return jsonify({'error': '支付未完成'}), 400

        # 获取PayPal返回的用户ID
        purchase_unit = order_result.purchase_units[0]
        user_id = purchase_unit.custom_id
        if not user_id:
            return jsonify({'error': '未找到关联用户信息'}), 400

        # 安全校验2:验证实际支付金额与预期一致(建议提前将订单信息存入数据库,此处简化为直接计算)
        captured_amount = purchase_unit.amount.value
        expected_points = int(float(captured_amount) * 10)

        # 更新用户积分
        user = User.query.get(int(user_id))
        if not user:
            return jsonify({'error': '用户不存在'}), 400
        
        user.points += expected_points
        db.session.commit()

        return jsonify({
            'status': 'success',
            'user_id': user_id,
            'updated_points': user.points,
            'captured_amount': captured_amount
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

关键安全注意事项

  • 禁止信任前端参数:用户ID必须通过JWT令牌获取,金额与积分的对应关系必须由后端预设,完全拒绝前端传递的积分值
  • 强制金额校验:捕获订单时必须对比实际支付金额与后端预期金额,防止用户篡改前端支付金额
  • 使用HTTPS:所有与PayPal交互的接口必须部署在HTTPS环境下,避免数据被拦截篡改
  • 沙箱测试优先:上线前务必在PayPal沙箱环境完成全流程测试,确认无误后切换为LiveEnvironment

前端集成示例(核心逻辑)

<script src="https://www.paypal.com/sdk/js?client-id=YOUR_PAYPAL_CLIENT_ID&currency=USD"></script>
<div id="paypal-button-container"></div>

<script>
// 假设已通过登录接口获取JWT令牌,存储在localStorage中
const token = localStorage.getItem('access_token');

paypal.Buttons({
    createOrder: async (data, actions) => {
        const response = await fetch('/api/paypal/create-order', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${token}`
            },
            body: JSON.stringify({ amount: '10.00' })
        });
        const orderData = await response.json();
        return orderData.order_id;
    },
    onApprove: async (data, actions) => {
        const captureResponse = await fetch('/api/paypal/capture-order', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ order_id: data.orderID })
        });
        const result = await captureResponse.json();
        alert(`积分已更新:${result.updated_points}`);
        window.location.href = '/payment-success';
    }
}).render('#paypal-button-container');
</script>

内容的提问来源于stack exchange,提问作者wildcolor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 14:37:58