支付退款后未收到回调是否正常?
结论
仅凭“全额退款或最后一笔分批退款后没有收到回调”,还不能判断这种情况是否正常。
退款通常是异步处理的。平台是否发送退款回调、回调包含哪些状态,以及全额退款和部分退款是否使用同一种通知,都取决于支付平台和接口协议。退款完成后,平台一般不会再次触发“支付成功回调”,而是发送单独的“退款结果通知”。如果平台不提供退款通知,就需要主动调用退款查询接口来确认最终状态。
业务系统不能只靠回调判断退款是否成功,还应提供主动查询和定时补偿机制。
可能的原因
监听了错误的回调类型
支付成功通知和退款结果通知通常是两套独立机制。订单退款后,支付平台一般不会再次发送支付回调。
请确认系统配置了“退款结果通知地址”,而不只是支付通知地址。
退款通知地址未正确传递或配置
有些支付接口要求在发起退款时传入退款通知地址,另一些平台则要求在商户后台统一配置。如果通知地址为空、格式错误或无法通过公网访问,平台可能无法送达通知。
测试环境和正式环境的配置也要分别检查,避免正式退款请求仍指向测试地址。
最后一笔退款尚未完成
分批退款时,每笔退款通常都有独立的退款单号和处理状态。最后一次提交成功,只能说明平台已经受理退款请求,不代表资金已经退回。
最后一笔退款可能仍在处理中,也可能已经失败或需要人工审核,因此暂时没有成功回调。
回调已经到达,但业务处理失败
常见情况包括:
- 网关、WAF 或反向代理拦截了请求;
- HTTPS 证书异常;
- 服务响应超时;
- 验签失败;
- 解密失败;
- 业务代码抛出异常;
- 收到回调后,没有按平台要求返回成功响应;
- 系统使用支付订单号去重,把分批退款中的后续通知误判为重复请求。
分批退款时尤其要注意,幂等键通常应使用“退款单号”或“退款通知事件 ID”,不能只使用支付订单号。
排查步骤
1. 查询每一笔退款的最终状态
逐笔记录并查询以下信息:
- 商户订单号;
- 支付平台交易号;
- 商户退款单号;
- 支付平台退款单号;
- 本次退款金额;
- 累计退款金额;
- 退款状态;
- 退款申请时间和完成时间。
分批退款必须逐笔查询,不能仅凭累计退款金额判断最后一笔退款已经成功。
2. 核对接口文档
先确认当前支付产品是否支持退款结果通知,再检查以下内容:
- 全额退款是否发送通知;
- 部分退款是否逐笔发送通知;
- 通知地址在哪里配置;
- 通知重试规则;
- 成功响应格式;
- 签名验证和报文解密方式;
- 同一订单多次退款时,使用哪个字段区分各笔退款。
问题中没有说明具体的支付平台和 API,因此应以所接入平台当前的接口文档为准。
3. 检查回调链路
按退款发生的时间范围,检查回调入口、反向代理和业务服务的请求日志,确认是否出现非 2xx 响应、超时、验签失败或数据库异常。
也可以在测试环境中使用可控的公网地址接收通知,确认平台是否真正发出了请求。日志中不要记录完整密钥、签名私钥或未经脱敏的支付数据。
4. 建立主动查询补偿
退款请求被受理后,先将退款记录保存为 PROCESSING。如果超过预期时间仍未收到回调,由定时任务主动查询退款结果:
- 查询成功:更新为
SUCCESS; - 查询失败:更新为
FAILED,并记录失败原因; - 仍在处理中:保留
PROCESSING,稍后继续查询; - 查询不到退款单:核对退款请求是否确实提交成功。
查询频率和最长补偿时间应符合支付平台的限流要求和接口规范。
代码示例
下面的代码是与具体支付平台无关的处理框架。实际字段名、验签方法和成功响应格式需要替换成平台规定的内容。
@PostMapping("/callbacks/refund")
public ResponseEntity<String> handleRefundCallback(
@RequestHeader Map<String, String> headers,
@RequestBody String body) {
// verifySignature 需要按照支付平台规则实现
if (!verifySignature(headers, body)) {
return ResponseEntity.status(401).body("INVALID_SIGNATURE");
}
RefundNotification notification = parseRefundNotification(body);
// 分批退款应以退款单号或事件 ID 做幂等控制
String idempotencyKey = notification.getRefundId();
refundService.executeInTransaction(() -> {
if (refundEventRepository.exists(idempotencyKey)) {
return;
}
RefundRecord refund = refundRepository.findByRefundId(
notification.getRefundId()
);
if (refund == null) {
throw new IllegalStateException("Refund record not found");
}
refund.updateStatus(
notification.getStatus(),
notification.getRefundAmount()
);
refundEventRepository.save(idempotencyKey);
});
// 返回值应替换为支付平台要求的成功响应
return ResponseEntity.ok("SUCCESS");
}
定时补偿可以使用类似的逻辑:
@Scheduled(fixedDelay = 60_000)
public void reconcilePendingRefunds() {
List<RefundRecord> records =
refundRepository.findPendingRefundsForReconciliation();
for (RefundRecord record : records) {
RefundQueryResult result =
paymentClient.queryRefund(record.getRefundId());
switch (result.getStatus()) {
case SUCCESS:
refundService.markSuccess(
record.getRefundId(),
result.getRefundAmount()
);
break;
case FAILED:
refundService.markFailed(
record.getRefundId(),
result.getFailureReason()
);
break;
case PROCESSING:
// 保持处理中,等待下一次查询
break;
default:
refundService.recordUnknownStatus(
record.getRefundId(),
result.getStatus()
);
}
}
}
注意事项
回调可能重复、延迟或乱序,因此处理逻辑必须支持幂等。分批退款时,每笔退款都应使用唯一的退款单号,各笔退款金额之和不能超过原支付金额。
收到回调后,应先完成验签,再更新业务状态。不能因为 HTTP 请求已经到达就认定退款成功,也不能在退款刚提交时就向用户显示“退款到账”。
如果主动查询确认全额退款和最后一笔分批退款都已成功,只是没有收到通知,应继续检查退款通知配置和回调日志。如果接口文档明确说明相关场景不会发送通知,就应通过主动查询确认最终状态。若文档说明平台应发送通知,但多次复现后仍未收到,可携带商户订单号、退款单号、请求时间和脱敏日志联系支付平台技术支持。
备注:内容仅供参考。