PAYATHON 2026

alipay-sdk-java-v3 transfer失败与异常如何处理?

支付老李

结论

判断转账结果时,不能只看是否抛出 ApiException,还要处理接口正常返回后的业务状态:

  1. 调用抛出 ApiException:说明本次调用没有取得 SDK 能正常反序列化的成功响应。原因可能是网络、签名、参数、权限或同步业务校验失败。此时必须记录并解析 responseBody。
  2. 接口正常返回:继续检查 response.status。
    • SUCCESS:转账成功。
    • FAIL:转账明确失败。
    • DEALING:结果尚未确定,不能直接判定成功或失败,需要调用转账查询接口确认最终状态。

ApiException.getErrorObject() 返回 null,不等于没有错误信息。支付宝返回业务错误时,当前 SDK 版本或对应接口模型不一定能把响应体反序列化为 errorObject。遇到这种情况,应读取 responseBody 中的 code、msg、sub_code 和 sub_msg。

为什么 errorObject 会是 null

只有同时满足以下条件,SDK 才可能填充 errorObject:

  • 返回内容符合 SDK 预期的错误结构;
  • 当前接口生成了对应的错误模型;
  • SDK 的异常转换逻辑成功完成反序列化。

支付宝开放平台的错误响应通常包含接口专属的响应节点,例如:

{
  "alipay_fund_trans_uni_transfer_response": {
    "code": "40004",
    "msg": "Business Failed",
    "sub_code": "具体业务错误码",
    "sub_msg": "具体错误原因"
  },
  "sign": "..."
}

这类响应已经包含完整的错误信息,但生成式 SDK 未必会将其转换为 errorObject。因此,错误处理不能只依赖 errorObject。

发生异常时,至少要记录:

  • e.getCode()
  • e.getMessage()
  • e.getResponseBody()
  • 请求流水号,如 out_biz_no
  • HTTP 响应头或支付宝返回的链路标识,前提是当前 SDK 可以取得这些信息

如果只记录 e.getMessage(),很容易丢失 sub_code 和 sub_msg。

异常和 status=FAIL 并不矛盾

两者对应不同的处理层级。

抛出 ApiException

这表示接口调用阶段没有得到 SDK 认可的正常响应。常见情况包括:

  • 网络连接失败或超时;
  • 请求参数不合法;
  • 签名失败;
  • 应用没有接口权限;
  • 收付款账户校验失败;
  • 余额不足等同步业务拒绝;
  • 返回数据无法反序列化。

付款方余额不足时进入 catch,说明在当前接口、账户和 SDK 的组合下,支付宝把这个场景作为同步业务错误返回。这并不表示所有转账失败都会抛出异常。

正常返回 status=FAIL

这表示网关已经正常处理请求并返回响应对象,但转账业务结果是失败。例如,订单可能先被受理,之后在业务处理阶段失败。

所以,官方文档中 status 可能为 FAIL 是合理的。失败究竟通过异常还是正常响应返回,取决于它发生在哪个阶段,不能根据余额不足这一个测试结果推断所有失败场景。

推荐的判断顺序

可以按下面的顺序处理:

  1. 调用 transfer。
  2. 捕获 ApiException,解析 responseBody 中的网关错误。
  3. 接口正常返回后检查 status。
  4. SUCCESS 按成功处理。
  5. FAIL 按明确失败处理,同时保存响应中的失败信息。
  6. 遇到 DEALING、空状态、未知状态、超时等结果不确定的情况,调用查询接口确认。
  7. 如果查询后仍无法确定结果,保留原来的 out_biz_no,进入人工处理或定时补偿流程,不能更换流水号直接重试。

代码示例

不同版本的 alipay-sdk-java-v3 可能生成不同的类名和 getter。下面的代码主要用于说明处理结构,实际接入时应以项目中的响应类型和方法签名为准。

try {
    AlipayFundTransUniTransferResponse response = api.transfer(request);

    if (response == null) {
        // 未取得有效响应,结果不能直接判定为失败
        markAsUnknown(outBizNo, "empty response");
        queryTransferResult(outBizNo);
        return;
    }

    String status = response.getStatus();

    if ("SUCCESS".equals(status)) {
        markAsSuccess(
            outBizNo,
            response.getOrderId(),
            response.getPayFundOrderId()
        );
        return;
    }

    if ("FAIL".equals(status)) {
        /*
         * 以当前 SDK 响应模型实际提供的字段为准。
         * 如果模型没有 error_code 或 fail_reason,不要自行猜测,
         * 应保存原始响应,并调用转账查询接口获取最终失败信息。
         */
        markAsFailed(
            outBizNo,
            getErrorCodeIfPresent(response),
            getFailReasonIfPresent(response)
        );
        return;
    }

    if ("DEALING".equals(status)) {
        markAsProcessing(outBizNo);
        queryTransferResult(outBizNo);
        return;
    }

    // 包括 null、空字符串以及未来可能新增的状态
    markAsUnknown(outBizNo, "unknown status: " + status);
    queryTransferResult(outBizNo);

} catch (ApiException e) {
    String responseBody = e.getResponseBody();

    GatewayError gatewayError = parseGatewayError(responseBody);

    if (gatewayError != null) {
        markAsRequestFailed(
            outBizNo,
            gatewayError.getCode(),
            gatewayError.getMsg(),
            gatewayError.getSubCode(),
            gatewayError.getSubMsg()
        );
    } else {
        // responseBody 为空或格式不符合预期时,保留原始内容
        markAsUnknown(
            outBizNo,
            "ApiException: " + e.getMessage()
        );
    }

    /*
     * 超时、连接中断等异常不一定代表支付宝没有执行请求。
     * 对结果不确定的异常,应使用原 out_biz_no 查询,不能直接重新转账。
     */
    if (isUncertainException(e)) {
        queryTransferResult(outBizNo);
    }
}

解析 responseBody 时,不要使用固定的字符串截取方式。可以通过 Jackson 读取响应节点:

public GatewayError parseGatewayError(String responseBody) {
    if (responseBody == null || responseBody.isBlank()) {
        return null;
    }

    try {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode root = mapper.readTree(responseBody);

        JsonNode errorNode =
            root.path("alipay_fund_trans_uni_transfer_response");

        if (errorNode.isMissingNode() || errorNode.isNull()) {
            return null;
        }

        GatewayError error = new GatewayError();
        error.setCode(text(errorNode, "code"));
        error.setMsg(text(errorNode, "msg"));
        error.setSubCode(text(errorNode, "sub_code"));
        error.setSubMsg(text(errorNode, "sub_msg"));
        return error;
    } catch (JsonProcessingException ex) {
        return null;
    }
}

private String text(JsonNode node, String fieldName) {
    JsonNode value = node.get(fieldName);
    return value == null || value.isNull() ? null : value.asText();
}

响应节点名称要以接口的实际返回值为准。可以先在测试环境中保存脱敏后的原始响应,确认响应结构后再确定解析路径。

error_code 和 fail_reason 从哪里获取

异常响应和正常业务响应需要分开处理:

  • 抛出 ApiException 时,通常读取 responseBody 中的 code、msg、sub_code 和 sub_msg。
  • 正常返回 status=FAIL 时,应从该接口响应模型声明的失败字段中读取原因。
  • 如果当前版本的转账响应类没有提供 error_code 或 fail_reason,应保存原始响应,再通过转账查询接口按 out_biz_no 或支付宝订单号查询,不能转而从 ApiException.errorObject 获取。
  • 查询接口会返回哪些错误字段,取决于当前接入的产品、接口版本及其响应模型。sub_code、error_code 和 fail_reason 不是完全相同的字段,不能混用。

即使暂时无法复现 status=FAIL,也不影响这套处理逻辑。该状态可能只会出现在请求已经受理、但后续业务处理失败的场景中。普通参数错误或余额不足不一定会返回这个状态。

注意事项

  • ApiException 不一定表示转账已经失败。发生读取超时或连接中断时,请求可能已经到达支付宝。
  • 对结果不确定的请求,应使用原来的 out_biz_no 查询。不要生成新的业务流水号直接重试,否则可能造成重复付款。
  • DEALING 既不是成功状态,也不是最终失败状态。
  • status=FAIL 应按明确失败处理,同时保存支付宝订单号、失败码、失败原因和原始响应,便于后续对账。
  • 错误处理不能依赖 errorObject 是否为空。
  • 日志中的姓名、账号、证件号等敏感信息需要脱敏,也不能输出私钥、应用密钥或完整请求报文。
  • 不同版本的 SDK 生成代码可能会调整异常模型和字段 getter。升级 SDK 后,需要重新验证异常响应、FAIL、DEALING 和超时查询流程。

备注:内容仅供参考。