PAYATHON 2026

Laravel 5 如何从 controller 携带数据 POST 重定向

支付小周

结论

Laravel 的 redirect() 无法生成携带自定义数据的外部 POST 请求。HTTP 重定向通常只会让浏览器向目标地址发起 GET 请求,不能像表单那样附带 MNT_ID 等 POST 字段。

一般可以这样处理:

  1. Controller 保存订单和配送信息。
  2. 生成支付参数和签名。
  3. 返回一个带有隐藏字段的中转页面。
  4. 中转页面自动向支付网关提交 POST 表单。

这样可以先保存本地数据,再让用户的浏览器通过 POST 请求进入支付网关。

Controller 中保存订单并准备支付参数

下面假设支付网关要求提交 MNT_ID、MNT_TRANSACTION_ID、MNT_AMOUNT 和 MNT_CURRENCY_CODE。具体字段名称、签名算法和金额格式应以支付平台的接口文档为准。

<?php

namespace App\Http\Controllers;

use App\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class PaymentController extends Controller
{
    public function pay(Request $request)
    {
        $this->validate($request, [
            'delivery_address' => 'required|string|max:500',
            'payment_method'   => 'required|string',
        ]);

        $order = DB::transaction(function () use ($request) {
            return Order::create([
                'delivery_address' => $request->input('delivery_address'),
                'payment_method'   => $request->input('payment_method'),
                'status'           => 'pending',
            ]);
        });

        // 金额应从服务器端订单数据中取得,不能信任客户端提交的金额。
        $amount = number_format($order->total_amount, 2, '.', '');

        $paymentData = [
            'MNT_ID'             => config('services.payment.merchant_id'),
            'MNT_TRANSACTION_ID' => (string) $order->id,
            'MNT_AMOUNT'         => $amount,
            'MNT_CURRENCY_CODE'  => 'USD',
        ];

        // 如果网关要求签名,应按其文档生成。
        $paymentData['MNT_SIGNATURE'] = $this->makeSignature($paymentData);

        return response()->view('payments.redirect', [
            'gatewayUrl' => config('services.payment.gateway_url'),
            'paymentData' => $paymentData,
        ]);
    }

    private function makeSignature(array $data)
    {
        $payload = implode('|', [
            $data['MNT_ID'],
            $data['MNT_TRANSACTION_ID'],
            $data['MNT_AMOUNT'],
            $data['MNT_CURRENCY_CODE'],
        ]);

        return hash_hmac(
            'sha256',
            $payload,
            config('services.payment.secret')
        );
    }
}

对应的路由可以写成:

Route::post('/payment/pay', 'PaymentController@pay');

这里通过数据库事务保存订单。事务提交后,Controller 不调用 redirect(),而是返回一个用于自动提交表单的 Blade 页面。

创建自动提交的 Blade 页面

创建 resources/views/payments/redirect.blade.php:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>正在跳转到支付页面</title>
</head>
<body>
    <p>正在进入支付页面,请稍候……</p>

    <form id="payment-form"
          method="POST"
          action="{{ $gatewayUrl }}">
        @foreach ($paymentData as $name => $value)
            <input type="hidden"
                   name="{{ $name }}"
                   value="{{ $value }}">
        @endforeach

        <noscript>
            <p>浏览器未启用 JavaScript,请点击下面的按钮继续。</p>
            <button type="submit">继续支付</button>
        </noscript>
    </form>

    <script>
        document.getElementById('payment-form').submit();
    </script>
</body>
</html>

浏览器收到页面后,会立即向支付网关提交表单。最终请求类似:

POST /payment HTTP/1.1
Host: gateway.example.com
Content-Type: application/x-www-form-urlencoded

MNT_ID=merchant123&MNT_TRANSACTION_ID=1001&MNT_AMOUNT=99.00

为什么不能直接使用 redirect()

下面的写法只能将参数放入 URL 查询字符串,不能创建 POST 请求:

return redirect()->away(
    'https://gateway.example.com/payment?MNT_ID=merchant123'
);

浏览器向目标地址发出的仍然是 GET 请求:

GET /payment?MNT_ID=merchant123 HTTP/1.1

redirect()->with() 同样解决不了这个问题:

return redirect()->away($gatewayUrl)->with('MNT_ID', $merchantId);

with() 通常会将数据写入当前 Laravel 应用的 Session。外部支付网站无法读取这个 Session,也就收不到其中的支付字段。

HTTP 的 307 和 308 状态码可以要求客户端保留原请求方法,但它们会重放原始请求及其请求体,无法用来重新组织支付参数。支付平台也未必支持这种接入方式。支付场景应采用网关明确要求的表单跳转方案。

另一种情况:由服务器直接调用支付接口

如果支付平台提供的是服务端 API,而非浏览器表单跳转接口,Controller 可以发起服务器间 POST 请求:

$response = $httpClient->post($gatewayUrl, [
    'form_params' => $paymentData,
]);

该请求发生在 Laravel 服务器和支付网关之间,不会将用户的浏览器带到支付页面。通常还要从响应中取得支付地址或支付凭证,再执行下一步:

$paymentUrl = $result['payment_url'];

return redirect()->away($paymentUrl);

自动提交表单和服务端 API 是两种不同的接入方式,具体选择取决于支付网关的协议,不能混用。

注意事项

  • 金额、币种和订单号等数据必须根据服务器端订单生成,不能直接使用浏览器提交的值。
  • 商户密钥只能保存在服务器配置中,不能输出到 Blade 页面。
  • 为支付参数生成签名时,字段顺序、字符编码和金额格式必须严格遵守网关文档。
  • 提交到外部网关的表单通常不需要 Laravel 的 @csrf。@csrf 是当前 Laravel 应用的防跨站请求令牌,支付网关一般无法识别。
  • 用户从支付页面返回并不代表付款成功。支付状态应通过网关的服务端通知、回调签名或订单查询接口确认。
  • 回调处理需要具备幂等性,防止支付平台重复通知时重复发货或重复更新订单。
  • 支付网关地址应使用 HTTPS,并通过配置文件或环境变量管理。
  • 自动提交页面应保留手动提交按钮,以便在 JavaScript 被禁用或自动提交失败时继续操作。

备注:内容仅供参考。