Loading

php在支付场景如何设计幂等性

在PHP中实现在线支付系统的幂等性,可以采用以下几种方法来避免用户多次点击支付按钮导致的重复扣款问题:

1. 生成唯一支付令牌

在用户发起支付请求时,系统生成一个唯一的支付令牌(如UUID),并将该令牌与用户的订单信息绑定。在后续的扣款操作中,系统校验支付令牌的有效性,防止重复扣款。
// 生成唯一支付令牌
$token = bin2hex(random_bytes(32));

// 将令牌存储到Redis或数据库中,并设置过期时间
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setex($token, 300, $orderId); // 设置令牌过期时间为300秒

// 将令牌传递给前端,供后续支付请求使用
echo json_encode(['token' => $token]);
在支付接口中验证令牌:
// 接收支付请求中的令牌
$token = $_POST['token'];

// 检查令牌是否已存在
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$orderId = $redis->get($token);

if ($orderId) {
    // 令牌有效,执行扣款操作
    // ...
    
    // 扣款成功后,删除令牌或标记为已使用
    $redis->del($token);
} else {
    // 令牌无效,拒绝请求
    echo json_encode(['error' => 'Invalid token']);
}

2. 数据库唯一索引

在数据库中为关键字段(如订单号)添加唯一索引,确保每次插入的数据都是唯一的。
// 创建订单表时添加唯一索引
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_no VARCHAR(255) NOT NULL,
    user_id INT NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(50) NOT NULL,
    UNIQUE KEY unique_order_no (order_no)
);

// 在执行支付操作前,先检查订单是否已存在
$stmt = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE order_no = ?");
$stmt->execute([$orderNo]);
if ($stmt->fetchColumn() > 0) {
    // 订单已存在,拒绝请求
    die('Order already exists');
}

// 执行扣款操作并插入订单记录
$stmt = $pdo->prepare("INSERT INTO orders (order_no, user_id, amount, status) VALUES (?, ?, ?, ?)");
$stmt->execute([$orderNo, $userId, $amount, 'paid']);

3. 使用分布式锁

在高并发场景下,可以使用分布式锁来确保同一订单的支付请求只有一个能够执行。
// 获取分布式锁
$lockKey = "payment_lock_{$orderId}";
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$acquired = $redis->setnx($lockKey, time());

if ($acquired) {
    // 设置锁的过期时间,防止死锁
    $redis->expire($lockKey, 30);
    
    // 执行支付操作
    // ...
    
    // 释放锁
    $redis->del($lockKey);
} else {
    // 无法获取锁,说明已有其他请求在处理,拒绝当前请求
    die('Payment is being processed');
}

4. 乐观锁

在数据库操作中使用乐观锁机制,确保在并发环境下数据的一致性。
// 获取订单的当前版本号
$stmt = $pdo->prepare("SELECT version FROM orders WHERE id = ?");
$stmt->execute([$orderId]);
$version = $stmt->fetchColumn();

// 执行支付操作并更新版本号
$stmt = $pdo->prepare("UPDATE orders SET amount = ?, status = 'paid', version = version + 1 WHERE id = ? AND version = ?");
$stmt->execute([$amount, $orderId, $version]);

// 检查受影响的行数
if ($stmt->rowCount() == 0) {
    // 版本号不匹配,说明数据已被其他请求修改,拒绝当前请求
    die('Payment already processed');
}

5. 前端限制

在前端页面上,可以通过禁用支付按钮、添加倒计时等方式来限制用户多次点击支付按钮。
// 点击支付按钮后禁用按钮,并设置倒计时
document.getElementById('payBtn').addEventListener('click', function() {
    this.disabled = true;
    let count = 5;
    const timer = setInterval(() => {
        this.value = `支付中(${count--})`;
        if (count < 0) {
            clearInterval(timer);
            this.disabled = false;
            this.value = '支付';
        }
    }, 1000);
});
通过以上方法的综合应用,可以有效地实现在线支付系统的幂等性,避免用户多次点击支付按钮导致的重复扣款问题。
posted @ 2025-03-10 09:53  Carvers  阅读(66)  评论(0)    收藏  举报