PHP 调用 local payment API 验证 transaction id 失败
结论
报错中的 Transaction Id () 为空,这通常说明验证接口实际收到的 CM_TID 是空值,并非某个非空的 transaction id 验证失败。
代码中还有一个明确的问题:$web_id = "" 没有填写。即使 CM_TID 正确,接口也可能因为无法匹配网站账户而拒绝验证。
另外,直接向 Cashmaal 账户充值获得的 transaction id,不一定是网站收款 API 创建的交易。verify_v2.php 可能只验证通过指定 web_id 发起,并由支付流程返回的 transaction id。具体范围应以 Cashmaal 当前的 API 文档或技术支持说明为准。
主要原因
1. CM_TID 没有传到 PHP
当前代码只读取:
$_POST['CM_TID']
如果 Cashmaal 通过查询字符串回跳,例如:
https://example.com/callback.php?CM_TID=2JW9651118P
transaction id 实际位于 $_GET['CM_TID'],此时 $_POST['CM_TID'] 没有值。
还需要确认回调字段名是否确实是 CM_TID。PHP 数组键区分大小写,CM_TID、cm_tid 和 transaction_id 是三个不同的字段。
2. web_id 为空或填写错误
示例中写的是:
$web_id = "";
这里必须替换为 Cashmaal 后台针对该网站生成的真实 Web ID:
$web_id = "YOUR_REAL_WEB_ID";
Web ID 通常要与发起这笔网站支付时使用的网站配置一致,不能用账户 ID、商户用户名或其他编号代替。
3. 使用了错误类型的 transaction id
“账户充值”和“网站订单支付”可能属于两种不同的业务。
如果 transaction id 来自手动充值、账户转账或其他资金操作,而不是通过 Cashmaal 网站支付接口完成的订单,验证接口可能找不到这笔交易,也可能因为它不属于当前 web_id 而拒绝验证。
建议通过完整流程测试:
- 在 Cashmaal 后台添加并配置网站。
- 使用该网站的
web_id发起支付。 - 完成支付。
- 从 Cashmaal 的回跳参数或服务器通知中取得
CM_TID。 - 使用同一个
web_id调用验证接口。
建议的排查方式
先检查应用实际收到了哪些参数,再调用验证接口。日志中不要记录密钥或其他敏感数据。
error_log('REQUEST_METHOD: ' . ($_SERVER['REQUEST_METHOD'] ?? ''));
error_log('CM_TID from POST: ' . ($_POST['CM_TID'] ?? '[missing]'));
error_log('CM_TID from GET: ' . ($_GET['CM_TID'] ?? '[missing]'));
需要逐项确认:
- 回调请求使用的是
GET还是POST。 - 实际参数名是否为
CM_TID。 CM_TID是否为空、包含空格,或被前端代码丢失。web_id是否为后台显示的真实值。- transaction id 是否来自该网站的支付流程。
- cURL 请求是否成功。
- HTTP 状态码是多少。
- 接口返回的是 JSON、HTML 错误页,还是空字符串。
json_decode()是否失败。
如果浏览器地址栏中已经出现 CM_TID,代码却只读取 $_POST,基本可以确定是请求方式不匹配。
改进后的代码
下面的示例同时兼容 GET 和 POST,并增加了对 cURL、HTTP 和 JSON 错误的检查。生产环境中,应优先按照 API 文档规定的请求方式读取参数,不建议长期无差别接受所有来源。
<?php
$web_id = 'YOUR_REAL_WEB_ID';
$CM_TID = trim((string) ($_POST['CM_TID'] ?? $_GET['CM_TID'] ?? ''));
if ($CM_TID === '') {
exit('Error: CM_TID was not received.');
}
if ($web_id === '' || $web_id === 'YOUR_REAL_WEB_ID') {
exit('Error: web_id is not configured.');
}
$query = http_build_query([
'CM_TID' => $CM_TID,
'web_id' => $web_id,
]);
$url = 'https://www.cashmaal.com/Pay/verify_v2.php?' . $query;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$result = curl_exec($ch);
if ($result === false) {
$curlError = curl_error($ch);
curl_close($ch);
exit('Verification request failed: ' . htmlspecialchars($curlError, ENT_QUOTES, 'UTF-8'));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300) {
exit('Verification service returned HTTP status ' . $httpCode);
}
try {
$obj = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
error_log('Cashmaal non-JSON response: ' . $result);
exit('Verification service returned an invalid response.');
}
if (!isset($obj['status'])) {
exit('Verification response does not contain status.');
}
if ((string) $obj['status'] !== '1') {
$error = isset($obj['error']) ? (string) $obj['error'] : 'Unknown verification error';
exit('Error: ' . htmlspecialchars($error, ENT_QUOTES, 'UTF-8'));
}
/*
* 不要只验证 status 和金额。
* 应同时核对 transaction_id、订单号、收款账户、币种、金额和网站信息。
*/
if (
!isset($obj['transaction_id'], $obj['USD_amount']) ||
!hash_equals($CM_TID, (string) $obj['transaction_id'])
) {
exit('Transaction data does not match the verification request.');
}
$expectedAmount = '2.000';
if (bccomp((string) $obj['USD_amount'], $expectedAmount, 3) !== 0) {
exit('Payment amount does not match the order.');
}
echo 'We received your payment.';
如果服务器没有安装 BCMath 扩展,可以先将金额换算为最小货币单位,再使用整数比较。支付金额不要直接使用浮点数处理。
安全和业务注意事项
原代码关闭了证书验证:
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
这会削弱 HTTPS 的身份验证能力,不应在生产环境中使用。正确的配置是:
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
如果启用后出现证书错误,应修复服务器的 CA 证书配置,而不是绕过验证。
判断支付成功时,也不能只检查:
$obj['status'] == 1
至少还要核对:
transaction_id是否与本次请求一致;order_id是否属于当前订单;- 收款账户是否为自己的商户账户;
trx_website或对应的网站标识是否正确;- 币种和金额是否完全匹配;
- transaction id 是否已经处理过。
数据库应为 transaction id 建立唯一约束,避免同一笔付款被用于多个订单。更新订单状态时,也应在事务中完成。
首先记录实际收到的 GET 和 POST 参数,找出 CM_TID 为空的原因。然后填写正确的 web_id,并确认 transaction id 确实来自该网站的支付流程。如果这三项都正确,接口仍然返回无效交易,可以将脱敏后的请求参数、HTTP 状态码和原始响应提交给 Cashmaal 支持方,请其确认该 transaction id 是否适用于 verify_v2.php。
备注:内容仅供参考。
