PAYATHON 2026

支付宝账务明细接口退款交易缺失 merchant_out_refund_no

支付小周

结论

仅凭现有信息,无法将 merchant_out_refund_no 缺失归因于 SDK、请求参数或商户权限。考虑到同一套调用方式只影响部分商家,而且重新查询历史数据后,原先存在的字段也会消失,问题更可能与支付宝账务明细的数据生成、字段映射或历史数据回补有关,并且不同商家之间可能存在差异。

在支付宝技术团队确认前,仍不能断定这是平台故障,也不能认定 merchant_out_refund_no 是所有【交易退款】账务记录的必返字段。建议按以下方式处理:

  1. 检查接口原始响应,确认字段是否在 SDK 模型转换或序列化过程中丢失。
  2. 整理商家账号、退款交易号、查询日期和接口响应等证据,提交支付宝工单。
  3. 不要再将账务明细接口作为退款请求号的唯一来源。
  4. 调用 alipay.trade.refund 时,自行保存 out_request_no。
  5. 使用原交易号、支付宝退款流水等字段辅助匹配。

为什么不像普通调用错误

目前的排查结果有两个比较明显的特征。

同一套调用程序返回不同

商家 A 能取得 merchant_out_refund_no,商家 B 无法取得,但双方使用的接口、SDK、请求参数和调用程序相同。因此,公共请求参数或普通代码错误导致该问题的可能性较低。

不过,“同一主体”不等于不同商家账号在支付宝内部必然走完全相同的数据链路。商家签约产品、交易渠道、退款入口、账务数据来源和历史数据迁移情况都可能不同。它们是否会影响该字段,需要支付宝结合具体商家账号核查,仅靠目前看到的现象无法确认。

重新查询历史数据时结果发生变化

如果业务系统过去曾保存同一条账务记录中的退款请求号,而现在查询相同日期时该字段已经缺失,说明接口返回的是支付宝当前的账务数据视图,不一定能在每次查询时还原此前响应中的所有扩展字段。

这类问题通常无法通过调整查询时间范围、重复调用或升级 SDK 解决,需要支付宝核查原始账务记录和字段映射。

先确认接口是否真的没有返回字段

提交工单前,应保存接口的原始响应字符串,不要只查看 SDK 转换后的 Java 对象。

部分 SDK、JSON 框架或内部 DTO 可能忽略以下内容:

  • 值为 null 的字段;
  • DTO 中没有声明的字段;
  • 命名不一致的字段;
  • 嵌套在扩展字段中的数据;
  • 反序列化失败但没有抛出异常的属性。

可以先对原始响应做最低限度的检查:

String rawResponse = response.getBody();

if (rawResponse == null || rawResponse.isBlank()) {
    throw new IllegalStateException("支付宝接口原始响应为空");
}

boolean containsRefundNo =
        rawResponse.contains("\"merchant_out_refund_no\"");

System.out.println("responseCode=" + response.getCode());
System.out.println("responseMsg=" + response.getMsg());
System.out.println("containsMerchantOutRefundNo=" + containsRefundNo);

生产环境记录日志时,应对商户订单号、支付宝交易号及其他敏感信息做脱敏处理,也不要记录签名、应用私钥等凭证。

如果原始响应中有该字段,但业务对象中没有,应检查 SDK 版本、响应模型和本地 JSON 映射。如果原始响应中同样没有该字段,问题更可能出在支付宝的返回数据或账务数据源,而非本地反序列化过程。

推荐的解决步骤

1. 准备可复现样本

分别准备正常商家和异常商家的退款样本,每类最好提供多个案例。每条样本至少记录:

  • 商家账号对应的 app_id;
  • 商家身份标识,如工单允许提供的 seller_id 或 PID;
  • 账务明细查询日期和时间范围;
  • 账务类型【交易退款】;
  • 原商户订单号;
  • 支付宝交易号;
  • 退款金额;
  • 退款发生时间;
  • 当次接口请求流水号,或网关返回的 trace_id、request_id 等定位信息;
  • 脱敏后的请求参数;
  • 完整且经过脱敏的原始响应;
  • 历史系统曾保存该退款请求号的证据。

具体能够提供哪些定位字段,应以接口实际返回的内容和支付宝工单要求为准,不要自行编造不存在的请求流水字段。

2. 请支付宝核查具体数据源

工单中不要只写“字段没有返回”,还应请支付宝针对具体退款记录确认以下事项:

  • 该字段在 alipay.data.bill.accountlog.query 中属于必返、条件返回,还是尽力返回;
  • 字段是否受退款渠道、产品类型、商家账号或历史数据来源影响;
  • 异常商家的退款记录中是否保存了原始 out_request_no;
  • 是否发生过账务数据重算、迁移或字段映射调整;
  • 重复查询历史日期时,是否可能返回不同的字段集合;
  • 当前数据能否回补;
  • 是否存在适合批量对账的替代数据源。

只有支付宝能根据商家账号和交易流水判断问题属于产品规则、数据缺失还是接口异常。

3. 当前对账不要依赖字段回补

在支付宝给出结论前,可以组合以下字段辅助定位退款:

  • 原商户订单号或支付宝交易号;
  • 退款金额;
  • 退款成功时间;
  • 支付宝侧退款流水号;
  • 退款状态;
  • 同一笔原交易的退款次数。

只使用“原交易号+退款金额”无法保证唯一匹配。同一笔交易可能发生多次部分退款,也可能有多笔金额相同的退款。无法唯一匹配的记录应进入人工核查队列,不宜自动关联。

从源头保存 out_request_no

merchant_out_refund_no 对应商户发起退款时传入的 out_request_no。更可靠的做法是,在调用 alipay.trade.refund 前,先将该编号与退款业务记录一并保存,不要等到账务接口返回后再获取。

String outRequestNo = refundOrder.getRefundRequestNo();

refundRepository.saveRequest(
        refundOrder.getMerchantOrderNo(),
        outRequestNo,
        refundOrder.getRefundAmount(),
        "CREATED"
);

AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizContent("""
{
  "out_trade_no": "%s",
  "refund_amount": "%s",
  "out_request_no": "%s"
}
""".formatted(
        refundOrder.getMerchantOrderNo(),
        refundOrder.getRefundAmount(),
        outRequestNo
));

AlipayTradeRefundResponse response = alipayClient.execute(request);

refundRepository.saveResponse(
        outRequestNo,
        response.getCode(),
        response.getMsg(),
        response.getTradeNo(),
        response.getBody()
);

这段示例只说明处理顺序。金额格式、JSON 转义、异常处理、数据库事务和敏感信息脱敏,仍需按照实际 SDK 版本和项目规范实现。

建议为退款请求号增加商户内唯一约束:

CREATE UNIQUE INDEX uk_refund_out_request_no
ON refund_request (merchant_id, out_request_no);

这样即使账务明细没有返回 merchant_out_refund_no,系统仍可通过本地退款台账完成关联。

退款查询接口能否补回请求号

核验单笔退款状态时,可以考虑使用 alipay.trade.fastpay.refund.query,但该接口通常需要原交易标识和 out_request_no。它适合核验已经知道退款请求号的记录,不能作为在完全不知道 out_request_no 时反向找回编号的通用方案。

AlipayTradeFastpayRefundQueryRequest request =
        new AlipayTradeFastpayRefundQueryRequest();

request.setBizContent("""
{
  "out_trade_no": "%s",
  "out_request_no": "%s"
}
""".formatted(outTradeNo, outRequestNo));

AlipayTradeFastpayRefundQueryResponse response =
        alipayClient.execute(request);

if (response.isSuccess()) {
    System.out.println("refundAmount=" + response.getRefundAmount());
    System.out.println("outRequestNo=" + response.getOutRequestNo());
} else {
    System.err.println(
            "code=" + response.getCode()
            + ", subCode=" + response.getSubCode()
            + ", subMsg=" + response.getSubMsg()
    );
}

具体的请求字段和响应字段,应以支付宝开放平台当前文档及所用 SDK 的实际模型为准。

如果本地完全没有保存 out_request_no,需要向支付宝确认能否通过支付宝交易号、支付宝退款流水号或账单下载产品取得退款请求号。不要默认存在一个可以根据账务记录反查 out_request_no 的公开接口。

临时对账策略

问题解决前,可以将退款明细分为三类:

账务明细存在 merchant_out_refund_no
    → 按退款请求号直接匹配

账务明细缺少 merchant_out_refund_no,但能通过支付宝退款流水唯一匹配
    → 标记为辅助匹配,并保存匹配依据

缺少退款请求号,其他字段也无法唯一匹配
    → 进入异常队列,禁止自动核销

还可以保存每次账务拉取时的字段完整度。一旦同一条历史账务记录的字段发生变化,系统就能及时发现,不必等到对账失败后再排查。

注意事项

  • “字段未返回”不等于“退款没有发生”。如果退款金额、时间和账务流水正常,缺失的可能只是关联字段。
  • 不要因为返回 JSON 中没有该字段,就直接将 merchant_out_refund_no 更新为 null,以免覆盖历史上已经保存的有效值。
  • 重新拉取历史数据时,应采用“非空更新”策略,保护已有的退款请求号。
  • 向支付宝反馈时不要只提供截图。原始请求、原始响应和具体交易样本更便于定位问题。
  • 不要认定同一主体下的所有商家账号都有完全相同的数据链路或产品配置。
  • 在支付宝明确接口契约前,不应把 merchant_out_refund_no 当作所有退款账务记录中稳定的必返字段。
  • 对账系统应以商户自行生成并保存的退款请求号为主要依据,支付宝账务数据用于核验,不应反过来依赖账务接口恢复商户侧标识。

备注:内容仅供参考。