PAYATHON 2026

如何使用自定义 PHP 集成 POLi payment?

支付老李

结论

GenerateURL Request 是 POLi 支付流程的第一步,只负责创建 Payment URL,不能代表整个 PHP 集成已经完成。

完整流程通常包括:

  1. 在服务端创建订单并调用 GenerateURL。
  2. 解析 POLi 返回的 Payment URL。
  3. 将用户重定向到该 URL。
  4. 接收支付完成后的浏览器返回请求或服务器通知。
  5. 调用 POLi 的交易查询或验证接口,确认支付状态。
  6. 核对金额、币种和商户订单号,确认一致后再将本地订单标记为已支付。

不能因为用户回到了成功页面就认定付款成功,因为返回地址可能被直接访问,也可能被伪造。

先确认接口资料是否仍然有效

问题中的 PDF 很可能对应某个特定时期的 POLi PaymentAPI。开发前,需要通过当前有效的商户后台或官方技术支持确认以下信息:

  • PaymentAPI REST URL
  • XML 根节点及各字段名称
  • 身份验证方式
  • 测试和生产环境地址
  • 返回 XML 的结构
  • 支付状态查询接口
  • 通知、取消和返回地址的配置方式
  • 当前账户所在地区是否仍支持该产品

示例里的 PriceBusterDVD、MerchantPassword 和接口地址都只是占位内容,不能直接用于生产环境。原问题给出的 XML 还缺少标签,本身并不是可以直接发送的完整 XML 文档。

PHP 实现方式

可以使用 PHP 的 cURL 扩展从服务端调用接口。商户密码不能暴露在浏览器 JavaScript、HTML 表单或其他前端代码中。

下面是一套完整的请求框架。XML 节点名称需要替换为当前 POLi 文档规定的准确名称。如果 PDF 中提供了完整 XML,应以其中的 schema 为准。

<?php

declare(strict_types=1);

$endpoint = 'https://YOUR-POLI-ENDPOINT.example/GenerateURL';

$merchantCode = getenv('POLI_MERCHANT_CODE');
$merchantPassword = getenv('POLI_MERCHANT_PASSWORD');

if (!$merchantCode || !$merchantPassword) {
    throw new RuntimeException('POLi credentials are not configured.');
}

$orderReference = 'ORDER-20260901-0001';
$amount = '123.11';

$xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><GenerateURLRequest/>');

/*
 * 以下节点名称仅展示构造 XML 的方法。
 * 请按照当前 POLi 接口文档替换为准确的节点名称和层级。
 */
$xml->addChild('MerchantCode', htmlspecialchars($merchantCode, ENT_XML1));
$xml->addChild('MerchantPassword', htmlspecialchars($merchantPassword, ENT_XML1));
$xml->addChild('RequestType', 'Manual');
$xml->addChild('Amount', $amount);
$xml->addChild(
    'MerchantReference',
    htmlspecialchars($orderReference, ENT_XML1)
);
$xml->addChild('Notification', 'No');
$xml->addChild('SelectedFICode', 'No');

$requestBody = $xml->asXML();

if ($requestBody === false) {
    throw new RuntimeException('Unable to build POLi request XML.');
}

$ch = curl_init($endpoint);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $requestBody,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Content-Type: text/xml; charset=UTF-8',
        'Accept: text/xml',
        'Content-Length: ' . strlen($requestBody),
    ],
]);

$responseBody = curl_exec($ch);

if ($responseBody === false) {
    $message = curl_error($ch);
    curl_close($ch);

    throw new RuntimeException('POLi request failed: ' . $message);
}

$httpStatus = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($httpStatus < 200 || $httpStatus >= 300) {
    throw new RuntimeException(
        sprintf('POLi returned HTTP %d: %s', $httpStatus, $responseBody)
    );
}

libxml_use_internal_errors(true);
$responseXml = simplexml_load_string($responseBody);

if ($responseXml === false) {
    throw new RuntimeException('POLi returned invalid XML.');
}

/*
 * PaymentURL 的实际 XPath 取决于接口返回结构。
 * 请根据当前文档调整这里的节点路径。
 */
$paymentUrl = trim((string) ($responseXml->PaymentURL ?? ''));

if ($paymentUrl === '') {
    throw new RuntimeException(
        'POLi response does not contain a Payment URL.'
    );
}

if (filter_var($paymentUrl, FILTER_VALIDATE_URL) === false) {
    throw new RuntimeException('POLi returned an invalid Payment URL.');
}

header('Location: ' . $paymentUrl, true, 303);
exit;

如果当前接口使用 HTTP Basic Authentication、API Token 或自定义认证头,需要按文档补充相应的认证选项。例如,只有文档明确要求使用 Basic Authentication 时,才应加入:

curl_setopt($ch, CURLOPT_USERPWD, $merchantCode . ':' . $merchantPassword);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);

XML 中包含商户密码,并不代表接口一定还需要 Basic Authentication。两种认证方式是否同时使用,应以对应版本的官方接口说明为准。

正确处理金额和订单号

不要通过浮点运算直接生成金额。例如:

$amount = number_format(123.11, 2, '.', '');

更稳妥的方式是让订单系统用最小货币单位保存金额。例如,将 123.11 保存为 12311,发送请求时再进行格式化:

$amountInCents = 12311;
$amount = sprintf('%d.%02d', intdiv($amountInCents, 100), $amountInCents % 100);

商户参考号必须对应一笔唯一的本地订单,并由服务端生成。发起支付前,应先在数据库中保存:

  • 本地订单号
  • 预期金额及币种
  • 初始状态,例如 pending
  • POLi 返回的交易标识
  • 创建时间和后续状态变化

支付完成后必须再次验证

用户完成操作后,POLi 可能会将浏览器重定向回商户页面,也可能向商户服务器发送通知。无论采用哪种方式,都不能直接信任请求中的 status=success、金额或订单号。

回调可以按以下逻辑处理:

<?php

declare(strict_types=1);

$transactionToken = $_GET['token'] ?? '';

if ($transactionToken === '') {
    http_response_code(400);
    exit('Missing transaction token.');
}

/*
 * 1. 使用服务端保存的凭据调用 POLi 状态查询接口。
 * 2. 从官方响应中取得交易状态、金额、币种和商户参考号。
 * 3. 查找对应的本地订单。
 * 4. 对比预期金额、币种和订单号。
 * 5. 仅在官方状态明确表示支付成功时更新订单。
 * 6. 使用数据库事务或条件更新,保证重复通知不会重复入账。
 */

现有信息没有提供状态接口 URL、Token 参数名称和表示支付成功的状态值,因此这些内容不能自行补全。应直接采用对应 PDF 或商户后台给出的字段。

安全和上线注意事项

  • 将商户密码保存在环境变量或密钥管理服务中,不要写入代码仓库。
  • 生产环境必须使用 HTTPS,并保留证书校验,不要设置 CURLOPT_SSL_VERIFYPEER => false。
  • 正确转义 XML 内容,避免订单备注中的 &、< 等字符破坏请求。
  • Payment URL 必须取自 POLi 的成功响应,不要自行拼接。
  • 记录 HTTP 状态码、POLi 交易号和错误码,但不要记录密码、完整认证头或敏感银行信息。
  • 回调处理需要支持重复调用,避免同一笔交易被重复发货或记账。
  • 为支付请求设置连接超时和总超时,并为失败情况设计安全的重试机制。
  • 支付失败、超时或状态未知时,不能自动按支付成功处理。
  • 正式上线前,需要分别测试成功、取消、失败、超时、重复通知、金额不一致和网络中断等情况。

PDF 中的 GenerateURL Request 可以作为发起支付的依据,但还需要补齐准确的 XML 标签、接口地址、认证方式、响应解析和支付状态验证,才能形成可用的 PHP 集成。

备注:内容仅供参考。