如何跟踪 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 已正确配置,网络故障、服务器维护或回调重试耗尽仍可能导致通知遗漏。建议每天运行一次对账任务:
- 找出已经到期,但本地仍无成功付款记录的订阅。
- 通过 PaySub 的交易查询接口或报表接口核实实际状态。
- 补录遗漏的交易。
- 对持续失败的订阅发出告警或暂停处理。
如果当前 PaySub 接口不支持逐期回调,就需要通过交易查询、结算报表或 PayGate 提供的其他通知机制跟踪付款。是否支持这些能力,需要 PayGate 根据具体商户账户和使用的 PaySub API 版本进行确认。
备注:内容仅供参考。
