PAYATHON 2026

如何用 Flutterwave API 更新 MySQL 支付状态?

支付阿杰

结论

付款验证成功后,应读取服务端验证响应中的 data.status 来判断交易状态,再通过 MySQLi 预处理语句更新数据库。

当前 SQL 直接拼接了 $payment_status 和 $tx_ref,而且字符串没有加引号。这既可能引发 SQL 错误,也会带来 SQL 注入风险。此外,$array["status"] 通常表示 API 请求是否成功,实际的交易状态应从 $array["data"]["status"] 读取。

推荐写法

以下代码基于这些条件:

  • controller.php 已创建 MySQLi 连接 $conn;
  • $response 来自 Flutterwave 服务端交易验证接口;
  • 数据库需要保存的目标状态是 success;
  • 当前订单的预期金额和币种分别保存在 $amountToPay 和 $expectedCurrency 中。
<?php

require_once 'controller.php';

$array = json_decode($response, true);

if (
    !is_array($array) ||
    !isset(
        $array['status'],
        $array['data']['id'],
        $array['data']['tx_ref'],
        $array['data']['amount'],
        $array['data']['currency'],
        $array['data']['status']
    )
) {
    exit('Invalid payment verification response');
}

$paymentId       = (string) $array['data']['id'];
$txRef           = (string) $array['data']['tx_ref'];
$transactionAmount = (float) $array['data']['amount'];
$currency        = (string) $array['data']['currency'];
$transactionStatus = (string) $array['data']['status'];

/*
 * Flutterwave 的具体成功状态值应以实际验证接口响应为准。
 * 常见交易成功状态为 "successful"。
 */
$isPaymentSuccessful =
    $array['status'] === 'success' &&
    $transactionStatus === 'successful' &&
    $transactionAmount >= (float) $amountToPay &&
    $currency === $expectedCurrency;

if (!$isPaymentSuccessful) {
    exit('Payment verification failed or unusual transaction detected');
}

/*
 * 如果数据库设计使用 "success",可在验证通过后映射为该值。
 */
$databaseStatus = 'success';

$stmt = $conn->prepare(
    'UPDATE transaction_details
     SET payment_id = ?, payment_status = ?
     WHERE tx_ref = ? AND payment_status = ?'
);

if (!$stmt) {
    exit('Unable to prepare database update');
}

$currentStatus = 'pending';

$stmt->bind_param(
    'ssss',
    $paymentId,
    $databaseStatus,
    $txRef,
    $currentStatus
);

if (!$stmt->execute()) {
    $stmt->close();
    $conn->close();
    exit('Unable to update payment record');
}

if ($stmt->affected_rows === 1) {
    $stmt->close();
    $conn->close();

    header('Location: congratulations.php');
    exit;
}

/*
 * affected_rows 为 0 可能表示:
 * 1. tx_ref 不存在;
 * 2. 订单已经被更新;
 * 3. 当前状态不是 pending。
 */
$stmt->close();
$conn->close();

exit('Payment record was not updated');

原代码的问题

读取了错误的状态字段

原代码使用的是:

$payment_status = $array["status"];

这个字段通常表示 Flutterwave API 请求本身是否成功,不一定说明付款已经完成。交易状态应读取:

$transactionStatus = $array['data']['status'];

只有确认交易状态为成功后,才能更新数据库。

SQL 字符串处理不正确

原来的 SQL 类似:

UPDATE transaction_details
SET payment_id=123, payment_status=success
WHERE tx_ref=ABC123

MySQL 会将 success 和 ABC123 视为列名,而不是字符串。手动添加引号或许能解决眼前的语法问题,却无法消除 SQL 注入风险。更稳妥的做法是使用预处理语句,由数据库驱动处理参数。

只比较金额还不够

下面的判断无法证明这笔付款属于当前订单:

if ($amountPaid >= $amountToPay)

还需要验证以下内容:

  • 交易状态已经成功;
  • tx_ref 与数据库中的订单一致;
  • 实付金额满足订单金额;
  • 币种与订单币种一致;
  • 该交易没有被其他订单使用。

预期金额和币种应从服务器数据库中读取,不能直接信任浏览器回传的数据。

输出内容会妨碍跳转

调用 header() 前不能向浏览器输出正文,因此不要在跳转前执行:

echo 'Payment successful';

数据库更新成功后,应直接跳转并终止脚本:

header('Location: congratulations.php');
exit;

统一状态值

Flutterwave 返回的交易成功状态可能与数据库采用的状态名称不同。例如,接口可能返回:

successful

而数据库需要保存:

success

验证通过后,可以显式转换为数据库使用的值:

$databaseStatus = 'success';

不能只因为顶层 status 是 success,就将订单标记为已支付。

其他注意事项

建议为 tx_ref 添加唯一索引,防止同一个业务订单产生多条支付记录:

ALTER TABLE transaction_details
ADD UNIQUE KEY uq_transaction_tx_ref (tx_ref);

使用下面的条件,可以使更新具备一定的幂等性:

WHERE tx_ref = ? AND payment_status = 'pending'

即使回调被重复调用,已经成功的订单也不会再次从 pending 状态更新。

如果通过 webhook 接收支付结果,还必须验证 webhook 的真实性,并再次调用服务端交易验证接口,确认交易状态、金额、币种和交易引用。不要只依赖跳转页面中的查询参数,因为用户可能关闭页面、重复刷新或伪造请求。

备注:内容仅供参考。