PAYATHON 2026

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 而拒绝验证。

建议通过完整流程测试:

  1. 在 Cashmaal 后台添加并配置网站。
  2. 使用该网站的 web_id 发起支付。
  3. 完成支付。
  4. 从 Cashmaal 的回跳参数或服务器通知中取得 CM_TID。
  5. 使用同一个 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。

备注:内容仅供参考。