PAYATHON 2026

如何跟踪 Paygate PaySub Recurring payment 通知

支付阿杰

结论

notify_url 应设为客户网站上可公开访问的 HTTPS 接口,用于接收 PayGate 服务器发送的每次扣款结果通知。接口收到通知后,不能根据访问时间直接认定扣款成功,还要校验签名、确认交易状态、执行幂等处理,并将交易关联到对应的订阅记录。

notify_url 和用户支付完成后跳转的 return_url 用途不同。前者接收 PayGate 服务器发给商户服务器的后台通知,即使捐款人没有打开网站,也必须能够正常工作。

PaySub 不同接入版本的通知字段和签名算法可能不同,是否主动回调每期扣款结果也可能有差异。具体配置应以当前 PayGate 提供的 PaySub 集成文档或商户后台为准,不能直接套用其他 PayGate 产品的字段。

配置 notify_url

先准备一个不需要登录、可从公网访问的固定地址,例如:

https://example.com/payments/paygate/notify

如果 PaySub 的订阅创建请求支持 notify_url 或 NOTIFY_URL 参数,可在创建订阅时提交该地址。参数名称以及参数是否参与签名,都必须严格遵循当前使用的 PaySub API 文档。

如果请求中没有这个参数,通常需要在 PayGate 商户后台统一设置通知地址,或由 PayGate 为商户账户开通回调。官方示例未提供 notify_url 时,不要自行猜测参数名。应向 PayGate 支持团队确认:

  • PaySub 是否会主动通知每一期 recurring payment 的结果。
  • 通知地址是在请求中提交,还是在商户后台统一设置。
  • 回调采用 POST、XML、JSON 还是表单编码。
  • 回调会提供哪些订阅号、交易号和状态字段。
  • 签名或 checksum 如何计算。
  • 接口需要返回什么内容,通知失败后如何重试。

建议的数据关联方式

创建订阅时,先为本地捐赠记录生成唯一编号,再将其作为 PaySub 支持的商户 reference 提交:

DONATION-SUB-20260831-000123

本地至少需要保存:

local_subscription_id
merchant_reference
paygate_subscription_id
expected_amount
currency
billing_interval
next_due_date
subscription_status

每次收到通知后,单独创建一条付款记录:

provider_transaction_id
merchant_reference
paygate_subscription_id
due_date
paid_at
amount
currency
provider_status
raw_notification

不要将”订阅”和”每期付款”保存在同一条记录中并反复覆盖,否则无法追踪某一期付款的失败、重试或重复扣款。

notify_url 处理示例

下面的 PHP 示例用于说明处理流程。代码中的字段名和签名逻辑都是占位内容,必须替换为 PayGate 当前文档规定的实际字段和规则。

<?php

// PayGate 可能发送 application/x-www-form-urlencoded、XML 或 JSON。
// 这里仅演示表单 POST。
$payload = $_POST;

if (empty($payload)) {
    http_response_code(400);
    exit('Missing payload');
}

// 以下字段名仅为结构示例,不能视为 PaySub 的官方字段。
$reference     = $payload['REFERENCE'] ?? null;
$transactionId = $payload['TRANSACTION_ID'] ?? null;
$status        = $payload['STATUS'] ?? null;
$amount        = $payload['AMOUNT'] ?? null;
$checksum      = $payload['CHECKSUM'] ?? null;

if (!$reference || !$transactionId || !$status || !$checksum) {
    http_response_code(400);
    exit('Missing required fields');
}

// 必须根据 PayGate 文档规定的字段顺序、编码及密钥计算 checksum。
// 不要直接使用下面这个拼接规则。
$expectedChecksum = calculatePaygateChecksum($payload, PAYGATE_SECRET);

if (!hash_equals($expectedChecksum, $checksum)) {
    http_response_code(401);
    exit('Invalid checksum');
}

// 根据 reference 查找本地订阅。
$subscription = findSubscriptionByReference($reference);

if (!$subscription) {
    http_response_code(404);
    exit('Unknown reference');
}

// 使用 PayGate 的唯一交易号做幂等判断。
// 同一个通知可能因网络超时被重复发送。
if (paymentExists($transactionId)) {
    http_response_code(200);
    exit('OK');
}

// 如果 PaySub 提供交易查询接口,建议再向 PayGate 查询一次交易状态,
// 然后使用查询结果更新本地记录。
$verifiedStatus = $status;

saveRecurringPayment([
    'subscription_id'        => $subscription['id'],
    'provider_transaction_id'=> $transactionId,
    'amount'                 => $amount,
    'provider_status'        => $verifiedStatus,
    'raw_notification'       => json_encode(
        $payload,
        JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
    ),
]);

if (isSuccessfulPaygateStatus($verifiedStatus)) {
    markPaymentAsPaid($transactionId);
} else {
    markPaymentAsFailedOrPending($transactionId, $verifiedStatus);
}

http_response_code(200);
exit('OK');

签名计算函数不能凭经验编写。下面只保留函数接口,具体实现必须以对应版本的 PayGate 文档为准:

function calculatePaygateChecksum(array $payload, string $secret): string
{
    // 按 PayGate 文档要求:
    // 1. 选择参与签名的字段;
    // 2. 使用规定的字段顺序;
    // 3. 按要求拼接密钥;
    // 4. 使用指定的摘要算法和大小写格式。
    throw new RuntimeException('Implement according to the PaySub specification');
}

状态处理

付款状态不能只分为成功和失败。支付平台的回调通常还会出现处理中、拒绝、取消或重试等状态。可以先保存 PayGate 返回的原始状态,再映射为本地状态:

function mapPaymentStatus(string $providerStatus): string
{
    return match ($providerStatus) {
        // 请替换为 PaySub 文档中的真实状态值。
        'SUCCESS_VALUE' => 'paid',
        'PENDING_VALUE' => 'pending',
        'FAILED_VALUE'  => 'failed',
        default         => 'unknown',
    };
}

只有 PaySub 明确定义为成功的状态,才能记为已收款。遇到未知状态时,应保留原始通知,等待后续查询或人工核对,不能按成功处理。

防止重复入账

支付通知通常采用”至少发送一次”的机制。如果商户接口响应超时,PayGate 可能重复发送同一笔交易的通知,因此整个处理过程必须具备幂等性。

可以为数据库中的 PayGate 交易号添加唯一约束:

CREATE UNIQUE INDEX uq_paygate_transaction
ON recurring_payments (provider_transaction_id);

如果通知中没有可作为唯一键的交易号,应向 PayGate 确认能用哪些字段组合识别同一期付款。不要只使用 reference,因为同一订阅的每期付款通常共用一个订阅 reference。

安全和可靠性注意事项

  • 使用有效的 HTTPS 证书。
  • 不要依赖浏览器 Cookie、登录状态或 CSRF token。
  • 只接受 PayGate 规定的请求方法和内容类型。
  • 校验 checksum、签名或消息认证字段。
  • 将金额、币种和订阅编号与本地记录逐一核对。
  • 不要只依赖来源 IP,因为 PayGate 的出口地址可能变化。
  • 保存原始通知,但不要在日志中记录银行卡资料或密钥。
  • 数据库更新成功后,再返回 PayGate 要求的确认响应。
  • 如果处理时间较长,可以先持久化通知,再交给队列处理。
  • 记录每次通知的接收时间、处理结果和错误原因。

不要完全依赖通知

即使 notify_url 已正确配置,网络故障、服务器维护或回调重试耗尽仍可能导致通知遗漏。建议每天运行一次对账任务:

  1. 找出已经到期,但本地仍无成功付款记录的订阅。
  2. 通过 PaySub 的交易查询接口或报表接口核实实际状态。
  3. 补录遗漏的交易。
  4. 对持续失败的订阅发出告警或暂停处理。

如果当前 PaySub 接口不支持逐期回调,就需要通过交易查询、结算报表或 PayGate 提供的其他通知机制跟踪付款。是否支持这些能力,需要 PayGate 根据具体商户账户和使用的 PaySub API 版本进行确认。

备注:内容仅供参考。