本文目录导读:

实现PHP项目的支付回调处理,核心在于验证签名、处理幂等性和修改订单状态,不同支付平台(支付宝、微信支付)的细节略有不同,但整体流程一致。
下面是一个通用的、安全的PHP支付回调处理方案,以支付宝和微信支付为例,并包含关键注意事项。
核心原则(必读)
- 验签是第一步:必须验证回调通知是否来自支付平台,防止伪造请求。
- 幂等性设计(最重要):防止因网络重试导致同一订单被多次处理(多次发货、多次加款),通常使用
订单号+交易状态作为唯一处理依据。 - 先验签,后处理业务:验签失败直接返回失败信息,不要执行任何业务逻辑。
- 处理成功需返回成功标识:支付宝返回
success,微信返回SUCCESS(或<xml>...</xml>),否则支付平台会持续重试(通常3-7天)。
通用支付回调处理流程(伪代码,适用于大多数场景)
<?php
// 1. 接收支付平台的回调数据
// 支付宝:$_POST (同步) 或 file_get_contents('php://input') (异步)
// 微信支付: file_get_contents('php://input') 获取XML
$rawData = file_get_contents('php://input');
// 2. 解析数据
// 支付宝:直接是 $_POST 数组
// 微信支付:需要解析XML
$data = parseCallbackData($rawData); // 假设此函数已实现
// 3. 验签(最关键的步骤)
$isValid = verifySign($data, $paymentType); // 支付宝/微信的签名验证
if (!$isValid) {
// 记录日志:签名验证失败
error_log("支付回调签名验证失败: " . json_encode($data));
// 返回失败标识
echo $paymentType == 'alipay' ? 'failure' : '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>';
exit;
}
// 4. 获取核心参数
$outTradeNo = $data['out_trade_no']; // 商户订单号
$tradeNo = $data['trade_no']; // 支付平台交易号(支付宝/微信)
$totalAmount = $data['total_amount']; // 支付金额(单位:元)
$tradeStatus = $data['trade_status']; // 交易状态
// 5. 幂等性处理(防止重复回调)
// 使用数据库锁或订单状态检查
// 启动数据库事务
$db->beginTransaction();
try {
// 查询数据库中的订单
$order = $db->query("SELECT * FROM orders WHERE order_no = ?", [$outTradeNo]);
if (!$order) {
throw new Exception("订单不存在");
}
// 关键:检查订单是否已经处理过
if ($order['status'] == 'paid') {
// 订单已支付成功,直接返回成功(幂等)
$db->commit();
echo $paymentType == 'alipay' ? 'success' : '<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>';
exit;
}
// 6. 验证金额是否一致(防止篡改)
// 数据库中的订单金额(单位:分或元,需统一)
$orderAmount = $order['amount']; // 假设数据库存的是元
if (abs(floatval($totalAmount) - floatval($orderAmount)) > 0.01) {
throw new Exception("金额不匹配");
}
// 7. 验证交易状态
// 支付宝:TRADE_SUCCESS
// 微信:SUCCESS
if ($tradeStatus != 'TRADE_SUCCESS' && $tradeStatus != 'SUCCESS') {
// 未支付成功,记录但不报错
// 支付宝其他状态如WAIT_BUYER_PAY是等待支付,不做处理
if ($tradeStatus != 'WAIT_BUYER_PAY') {
throw new Exception("非成功状态的回调: " . $tradeStatus);
}
$db->commit();
echo $paymentType == 'alipay' ? 'success' : '<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>';
exit;
}
// 8. 业务逻辑处理(修改订单状态,更新库存/余额等)
$updateResult = $db->execute("UPDATE orders SET status='paid', pay_time=NOW(), trade_no=? WHERE order_no=?", [$tradeNo, $outTradeNo]);
if (!$updateResult) {
throw new Exception("订单状态更新失败");
}
// 9. 其他业务逻辑(如:积分增加、虚拟商品发货等)
// ...
// 10. 提交事务
$db->commit();
// 11. 返回成功标识给支付平台
echo $paymentType == 'alipay' ? 'success' : '<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>';
} catch (Exception $e) {
// 回滚事务
$db->rollBack();
error_log("支付回调处理失败: " . $e->getMessage());
// 返回失败标识,让支付平台重试
echo $paymentType == 'alipay' ? 'failure' : '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $e->getMessage() . ']]></return_msg></xml>';
}
基于SDK的推荐实现(更安全、更简洁)
绝大多数情况推荐使用官方SDK,因为签名算法复杂且经常更新。
支付宝回调(使用官方SDK)
require_once 'vendor/autoload.php'; // 引入composer自动加载
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Util\ResponseChecker;
use Alipay\EasySDK\Kernel\Config;
// 配置(通常在项目初始化时配置一次)
$options = new Config();
$options->protocol = 'https';
$options->gatewayHost = 'openapi.alipay.com'; // 或沙箱地址
$options->signType = 'RSA2';
$options->appId = '你的APPID';
$options->merchantPrivateKey = '你的商户私钥(PEM格式)';
$options->alipayPublicKey = '支付宝公钥(PEM格式)';
Factory::setOptions($options);
// 处理异步通知
$result = Factory::payment()->common()->verifyNotify($_POST); // 验证签名
if ($result) {
// 签名验证通过
$outTradeNo = $_POST['out_trade_no'];
$tradeNo = $_POST['trade_no'];
$totalAmount = $_POST['total_amount'];
// 幂等性和金额验证(同上方案一中的步骤5-8)
// ...
// 处理成功
echo 'success';
} else {
// 验签失败
echo 'failure';
}
微信支付回调(使用官方SDK)
require_once 'vendor/autoload.php';
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use Psr\Http\Message\ResponseInterface;
// 配置(通常在项目初始化时)
$merchantId = '你的商户号';
$merchantSerialNumber = '你的商户证书序列号';
$merchantPrivateKey = '你的商户私钥(PEM格式)';
$wechatpayCertificate = '微信支付平台证书(PEM格式)';
// 创建实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantSerialNumber,
'privateKey' => $merchantPrivateKey,
'certs' => [$wechatpayCertificate],
]);
// 处理回调
$inWechatpaySignature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'];
$inWechatpayTimestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'];
$inWechatpaySerial = $_SERVER['HTTP_WECHATPAY_SERIAL'];
$inWechatpayNonce = $_SERVER['HTTP_WECHATPAY_NONCE'];
$inbody = file_get_contents('php://input');
// 1. 验签
$verified = $instance->get('v3/certificates')->verify($inWechatpaySerial, $inWechatpaySignature, $inbody, $inWechatpayNonce, $inWechatpayTimestamp);
if (!$verified) {
// 验签失败
echo '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>';
exit;
}
// 2. 解析body
$data = json_decode($inbody, true);
$resource = $data['resource'];
$ciphertext = $resource['ciphertext'];
$associatedData = $resource['associated_data'];
$nonce = $resource['nonce'];
// 3. 解密数据(微信支付回调内容对称加密)
$decrypted = Rsa::decrypt($ciphertext, $associatedData, $nonce, $merchantPrivateKey);
// $decrypted 现在是明文的JSON字符串
$paymentData = json_decode($decrypted, true);
$outTradeNo = $paymentData['out_trade_no'];
$tradeNo = $paymentData['transaction_id'];
$totalAmount = $paymentData['amount']['total'] / 100; // 微信单位是分,转为元
$tradeStatus = $paymentData['trade_state']; // 应为 'SUCCESS'
// 4. 幂等性、金额验证(同上)
// ...
echo '<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>';
关键注意事项
- 日志记录:所有回调数据(原始数据+处理后数据)必须记录到日志,便于排查问题。
- 异步与同步:
- 异步通知(最重要):由支付平台服务器发起的,必须在
php://input读取。 - 同步通知(Return URL):用户支付完成后浏览器跳转到的页面,不能作为最终支付成功的依据(用户可能关闭页面或掉线),同步通知可作为“支付中”的页面展示,但业务处理依赖异步通知。
- 异步通知(最重要):由支付平台服务器发起的,必须在
- 服务器时间:回调中常会验证
notify_time或time_expire,确保服务器时间准确(使用NTP同步)。 - 超时设置:回调脚本不要有
set_time_limit(0)等阻塞操作,应快速返回success,对于耗时业务(如发货),建议返回成功给支付平台后,再异步处理(通过消息队列、定时任务或单独进程)。 - 签名算法版本:确保你的SDK或实现代码与支付平台最新签名规则一致(例如支付宝RSA2)。
- 金额单位:支付宝金额是元(浮点),微信支付金额是分(整数),处理时务必统一单位比较,使用
int或bccomp(PHP高精度计算函数)比较金额,避免浮点数误差。 - 退款回调:同样需要验签和幂等处理,但逻辑是退款的业务处理。
测试建议
- 使用沙箱环境:支付宝、微信都有沙箱(测试)环境,使用测试账号免费调试。
- 本地调试:使用
ngrok或frp将公网地址映射到本地,方便断点调试回调代码。 - 模拟回调:很多支付平台支持在后台手动触发回调(模拟通知),用于测试幂等性。
最核心的三步:验签 -> 幂等性检查 -> 修改订单状态。 千万不要为了省事跳过验签或幂等性检查。