Laravel 如何接收 payment gateway 回调的支付状态
结论
这段实现可能同时有两个问题:
Input没有导入。新版 Laravel 通常通过Request读取请求参数。- payment gateway 发起的是站外
POST请求,不会携带 Laravel 的 CSRF Token。请求可能还没进入 Controller,就被拦截并返回419。
处理支付回调时,应当为回调路由排除 CSRF 校验,通过注入的 Request 读取参数,验证支付网关签名,再根据网关订单号更新数据库,最后返回网关规定的成功响应。
Controller 中的问题
Controller 导入了 Request,但没有实际使用:
use Illuminate\Http\Request;
代码调用的是:
$status_code = Input::post('status_code');
当前命名空间是 App\Http\Controllers,代码又没有导入 Input。PHP 可能会尝试查找 App\Http\Controllers\Input,随后抛出类不存在错误。
即使显式导入旧版的 Input Facade,也不建议继续使用这种写法。可以把 Request 注入方法:
public function updateTransaction(Request $request)
{
$statusCode = $request->input('status_code');
}
如果 payment gateway 使用表单格式提交,或者通过正确的 Content-Type: application/json 发送 JSON,input() 通常可以读到对应参数。
定义回调路由
现代 Laravel 可以用类数组形式定义路由:
use App\Http\Controllers\NotifyController;
use Illuminate\Support\Facades\Route;
Route::post('/notify', [NotifyController::class, 'updateTransaction'])
->name('payment.notify');
回调不需要额外定义一个路径相同的 GET 路由。浏览器访问 GET /notify,与支付网关请求 POST /notify 是两回事。GET 页面能打开,并不能证明 POST 回调可以正常工作。
传给 payment gateway 的地址应当是完整且可从公网访问的 HTTPS 地址,例如:
$notifyUrl = route('payment.notify');
生产环境的 notify_url 不能使用 localhost、127.0.0.1 或局域网地址。
排除 CSRF 校验
Laravel 默认会校验 Web 路由收到的 POST 请求。payment gateway 无法获得当前应用生成的 CSRF Token,所以需要单独为回调接口排除这项校验。
使用 App\Http\Middleware\VerifyCsrfToken 的 Laravel 项目可以这样配置:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
protected $except = [
'notify',
];
}
一些较新的 Laravel 项目会在 bootstrap/app.php 中配置 CSRF 例外,写法类似:
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'notify',
]);
})
采用哪种写法,要看项目使用的 Laravel 版本和目录结构。只排除支付回调路径,不要关闭整个应用的 CSRF 保护。
接收并记录回调
可以先用下面的代码确认回调是否到达应用:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class NotifyController extends Controller
{
public function updateTransaction(Request $request)
{
Log::info('Payment notification received', [
'content_type' => $request->header('Content-Type'),
'payload' => $request->all(),
'raw_body' => $request->getContent(),
]);
$statusCode = $request->input('status_code');
if ($statusCode === null) {
Log::warning('Payment notification has no status_code');
return response('Missing status_code', 400);
}
Storage::disk('local')->put(
'payment_status.txt',
(string) $statusCode
);
return response('OK', 200);
}
}
Storage::disk('local') 对应哪个实际目录,由 config/filesystems.php 决定。不能只根据 Laravel 版本,就断定文件会直接写入 storage/app/payment_status.txt。需要检查 local disk 的 root 配置。
调试日志通常可以在应用日志中查看,例如:
storage/logs/laravel.log
排查 server-to-server callback 时,日志比 print 或 echo 更合适。支付网关通常只检查 HTTP 状态码和响应内容,不会把输出显示在用户浏览器中。
正式环境应更新订单,而不是只写文件
正式环境中,应根据支付网关返回的交易号或商户订单号,把支付状态写入数据库:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class NotifyController extends Controller
{
public function updateTransaction(Request $request)
{
Log::info('Payment callback received', [
'payload' => $request->all(),
]);
// 参数名必须以支付网关文档为准。
$orderNumber = $request->input('order_id');
$statusCode = $request->input('status_code');
$signature = $request->input('signature');
if (!$orderNumber || !$statusCode || !$signature) {
return response('Invalid payload', 400);
}
// 必须按照支付网关提供的算法验证签名。
if (!$this->verifyGatewaySignature($request->all(), $signature)) {
Log::warning('Invalid payment callback signature', [
'order_id' => $orderNumber,
]);
return response('Invalid signature', 401);
}
DB::transaction(function () use ($orderNumber, $statusCode) {
$order = Order::where('order_number', $orderNumber)
->lockForUpdate()
->firstOrFail();
// 实际成功值必须以网关文档为准。
if ($statusCode === 'success' && $order->payment_status !== 'paid') {
$order->payment_status = 'paid';
$order->paid_at = now();
$order->save();
} elseif ($statusCode === 'failure') {
$order->payment_status = 'failed';
$order->save();
}
});
// 响应内容必须符合支付网关的回调协议。
return response('OK', 200);
}
private function verifyGatewaySignature(array $payload, string $signature): bool
{
// 此处必须实现支付网关官方文档规定的验签算法。
return false;
}
}
不要直接使用示例里 verifyGatewaySignature() 的返回值。不同 payment gateway 的字段、签名算法、证书和成功响应格式并不相同,具体实现必须以对应网关的接口文档为准。
仍未收到回调时的排查顺序
可以按下面的顺序检查:
notify_url是否是完整且可从公网访问的 HTTPS 地址。- payment gateway 实际使用的是
POST、GET还是其他请求方法。 - 回调是否因 CSRF 校验返回
419。 - Web 服务器、反向代理或防火墙是否拦截了外部请求。
- 参数名是否确实是
status_code,并注意大小写和嵌套结构。 - 请求体采用表单、JSON、XML,还是网关自定义格式。
storage目录是否可写,localdisk 的根目录配置是否符合预期。storage/logs/laravel.log、Web 服务器访问日志和支付网关回调日志中是否记录了错误。- Controller 是否返回了网关要求的 HTTP
2xx状态码和确认文本。否则,网关可能会反复重试。
支付回调还需要保证幂等性。同一笔支付通知可能发送多次,应用不能因此重复发货、重复增加余额或重复执行其他业务操作。判断支付是否成功时,应以验签后的回调数据或主动查询支付网关得到的结果为准,不能只相信未经验证的 status_code。
备注:内容仅供参考。
