PAYATHON 2026

Mercury payment system 支付金额未正确显示

支付阿杰

结论

问题出在初始化支付请求。函数虽然接收了 $amount,实际发送给 Mercury 的金额却被写死为 50.23:

$soap_request .= ' 50.23 ';

因此,支付网关不会显示调用函数时传入的 $amount。金额会在 InitializePayment 阶段与 paymentId 绑定。之后将 paymentId POST 到 CheckoutPOS.aspx 时,通常无法通过页面跳转请求再次修改金额。

请将硬编码金额替换为格式化后的 $amount,并确认它写入了 SOAP XML 中正确的金额元素。

修正金额参数

先将金额转换为 Mercury 接口要求的十进制格式:

$formatted_amount = number_format((float) $amount, 2, '.', '');
$formatted_tax = number_format((float) $tax, 2, '.', '');

构造 SOAP 请求时使用这两个变量:

$soap_request .= ' ' . $formatted_amount . ' ';
$soap_request .= ' ' . $formatted_tax . ' ';

原来的代码:

$soap_request .= ' 50.23 ';

至少应改为:

$soap_request .= ' ' . number_format((float) $amount, 2, '.', '') . ' ';

例如:

$payment_id = get_ms_payment_id(
    $ms_merchant_id,
    $ms_password,
    $order_header_id,
    125.50,
    0.00,
    $success_url,
    $failure_url
);

此时,初始化请求中实际传递的金额应为:

125.50

不要传入带货币符号或千位分隔符的值,如 $125.50、1,250.00。

检查完整的 SOAP XML

题目中的 $soap_request 只显示了空白字符串和数据值,没有显示 XML 元素名称。这可能是代码展示时标签被过滤,也可能是原始请求没有按预期生成。

发送请求前,可以临时记录原始 XML,确认金额位于 InitializePayment 要求的金额元素中:

error_log($soap_request);

需要检查以下内容:

  • XML 是否完整,格式是否合法;
  • $formatted_amount 是否位于正确的金额元素中;
  • 金额元素的名称和大小写是否与当前 WSDL 一致;
  • $tax 是单独的税额,还是已经包含在 $amount 中;
  • XML 中是否有多个金额字段,导致金额写入了错误位置;
  • XML 声明、SOAP Envelope 和命名空间是否正确。

由于示例没有保留具体的 XML 标签,无法准确判断金额元素的名称。请以当前 Mercury HostedCheckout WSDL 或接入文档为准,不要只按字段顺序拼接数据。

避免直接拼接未经转义的 XML

商户编号、订单号、URL 和备注等值可能包含 &、< 或 >。直接拼接这些内容可能破坏 XML,至少应先进行 XML 转义:

function xml_value($value)
{
    return htmlspecialchars(
        (string) $value,
        ENT_XML1 | ENT_QUOTES,
        'UTF-8'
    );
}

使用方式:

$formatted_amount = number_format((float) $amount, 2, '.', '');
$formatted_tax = number_format((float) $tax, 2, '.', '');

$merchant_id = xml_value($ms_merchant_id);
$password = xml_value($ms_password);
$order_id = xml_value($order_header_id);
$success = xml_value($success_url);
$failure = xml_value($failure_url);

更稳妥的方法是使用 DOMDocument、XMLWriter 或 SoapClient 生成 SOAP XML,避免依赖空格和字段顺序。

检查请求地址和 HTTP 头

当前代码中的请求地址和 HTTP 头并不一致。URL 是:

$url = "https://hc.mercurydev.net/hcws/hcservice.asmx";

手动设置的请求路径和主机却是:

"POST /ws/ws.asmx HTTP/1.1",
"Host: w1.mercurypay.com",

URL 指向 hc.mercurydev.net/hcws/hcservice.asmx,HTTP 头引用的却是另一个主机和路径。即使当前能够返回 paymentId,也建议删除这两项,由 cURL 根据 $url 自动生成请求行和 Host:

$header = [
    "Content-Type: text/xml; charset=utf-8",
    'SOAPAction: "http://www.mercurypay.com/InitializePayment"',
];

完整的 cURL 配置可以整理为:

$ch = curl_init($url);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => $header,
    CURLOPT_POSTFIELDS => $soap_request,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
]);

$result = curl_exec($ch);

if ($result === false) {
    throw new RuntimeException(
        'Mercury request failed: ' . curl_error($ch)
    );
}

$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code < 200 || $http_code >= 300) {
    throw new RuntimeException(
        'Mercury returned HTTP status ' . $http_code
    );
}

检查跳转到网关时提交的 paymentId

向以下地址提交数据时:

https://hc.mercurydev.net/CheckoutPOS.aspx

请确认 POST 字段的名称、大小写和编码完全符合 Mercury 文档。除了检查字段值是否像有效的 paymentId,还要确认:

  • 请求使用 POST,而不是查询字符串或 GET;
  • 隐藏字段名称与文档一致;
  • paymentId 没有被重复进行 URL 编码;
  • 初始化接口和支付网关属于同一环境;
  • 没有将测试环境生成的 ID 发往生产环境,也没有反向混用;
  • 没有重复使用已消费或已失效的 paymentId。

跳转表单通常类似下面的结构,但字段名必须以 Mercury 文档为准:

<form method="post" action="https://hc.mercurydev.net/CheckoutPOS.aspx">
    <input type="hidden" name="PaymentID" value="实际返回的 paymentId">
    <button type="submit">Pay</button>
</form>

如果文档规定的字段名不是 PaymentID,请改用文档中的准确名称。

不要只解析 paymentId

返回 paymentId 并不一定说明服务端接受了所有初始化参数。应保存并检查完整的 SOAP 响应,查看其中是否还有状态码、错误消息或警告字段:

error_log($result);

同时确认 curl_exec()、HTTP 状态码和 XML 解析均正常。当前代码直接访问多层数组:

$x_data["soap:Envelope"]["soap:Body"]

如果响应中的命名空间前缀发生变化,例如从 soap: 变为 soapenv:,这种解析方式可能失效。生产代码应根据 XML 命名空间解析响应,不要依赖前缀文本。

其他注意事项

正式环境中不要关闭 TLS 证书验证:

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

应保持证书验证开启,并正确配置系统 CA:

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

排查时,每次初始化都应使用唯一的 $order_header_id,避免服务端将重复订单号识别为已有交易。修改金额后,还必须重新调用 InitializePayment 获取新的 paymentId,因为旧的 paymentId 仍与旧金额关联。

先修正硬编码的 50.23,再核对 SOAP 请求中的金额元素、测试环境地址和网关 POST 字段。问题通常不在网关页面的显示逻辑,而在于 paymentId 初始化时已经绑定了错误的金额。

备注:内容仅供参考。