支付宝多次推送0.01支付成功但查不到支付记录
明确结论
收到 0.01 元的”支付成功”通知,并不代表系统里真的有这笔订单,也不能只根据 out_trade_no=16912277819535 判断这笔支付属于你的应用。
常见原因有以下几种:
- 回调接口没有严格验签,外部请求可以伪造支付宝通知。
- 其他支付宝应用、测试环境或历史系统也配置了这个回调地址。
- 通知确实来自支付宝,但其中的
app_id、seller_id或订单号不属于当前商户。
排查时,先保存完整的回调参数,再依次检查签名、app_id、seller_id、本地订单、金额和币种。只要有一项不符,就不能更新订单状态或发放权益。
为什么用订单号查不到
out_trade_no 由商户系统生成,并不是支付宝的全局唯一编号。其他商户可能生成相同的值,所以这个字段无法单独证明订单属于你的系统。
支付宝的交易流水号是 trade_no。排查时至少要保留以下字段:
app_idseller_idout_trade_notrade_notrade_statustotal_amountreceipt_amountnotify_idnotify_timesignsign_type
如果日志里只有金额和 out_trade_no,通常无法确认通知来源。
第一步:确认回调是否通过验签
攻击者可以直接向公开的 notify_url 发送 HTTP 请求。接口收到了请求,不等于请求来自支付宝。系统必须使用当前应用对应的支付宝公钥或证书验签。
以 Java SDK 的普通公钥模式为例:
@PostMapping("/alipay/notify")
public String handleNotify(HttpServletRequest request) throws Exception {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), String.join(",", entry.getValue()));
}
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
log.warn("Alipay notify signature verification failed: {}", safeLog(params));
return "failure";
}
// 验签通过后才能进行业务校验
return processVerifiedNotify(params);
}
如果项目采用支付宝公钥证书模式,应使用当前 SDK 提供的证书验签能力,不要把证书内容当成普通支付宝公钥传入。
验签时还要注意:
- 使用支付宝开放平台中与当前应用匹配的支付宝公钥或证书。
- 验签前不要自行拼接、排序或修改参数。
- 不要把
sign、sign_type放入待签名内容。 - 字符集必须与接入配置一致,常见值是
UTF-8。 - 即使请求来源 IP 看起来正常,也不能跳过验签。
- 回调参数可以写入日志,但不要记录商户私钥等敏感信息。
如果这些异常通知全部验签失败,通常可以判断为伪造请求或错误流量。
第二步:校验通知是否属于当前应用和商户
即使验签成功,也不能马上把订单改为已支付。系统还需要核对通知中的应用和收款方身份。
private String processVerifiedNotify(Map<String, String> params) {
String notifyAppId = params.get("app_id");
String notifySellerId = params.get("seller_id");
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
if (!configuredAppId.equals(notifyAppId)) {
log.warn("Unexpected app_id: {}", notifyAppId);
return "failure";
}
if (!configuredSellerId.equals(notifySellerId)) {
log.warn("Unexpected seller_id: {}", notifySellerId);
return "failure";
}
Order order = orderRepository.findByOutTradeNo(outTradeNo);
if (order == null) {
saveUnknownNotification(params);
return "failure";
}
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
if (order.getPayAmount().compareTo(notifiedAmount) != 0) {
log.error(
"Payment amount mismatch, outTradeNo={}, expected={}, actual={}",
outTradeNo,
order.getPayAmount(),
notifiedAmount
);
return "failure";
}
if (!"TRADE_SUCCESS".equals(tradeStatus)
&& !"TRADE_FINISHED".equals(tradeStatus)) {
return "success";
}
markOrderPaidIdempotently(order, tradeNo, params);
return "success";
}
实际业务还要检查以下内容:
- 订单的支付渠道确实是支付宝。
- 订单当前状态允许变更为已支付。
- 金额使用
BigDecimal比较,不能使用double。 - 如果业务保存了币种,还要核对币种。
- 同一个
trade_no不能关联多个本地订单。 - 同一订单收到重复通知时,处理必须幂等。
- 异常回调不能覆盖已关闭、已退款或已完成的订单状态。
第三步:主动查询支付宝交易
从回调日志中取得 trade_no 后,可以调用 alipay.trade.query 主动查询。查询时必须使用实际发生交易的应用和商户配置。
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setTradeNo(tradeNo);
// 也可以使用:model.setOutTradeNo(outTradeNo);
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
log.info(
"tradeNo={}, outTradeNo={}, tradeStatus={}, totalAmount={}",
response.getTradeNo(),
response.getOutTradeNo(),
response.getTradeStatus(),
response.getTotalAmount()
);
} else {
log.warn(
"Alipay query failed, code={}, subCode={}, message={}, subMessage={}",
response.getCode(),
response.getSubCode(),
response.getMsg(),
response.getSubMsg()
);
}
可以按下面的情况判断查询结果:
- 使用当前应用查询成功:交易可能真实存在,需要继续核对订单创建日志、商户身份和金额。
- 查询提示交易不存在:可能是假回调,也可能是查询时使用了错误的应用、环境或商户配置。
trade_no能查到,但用out_trade_no查不到:重点检查查询所用的app_id、商户账号,以及生产或沙箱环境。- 回调中没有
trade_no,或者缺少其他必要字段:优先按伪造请求或非标准请求处理,不能发货。
不要只相信回调里的交易状态。对于未知订单、金额不符等高风险情况,应通过查询接口再次确认。
第四步:检查回调地址是否被复用
需要检查所有可能配置过该地址的位置:
- 当前生产应用的异步通知地址。
- 支付宝沙箱应用。
- 已下线的旧应用或旧商户账号。
- 测试、预发布和生产环境。
- 多个 App 共用的支付网关。
- 第三方支付服务商或聚合支付平台。
- 服务器反向代理、网关重写和历史域名跳转。
如果多个应用必须共用一个入口,应先根据已验证的 app_id 路由到对应配置,再使用该应用的验签材料验证。不能用同一套固定配置处理所有通知。
第五步:补充可追踪日志
每次通知都应记录为独立事件,并保留以下信息:
received_at
request_id
source_ip
request_method
request_path
app_id
seller_id
out_trade_no
trade_no
trade_status
total_amount
notify_id
notify_time
sign_type
signature_verified
business_validation_result
response_body
原始参数应按照安全规范保存,保存时间至少覆盖支付宝可能重复通知的时间范围。sign 可以用于取证,但商户私钥和应用私钥绝不能写入日志。
还要结合网关访问日志,核对多次通知的情况:
- 参数和签名是否完全相同;
notify_id、trade_no是否相同;- 通知时间是否符合重试规律;
- 来源 IP 和 User-Agent 是否一致;
- 接口每次实际返回的是
success、failure,还是 HTML 页面。
如果同一笔有效通知反复到达,常见原因是接口没有返回纯文本 success,或者处理超时、执行过程中出现异常。返回内容必须是协议要求的文本,不能包装成 JSON:
success
未知订单应该怎样处理
收到未知订单的成功回调后,绝不能临时创建订单、充值或发货。可以先把通知存入隔离表,并标记为”待核查”。
返回 success 还是 failure,需要根据处理结果决定:
- 验签失败:返回
failure,同时记录安全日志。 - 验签成功,但
app_id或seller_id不属于当前系统:拒绝业务处理,并检查配置。 - 验签成功且属于当前商户,但本地订单不存在:先持久化完整通知,再主动查询交易。为避免无休止重试,可以在可靠保存异常事件后返回
success。这里的success只表示通知已接收,不代表订单有效。 - 订单存在且所有字段一致:使用事务和幂等机制更新订单,然后返回
success。
最终排查重点
对于 16912277819535,应先从原始回调中找到 trade_no、app_id、seller_id 和 sign。如果当时没有保存这些字段,只靠现有订单号很难继续追溯。
通知未通过验签,通常说明有人直接请求了回调接口。验签通过但身份字段不匹配,通常是回调地址被复用或配置有误。验签、商户身份和主动查询都正常时,应继续检查订单创建链路、数据库分库分表、数据清理任务,以及生产环境和测试环境是否查错了数据库。
备注:内容仅供参考。
