PAYATHON 2026

如何在 PHP API 中集成 PayGate PayWeb3

支付老李

结论

ERROR=DATA_CHK 通常表示 PayGate 根据收到的字段重新计算了 MD5 校验值,但计算结果与请求中的 CHECKSUM 不一致。

完整的支付流程如下:

  1. 后端调用 initiate.trans 创建支付请求。
  2. 从响应中获取 PAY_REQUEST_ID 和 CHECKSUM。
  3. 用户浏览器将这两个字段提交到 process.trans,进入 PayGate 支付页面。
  4. 支付完成后,PayGate 把结果 POST 到 RETURN_URL。
  5. 后端调用 query.trans 查询并确认最终支付状态。

发起支付时调用 initiate.trans 没有问题,但它返回的内容不是最终支付结果。

为什么会出现 DATA_CHK

PayWeb3 并不是对 JSON、URL 编码后的字符串或字段名计算 MD5。计算校验值时,需要按照文档规定的顺序直接拼接字段值,并在末尾追加 Encryption Key:

PAYGATE_ID
+ REFERENCE
+ AMOUNT
+ CURRENCY
+ RETURN_URL
+ TRANSACTION_DATE
+ LOCALE
+ COUNTRY
+ EMAIL
+ Encryption Key

常见错误有:

  • 字段顺序与 PayGate 文档不一致。
  • 计算 CHECKSUM 时加入了字段名、&、= 或 URL 编码后的内容。
  • 生成校验值后又修改了某个字段。
  • 使用了其他环境的 PAYGATE_ID 或 Encryption Key。
  • 可选字段没有实际发送,却被加入校验字符串,或者字段已经发送但未参与校验。
  • AMOUNT 使用了小数或错误的单位。
  • 字符串中混入了不可见空格、换行,或者字符编码不同。
  • RETURN_URL、EMAIL 等字段在计算校验值时使用的内容,与实际发送的内容不完全一致。

PHP 数组通常会保留插入顺序,但签名计算不宜依赖通用的 implode('', $data)。直接写明参与签名的字段及其顺序,检查起来更直观,也能防止以后调整数组时意外破坏校验值。

建议的发起请求代码

下面的示例固定了字段顺序,并检查 cURL 请求和 PayGate 的返回结果:

public function subscription(Request $request)
{
    $validator = Validator::make($request->all(), [
        'subscription' => 'required|integer',
        'email' => 'required|email|exists:users,email',
        'amount' => 'required|integer|min:1',
        'payment_mode' => 'required|integer',
    ], [
        'exists' => 'The :attribute doesn\'t exist',
    ]);

    if ($validator->fails()) {
        return response()->json([
            'success' => false,
            'error_messages' => $validator->messages()->all(),
        ], 422);
    }

    $paygateId = 'YOUR_PAYGATE_ID';
    $encryptionKey = env('PAYGATE_ENCRYPTION_KEY');

    $data = [
        'PAYGATE_ID'       => $paygateId,
        'REFERENCE'        => 'pgtest_' . uniqid(),
        'AMOUNT'           => (string) $request->integer('amount'),
        'CURRENCY'         => 'ZAR',
        'RETURN_URL'       => route('paygate.return'),
        'TRANSACTION_DATE' => now()->format('Y-m-d H:i:s'),
        'LOCALE'           => 'en-za',
        'COUNTRY'          => 'ZAF',
        'EMAIL'            => trim($request->email),
    ];

    $checksumSource =
        $data['PAYGATE_ID'] .
        $data['REFERENCE'] .
        $data['AMOUNT'] .
        $data['CURRENCY'] .
        $data['RETURN_URL'] .
        $data['TRANSACTION_DATE'] .
        $data['LOCALE'] .
        $data['COUNTRY'] .
        $data['EMAIL'] .
        $encryptionKey;

    $data['CHECKSUM'] = md5($checksumSource);

    $ch = curl_init('https://secure.paygate.co.za/payweb3/initiate.trans');

    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => http_build_query(
            $data,
            '',
            '&',
            PHP_QUERY_RFC3986
        ),
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/x-www-form-urlencoded',
        ],
        CURLOPT_TIMEOUT => 30,
    ]);

    $rawResponse = curl_exec($ch);
    $curlError = curl_error($ch);
    $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    if ($rawResponse === false) {
        return response()->json([
            'success' => false,
            'message' => 'Unable to contact PayGate',
            'error' => $curlError,
        ], 502);
    }

    parse_str($rawResponse, $paygateResponse);

    if (isset($paygateResponse['ERROR'])) {
        return response()->json([
            'success' => false,
            'message' => 'PayGate rejected the initiation request',
            'paygate_error' => $paygateResponse['ERROR'],
            'http_status' => $httpStatus,
        ], 400);
    }

    if (
        empty($paygateResponse['PAY_REQUEST_ID']) ||
        empty($paygateResponse['CHECKSUM'])
    ) {
        return response()->json([
            'success' => false,
            'message' => 'Unexpected response from PayGate',
        ], 502);
    }

    return response()->json([
        'success' => true,
        'process_url' => 'https://secure.paygate.co.za/payweb3/process.trans',
        'PAY_REQUEST_ID' => $paygateResponse['PAY_REQUEST_ID'],
        'CHECKSUM' => $paygateResponse['CHECKSUM'],
    ]);
}

AMOUNT 一般要使用最小货币单位,例如:

3299 = ZAR 32.99

具体的金额单位和字段要求,仍需以当前 PayWeb3 商户文档为准。

跳转到支付页面

initiate.trans 成功后,前端不能只打开一个 URL,而要通过 HTML 表单向 process.trans 发起 POST 请求:

<form method="POST" action="https://secure.paygate.co.za/payweb3/process.trans">
    <input type="hidden" name="PAY_REQUEST_ID" value="PAYGATE返回的PAY_REQUEST_ID">
    <input type="hidden" name="CHECKSUM" value="PAYGATE返回的CHECKSUM">
    <button type="submit">Pay now</button>
</form>

也可以让后端生成表单,并自动提交:

<script>
    document.forms[0].submit();
</script>

这里不要重新计算 CHECKSUM,直接使用 initiate.trans 返回的值。

处理 RETURN_URL

支付结束后,PayGate 会将结果提交到 RETURN_URL。该地址必须能从公网访问:

Route::post('/payments/paygate/return', [
    PaymentController::class,
    'paygateReturn',
])->name('paygate.return');

示例地址:

http://localhost/avvata/public/home

这个地址只能由当前电脑访问,PayGate 服务器无法连接。开发时可以使用公开的 HTTPS 测试地址或安全的隧道服务,生产环境则应使用正式的 HTTPS 域名。

不能直接信任回调中的支付状态。后端需要验证返回的校验值,再通过 query.trans 确认交易状态,然后才能把订单更新为已支付。

使用 query.trans 确认结果

查询请求通常需要使用此前保存的 PAY_REQUEST_ID、原始 REFERENCE、商户 ID 和 Encryption Key 计算校验值:

$query = [
    'PAYGATE_ID' => $paygateId,
    'PAY_REQUEST_ID' => $payRequestId,
    'REFERENCE' => $reference,
];

$query['CHECKSUM'] = md5(
    $query['PAYGATE_ID'] .
    $query['PAY_REQUEST_ID'] .
    $query['REFERENCE'] .
    $encryptionKey
);

然后以 application/x-www-form-urlencoded 格式 POST 到:

https://secure.paygate.co.za/payweb3/query.trans

查询请求的字段及校验顺序可能与发起请求不同。两者必须分别按照当前 PayWeb3 文档实现,不能共用同一套签名逻辑。

排查 DATA_CHK 的实用方法

排查时可以临时记录以下内容,但不要把 Encryption Key 写入日志:

logger()->debug('PayGate initiate debug', [
    'data' => $data,
    'checksum_source_without_key' =>
        $data['PAYGATE_ID'] .
        $data['REFERENCE'] .
        $data['AMOUNT'] .
        $data['CURRENCY'] .
        $data['RETURN_URL'] .
        $data['TRANSACTION_DATE'] .
        $data['LOCALE'] .
        $data['COUNTRY'] .
        $data['EMAIL'],
    'checksum' => $data['CHECKSUM'],
]);

逐项检查:

  1. PAYGATE_ID 和 Encryption Key 是否属于同一商户,以及同一个测试或生产环境。
  2. 字段顺序是否与当前文档完全一致。
  3. 每个参与校验的字段是否都已实际发送。
  4. 实际发送的值是否与参与校验的值逐字一致。
  5. REFERENCE 是否唯一,前后是否存在空格。
  6. AMOUNT 是否为使用最小货币单位表示的整数。
  7. 是否错误地对 URL 编码后的字符串计算了 MD5。
  8. 是否把 CHECKSUM 本身加入了校验源字符串。

问题中的 Encryption Key 已经出现在代码示例里。如果这是真实密钥,应立即通过商户后台或 PayGate 支持渠道更换,并改为从环境变量中读取:

PAYGATE_ID=your_paygate_id
PAYGATE_ENCRYPTION_KEY=your_encryption_key

不要把真实密钥提交到 Git 仓库、发送给前端,或者写入公开日志。

备注:内容仅供参考。