PAYATHON 2026

支付宝沙箱退款部分成功、部分失败并返回 code=20000

支付老李

结论

code=20000、msg=Service Currently Unavailable、sub_msg=系统异常 表示支付宝暂时无法正常完成接口处理。这是系统级异常,不是“商品不允许退款”之类的业务拒绝码,也不能说明退款一定失败。

支付宝退款通常只识别交易信息和退款请求,不会根据商品名称或 SKU 判断能否退款。如果一直是“某些商品可以退款,某些商品无法退款”,应先比较这些商品对应订单的金额、交易号、订单状态和请求组装逻辑。沙箱环境也可能出现数据异常或服务不稳定,但在核对请求和交易状态前,不能仅凭 20000 判断问题来自沙箱限制。

先检查退款参数

alipay.trade.refund 中需要重点检查以下参数:

  • out_trade_no:商户订单号,必须与支付时提交的订单号完全一致。
  • trade_no:支付宝交易号,应使用支付成功通知或查询接口返回的 trade_no。
  • refund_amount:本次退款金额,不能超过当前剩余可退金额。
  • out_request_no:退款请求号。同一笔交易多次退款时必须提供,用于标识每次退款操作。

还需要确认“支付宝回调流水号”具体指哪个字段。退款接口需要支付宝交易号 trade_no,而不是通知 ID、回调请求编号或应用自行生成的流水号。

每次退款操作应生成一个固定的 out_request_no。发生超时或返回 20000 后,重试时继续使用原请求号。不要每次重试都生成新值,否则支付宝可能将其识别为多次独立退款。

为什么看起来与特定商品有关

支付宝通常不了解商户内部的商品退款规则。如果不同商品的退款结果长期存在差异,问题通常出在相关订单数据上。

  1. 退款金额不同

    某些商品价格可能有三位以上小数,也可能存在浮点计算误差、优惠分摊误差,或者退款金额超过订单剩余可退金额。

    金额应使用十进制定点数处理,不要使用 double:

    BigDecimal refundAmount = new BigDecimal("19.90")
        .setScale(2, RoundingMode.UNNECESSARY);
  2. 保存了错误的支付宝流水号

    检查失败订单中保存的是否确实是 trade_no,尤其要排除误存通知 ID、支付请求号或其他订单 trade_no 的情况。

  3. out_trade_no 与 trade_no 不属于同一笔交易

    如果同时提交这两个字段,必须确保它们指向同一笔支付。排查时可以分别使用准确的 trade_no 或 out_trade_no 定位交易,避免本地关联错误造成歧义。

  4. 交易状态或可退金额不同

    发起退款前,通过 alipay.trade.query 查询交易,重点检查:

    • trade_status
    • total_amount
    • receipt_amount
    • buyer_pay_amount
    • 已退款金额及剩余可退金额

    交易需要支付成功且仍有可退金额,才能继续发起退款。

  5. 商品分支改变了请求内容

    检查代码是否根据商品类型、金额、订单来源或支付渠道走了不同的处理逻辑。例如,部分分支使用“元”,另一些使用“分”,或者某个分支取错了订单号。

推荐的请求方式

下面的示例使用支付宝 Java SDK。具体类名和初始化方式以项目实际使用的 SDK 版本为准:

AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();

JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", order.getOutTradeNo());
bizContent.put("trade_no", order.getAlipayTradeNo());
bizContent.put("refund_amount", order.getRefundAmount().toPlainString());
bizContent.put("out_request_no", refundRecord.getOutRequestNo());
bizContent.put("refund_reason", "用户申请退款");

request.setBizContent(bizContent.toJSONString());

AlipayTradeRefundResponse response = alipayClient.execute(request);

log.info(
    "Alipay refund result: outTradeNo={}, tradeNo={}, outRequestNo={}, " +
    "refundAmount={}, code={}, msg={}, subCode={}, subMsg={}, body={}",
    order.getOutTradeNo(),
    order.getAlipayTradeNo(),
    refundRecord.getOutRequestNo(),
    order.getRefundAmount().toPlainString(),
    response.getCode(),
    response.getMsg(),
    response.getSubCode(),
    response.getSubMsg(),
    response.getBody()
);

if (response.isSuccess()) {
    // 还应结合返回字段和后续查询确认本地退款状态
} else if ("20000".equals(response.getCode())) {
    // 不要立即认定退款失败,也不要生成新的 out_request_no 直接重退
    // 将记录置为“结果待确认”,随后调用退款查询接口
} else {
    // 按明确的业务错误码处理
}

收到 20000 后如何处理

20000 是系统级异常。收到该响应后,应先查询退款结果,不要立即使用新的退款请求号再次提交。

调用 alipay.trade.fastpay.refund.query,并传入原交易标识和原 out_request_no:

AlipayTradeFastpayRefundQueryRequest queryRequest =
    new AlipayTradeFastpayRefundQueryRequest();

JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", order.getOutTradeNo());
bizContent.put("trade_no", order.getAlipayTradeNo());
bizContent.put("out_request_no", refundRecord.getOutRequestNo());

queryRequest.setBizContent(bizContent.toJSONString());

AlipayTradeFastpayRefundQueryResponse queryResponse =
    alipayClient.execute(queryRequest);

log.info(
    "Alipay refund query: outTradeNo={}, outRequestNo={}, " +
    "code={}, msg={}, subCode={}, subMsg={}, body={}",
    order.getOutTradeNo(),
    refundRecord.getOutRequestNo(),
    queryResponse.getCode(),
    queryResponse.getMsg(),
    queryResponse.getSubCode(),
    queryResponse.getSubMsg(),
    queryResponse.getBody()
);

如果查询到退款结果,应更新本地退款记录,不再重复提交。只有在确认不存在该退款记录后,才能使用相同的 out_request_no 重试。重试次数应有限,并采用退避机制,例如间隔数秒后再次请求,避免短时间内连续调用。

建议的排查顺序

选择一笔退款成功的订单和一笔退款失败的订单,完整比较以下内容:

  • 支付时提交的 out_trade_no
  • 支付结果中的 trade_no
  • 支付查询返回的交易状态和金额
  • 退款请求的原始 biz_content
  • refund_amount 的字符串值和小数位数
  • out_request_no 是否保持不变且真正唯一
  • 应用、商户、沙箱账号和网关环境是否一致
  • 支付与退款使用的 app_id、私钥及支付宝公钥是否属于同一套沙箱配置

如果请求数据、交易状态和金额都一致,但部分沙箱交易仍然稳定返回 20000,可以新建一个完全不同的 out_trade_no,并使用测试买家重新支付验证。如果问题仍能复现,应保留以下信息并提交给支付宝技术支持:

  • app_id
  • 接口名称
  • 请求时间
  • out_trade_no
  • trade_no
  • out_request_no
  • 完整响应体
  • SDK 日志中的网关请求标识

不要提交商户私钥、应用私钥或完整的敏感账户信息。

注意事项

退款接口的同步响应异常,不代表资金一定没有变化。本地退款状态至少应区分“退款成功”“明确失败”和“结果待确认”。遇到超时、网络中断或 code=20000 时,应将记录设为待确认,再通过退款查询完成最终对账。

商品订单号必须唯一。同一商品被重复购买时,每次支付也要生成新的 out_trade_no。商品 ID 可以重复使用,支付订单号不能复用。只要交易标识、退款金额和退款请求号处理正确,退款结果通常与商品本身无关。

备注:内容仅供参考。