如何用 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 的真实性,并再次调用服务端交易验证接口,确认交易状态、金额、币种和交易引用。不要只依赖跳转页面中的查询参数,因为用户可能关闭页面、重复刷新或伪造请求。
备注:内容仅供参考。
