本文目录导读:

在PHP项目中实现支付回调处理是一个常见需求,这里以支付宝和微信支付为例,给出完整的安全处理流程和代码示例,核心原则是:验签、幂等、更新订单状态。
通用安全处理流程(关键)
支付回调是异步通知,必须严格按以下步骤处理,否则极易出现安全漏洞或重复发货:
- 验证签名:必须使用官方SDK或验证方法确认回调数据来自支付宝/微信,而不是伪造的请求。
- 检查订单状态:查询本地数据库,确认该笔订单处于“未支付”状态,避免重复处理。
- 验证金额:对比回调中的支付金额与数据库中订单的应付金额是否一致。
- 更新订单状态:将订单标记为“已支付”,并存入支付宝/微信的交易流水号。
- 返回成功响应:必须向支付平台返回字符串
success(或特定格式),否则平台会持续重试通知(通常持续数小时)。
支付宝回调处理(示例)
使用支付宝官方SDK(alibabacloud/client 或旧版 aop)。
接收回调数据
// notify.php
<?php
require_once 'vendor/autoload.php';
// 使用支付宝官方SDK
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Config;
// 初始化配置(建议从配置文件读取)
$config = new Config();
$config->protocol = 'https';
$config->gatewayHost = 'openapi.alipay.com';
$config->appId = 'your_app_id';
$config->signType = 'RSA2';
$config->alipayPublicKey = '支付宝公钥字符串';
$config->merchantPrivateKey = '商户私钥字符串';
Factory::setOptions($config);
// 获取POST过来的通知参数(支付宝是POST请求)
$params = $_POST;
try {
// 1. 验证签名
$result = Factory::payment()->common()->verifyNotify($params);
if (!$result) {
http_response_code(400);
echo 'fail'; // 支付宝收到fail会继续重试
exit;
}
// 2. 获取关键参数
$outTradeNo = $params['out_trade_no']; // 商户订单号
$tradeNo = $params['trade_no']; // 支付宝交易号
$totalAmount = $params['total_amount']; // 支付金额
$tradeStatus = $params['trade_status']; // 交易状态
// 3. 检查订单状态(从数据库查询)
$order = getOrderByOutTradeNo($outTradeNo); // 自定义函数
if (!$order) {
// 订单不存在
echo 'fail';
exit;
}
if ($order['status'] == 'paid') {
// 已经处理过,直接返回成功(幂等)
echo 'success';
exit;
}
// 4. 验证金额
if (abs($totalAmount - $order['amount']) > 0.01) {
// 金额不一致,可能存在风险
echo 'fail';
exit;
}
// 5. 只处理交易成功状态 (TRADE_SUCCESS 或 TRADE_FINISHED)
if ($tradeStatus == 'TRADE_SUCCESS' || $tradeStatus == 'TRADE_FINISHED') {
// 6. 更新数据库订单状态
updateOrderStatus($outTradeNo, 'paid', $tradeNo); // 自定义函数
}
// 7. 返回成功
echo 'success';
} catch (\Exception $e) {
// 记录日志
error_log('Alipay notify error: ' . $e->getMessage());
echo 'fail';
}
重要提示:
- 支付宝异步通知只有
POST,同步通知(return_url)是GET,不要在同步通知里修改订单状态,因为同步通知不可靠(用户可能关闭页面)。 verifyNotify方法已自动处理签名验证。
微信支付回调处理(示例)
微信支付回调使用XML格式,需要解析XML并验签(官方SDK已封装)。
使用官方SDK(推荐)
// wx_notify.php
<?php
require_once 'vendor/autoload.php';
use EasyWeChat\Factory;
$config = [
'app_id' => 'your_app_id',
'mch_id' => 'your_mch_id',
'key' => 'your_api_v3_key', // 注意:微信支付V3使用APIv3密钥
'cert_path' => 'path/to/cert.pem', // 证书路径
'key_path' => 'path/to/key.pem',
'notify_url' => 'https://yourdomain.com/wx_notify.php',
];
$app = Factory::payment($config);
// 微信支付回调处理
$response = $app->handlePaidNotify(function ($message, $fail) {
// 1. 从$message中获取数据(已经验签过)
$outTradeNo = $message['out_trade_no']; // 商户订单号
$transactionId = $message['transaction_id']; // 微信支付订单号
$totalFee = $message['total_fee']; // 支付金额(单位:分)
$openid = $message['openid'];
// 2. 查询本地订单
$order = getOrderByOutTradeNo($outTradeNo);
if (!$order || $order['status'] == 'paid') {
// 订单不存在或已支付,返回true告诉微信不再重复通知
return true;
}
// 3. 验证金额(注意比较单位:微信传的是分)
if ($totalFee != $order['amount'] * 100) {
// 金额异常,记录日志
return true; // 或者 return $fail('金额不一致');
}
// 4. 更新订单状态
updateOrderStatus($outTradeNo, 'paid', $transactionId);
// 5. 返回 true 表示处理成功
return true;
});
// 输出响应给微信服务器
$response->send();
说明:
handlePaidNotify自动完成签名验证和XML解析。- 如果回调函数返回
true,SDK会自动返回成功给微信;返回$fail('错误原因')则微信会重试。 - 务必注意金额单位:微信使用分,数据库一般用元,需要转换。
幂等性处理(重点)
支付平台可能会重复发送多次通知(尤其网络波动时),必须保证重复通知不会导致多次发货或多次入账。
推荐做法:
- 在更新订单状态时,使用 SQL 条件更新:
UPDATE orders SET status='paid' WHERE order_id=xxx AND status='unpaid',如果影响行数为0,说明已经处理过,直接返回成功。 - 或为订单新增一个
notify_id字段,用数据库唯一索引保证同一通知只处理一次。
日志记录(调试必备)
所有回调都应当记录完整日志,用于排查问题。
function logNotify($type, $params, $result) {
$log = [
'time' => date('Y-m-d H:i:s'),
'type' => $type,
'params' => json_encode($params),
'result' => $result,
];
file_put_contents('/var/log/payment_notify.log', json_encode($log) . PHP_EOL, FILE_APPEND);
}
常见问题
| 问题 | 解决方案 |
|---|---|
| 签名验证失败 | 检查支付宝公钥/微信API密钥是否正确,注意区分应用公钥和支付宝公钥 |
| 回调不触发 | 检查服务器外网可访问性、防火墙、nginx配置(不限制POST大小) |
| 重复通知 | 确保订单状态更新的幂等性 |
| 金额比较错误 | 注意微信是分,支付宝是元;比较时允许微小浮点误差(如abs($a-$b) < 0.01) |
生产环境建议
- 使用队列:回调处理中如果有短信、邮件等耗时操作,应放入消息队列异步处理,避免回调超时。
- 配置安全:公钥/密钥存储在环境变量或配置文件中,不要硬编码。
- 测试环境:使用支付宝沙箱、微信沙箱测试回调。
- 通知重试:支付平台重试间隔为15s/3min/10min等,确保你的接口能快速响应(< 5s)。
如果需要更详细的某个步骤(如生成订单、验签源码),可以提供具体支付渠道和版本再展开说明。