如何在 PHP API 中集成 PayGate PayWeb3
结论
ERROR=DATA_CHK 通常表示 PayGate 根据收到的字段重新计算了 MD5 校验值,但计算结果与请求中的 CHECKSUM 不一致。
完整的支付流程如下:
- 后端调用
initiate.trans创建支付请求。 - 从响应中获取
PAY_REQUEST_ID和CHECKSUM。 - 用户浏览器将这两个字段提交到
process.trans,进入 PayGate 支付页面。 - 支付完成后,PayGate 把结果 POST 到
RETURN_URL。 - 后端调用
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'],
]);
逐项检查:
PAYGATE_ID和 Encryption Key 是否属于同一商户,以及同一个测试或生产环境。- 字段顺序是否与当前文档完全一致。
- 每个参与校验的字段是否都已实际发送。
- 实际发送的值是否与参与校验的值逐字一致。
REFERENCE是否唯一,前后是否存在空格。AMOUNT是否为使用最小货币单位表示的整数。- 是否错误地对 URL 编码后的字符串计算了 MD5。
- 是否把
CHECKSUM本身加入了校验源字符串。
问题中的 Encryption Key 已经出现在代码示例里。如果这是真实密钥,应立即通过商户后台或 PayGate 支持渠道更换,并改为从环境变量中读取:
PAYGATE_ID=your_paygate_id
PAYGATE_ENCRYPTION_KEY=your_encryption_key
不要把真实密钥提交到 Git 仓库、发送给前端,或者写入公开日志。
备注:内容仅供参考。
