alipay-sdk-java-v3 transfer失败与异常如何处理?
结论
判断转账结果时,不能只看是否抛出 ApiException,还要处理接口正常返回后的业务状态:
- 调用抛出
ApiException:说明本次调用没有取得 SDK 能正常反序列化的成功响应。原因可能是网络、签名、参数、权限或同步业务校验失败。此时必须记录并解析responseBody。 - 接口正常返回:继续检查
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 是合理的。失败究竟通过异常还是正常响应返回,取决于它发生在哪个阶段,不能根据余额不足这一个测试结果推断所有失败场景。
推荐的判断顺序
可以按下面的顺序处理:
- 调用
transfer。 - 捕获
ApiException,解析responseBody中的网关错误。 - 接口正常返回后检查
status。 SUCCESS按成功处理。FAIL按明确失败处理,同时保存响应中的失败信息。- 遇到
DEALING、空状态、未知状态、超时等结果不确定的情况,调用查询接口确认。 - 如果查询后仍无法确定结果,保留原来的
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和超时查询流程。
备注:内容仅供参考。
