PAYATHON 2026

支付宝多次推送0.01支付成功但查不到支付记录

支付老李

明确结论

收到 0.01 元的”支付成功”通知,并不代表系统里真的有这笔订单,也不能只根据 out_trade_no=16912277819535 判断这笔支付属于你的应用。

常见原因有以下几种:

  1. 回调接口没有严格验签,外部请求可以伪造支付宝通知。
  2. 其他支付宝应用、测试环境或历史系统也配置了这个回调地址。
  3. 通知确实来自支付宝,但其中的 app_id、seller_id 或订单号不属于当前商户。

排查时,先保存完整的回调参数,再依次检查签名、app_id、seller_id、本地订单、金额和币种。只要有一项不符,就不能更新订单状态或发放权益。

为什么用订单号查不到

out_trade_no 由商户系统生成,并不是支付宝的全局唯一编号。其他商户可能生成相同的值,所以这个字段无法单独证明订单属于你的系统。

支付宝的交易流水号是 trade_no。排查时至少要保留以下字段:

  • app_id
  • seller_id
  • out_trade_no
  • trade_no
  • trade_status
  • total_amount
  • receipt_amount
  • notify_id
  • notify_time
  • sign
  • sign_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。如果当时没有保存这些字段,只靠现有订单号很难继续追溯。

通知未通过验签,通常说明有人直接请求了回调接口。验签通过但身份字段不匹配,通常是回调地址被复用或配置有误。验签、商户身份和主动查询都正常时,应继续检查订单创建链路、数据库分库分表、数据清理任务,以及生产环境和测试环境是否查错了数据库。

备注:内容仅供参考。