支付宝小程序支付此前正常,现在提示付款失败是什么原因?
结论
之前可以正常付款,现在却统一提示“付款失败”,问题通常不在小程序前端按钮,而是支付链路中的某个条件发生了变化。常见原因有:
- 商户支付产品的签约状态、账户状态或风控状态发生变化;
- 小程序
appId、商户身份与支付签约主体不匹配; - 应用私钥、支付宝公钥或证书配置失效;
- 服务端创建交易失败后,仍让前端继续拉起支付;
- 订单号重复、订单已关闭,或金额、订单参数不合法;
- 前端传入的
tradeNO错误、为空或已经失效; - 用户取消付款、余额不足或支付方式受限,但页面统一显示为付款失败;
- 网络、支付宝客户端或运行环境异常。
排查时不能只看页面上的“付款失败”。还要同时检查前端的失败回调、服务端创建订单的响应,以及支付宝侧记录的真实交易状态。
先确认失败发生在哪一步
支付宝小程序支付通常分为两个关键步骤:
- 服务端调用支付接口创建交易,取得支付宝交易号。
- 小程序调用
my.tradePay拉起收银台。
如果服务端创建交易时已经失败,前端自然无法完成付款。若交易创建成功,但拉起收银台或付款时失败,则要继续检查订单状态、用户支付条件和风险控制。
前端应完整记录支付回调,不能只显示一条固定提示:
my.tradePay({
tradeNO: tradeNo,
success: (result) => {
console.log('tradePay success:', JSON.stringify(result));
// 不建议仅凭前端回调确认最终支付结果
// 应请求服务端查询订单状态
queryOrderStatus();
},
fail: (error) => {
console.error('tradePay fail:', JSON.stringify(error));
my.alert({
title: '付款未完成',
content: error.errorMessage || error.message || '请稍后重试'
});
}
});
日志中应保留完整的错误码、错误信息、订单号、tradeNO、请求时间和当前环境。只记录“付款失败”四个字,很难判断故障出在哪一层。
常见原因及处理方法
1. 商户账户或支付产品状态异常
即使代码没有改动,商户账户、支付产品签约、结算能力或风控状态仍可能发生变化。例如:
- 支付产品尚未生效、被暂停或需要补充资料;
- 商户账户受到风险限制;
- 企业资质、证件或协议需要更新;
- 当前交易触发了支付宝侧风控;
- 某种支付方式不适用于当前买家。
先到支付宝开放平台和商家后台检查应用及商户状态,确认相关支付能力仍然有效。如果只有部分用户、部分金额或特定商品付款失败,应重点考虑支付方式限制和风险控制。
风控问题通常无法靠反复修改前端代码解决。应保存失败时间、商户订单号和支付宝返回的错误信息,再通过对应的官方支持渠道核查。
2. appId、商户主体或环境不匹配
检查应用实际运行时加载的配置,尤其要确认生产环境是否误用了沙箱配置、其他应用的密钥或错误的网关地址。
需要核对:
- 小程序的
appId; - 服务端请求支付接口时使用的
app_id; - 签约支付能力的商户主体;
- 应用绑定关系;
- 生产环境和沙箱环境;
- 网关地址及回调地址配置。
前端小程序与服务端创建交易时使用的应用身份必须一致。如果近期切换过应用、重新发布过小程序、迁移过商户主体或调整过服务端配置,应优先检查这一项。
3. 密钥或证书配置发生变化
如果应用近期更换过密钥、切换到证书模式或更新过证书,而服务端仍使用旧配置,支付请求可能因验签或身份认证失败而无法完成。
需要检查:
- 应用私钥是否属于当前应用;
- 支付宝公钥是否配置正确;
- 是否混用了“支付宝公钥”和“应用公钥”;
- 证书文件是否完整、是否过期;
- 部署环境是否加载了正确的证书;
- 签名方式及字符集是否与当前配置一致;
- 多台服务器是否使用同一套密钥配置。
不要在日志中输出完整私钥。可以记录应用 appId、证书序列号或配置版本,以确认服务器实际加载的是哪套配置。
4. 服务端创建交易失败
服务端调用 alipay.trade.create 等接口后,要先判断支付宝返回的结果是否成功,再把交易号交给小程序。不能只检查 HTTP 状态码,也不能在接口失败后继续返回空的 tradeNO。
下面是示意代码,具体类名和字段应以所使用的支付宝 SDK 版本为准:
AlipayTradeCreateRequest request = new AlipayTradeCreateRequest();
request.setNotifyUrl(notifyUrl);
AlipayTradeCreateModel model = new AlipayTradeCreateModel();
model.setOutTradeNo(outTradeNo);
model.setTotalAmount(totalAmount);
model.setSubject(subject);
model.setBuyerId(buyerId);
request.setBizModel(model);
AlipayTradeCreateResponse response = alipayClient.execute(request);
if (!response.isSuccess()) {
log.error(
"create payment failed, outTradeNo={}, code={}, subCode={}, msg={}, subMsg={}",
outTradeNo,
response.getCode(),
response.getSubCode(),
response.getMsg(),
response.getSubMsg()
);
throw new IllegalStateException("创建支付宝交易失败");
}
String tradeNo = response.getTradeNo();
if (tradeNo == null || tradeNo.isEmpty()) {
throw new IllegalStateException("支付宝未返回 trade_no");
}
排查时应重点查看支付宝响应中的这些字段:
codemsgsub_codesub_msgtrade_noout_trade_no
其中,sub_code 和 sub_msg 通常比页面上的统一提示更接近真实原因。
5. 订单号重复或订单状态异常
out_trade_no 必须在商户系统内保持唯一。重复使用旧订单号时,可能出现订单状态、金额或商品信息冲突。
常见情况包括:
- 时间戳精度不足,导致并发生成相同订单号;
- 支付失败后直接复用已经关闭的订单;
- 同一订单号对应的金额发生变化;
- 订单已经支付,却再次发起付款;
- 交易已经超时关闭;
- 数据库事务已经回滚,但外部交易仍然存在。
订单号可以由业务前缀、日期、数据库序列或随机标识组合生成,例如:
String outTradeNo =
"MP"
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ UUID.randomUUID().toString().replace("-", "").substring(0, 12);
用户重新付款时,应根据订单当前状态决定继续支付原交易还是创建新交易,不能无条件复用原订单号。
6. 金额和业务参数不合法
金额必须由服务端校验,不能直接信任前端上传的价格。常见问题有:
- 金额为
0或负数; - 金额格式不符合接口要求;
- 使用浮点数计算,导致金额出现精度误差;
- 商品标题为空或包含异常内容;
- 买家身份参数与当前登录用户不一致;
- 必填参数为空;
- 请求参数编码异常。
涉及金额计算时,应使用十进制定点类型,例如 Java 的 BigDecimal,不要直接使用 double:
BigDecimal unitPrice = new BigDecimal("19.90");
BigDecimal quantity = new BigDecimal("2");
BigDecimal totalAmount = unitPrice.multiply(quantity);
String paymentAmount = totalAmount.setScale(2, RoundingMode.HALF_UP).toPlainString();
不要只依赖前端支付结果
前端的支付成功回调只能说明客户端调用得到了相应结果,不能替代服务端对账。用户可能在付款后关闭页面,异步通知也可能因为网络问题延迟到达。
服务端应通过以下方式确认最终状态:
- 接收并验签支付宝异步通知;
- 使用
alipay.trade.query主动查询交易; - 以服务端记录的订单状态为准;
- 更新订单时保证幂等,防止重复发货或重复记账。
查询订单的示意逻辑如下:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(outTradeNo);
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
// 根据支付宝实际返回的交易状态更新本地订单
} else {
log.warn(
"query payment failed, outTradeNo={}, code={}, subCode={}, msg={}, subMsg={}",
outTradeNo,
response.getCode(),
response.getSubCode(),
response.getMsg(),
response.getSubMsg()
);
}
异步通知验签成功后,还要核对 app_id、商户订单号、金额和收款主体。不能只看到支付状态就直接发货。
建议的排查顺序
可以按照下面的顺序缩小问题范围:
- 记录一笔失败交易的商户订单号、发生时间和用户环境。
- 查看小程序
my.tradePay的完整失败回调。 - 检查服务端创建交易的完整响应,确认是否取得有效的
trade_no。 - 使用
out_trade_no或trade_no查询支付宝交易状态。 - 核对金额、订单号、买家身份和订单有效期。
- 核对生产环境中的
appId、密钥、证书、网关和商户绑定关系。 - 检查支付产品签约、商户账户及应用状态。
- 分别使用不同账号、不同金额和不同设备测试,判断问题是全量发生,还是只在特定条件下出现。
- 如果返回信息指向风控、权限或商户状态,应携带错误码和订单号联系支付宝官方支持核查。
注意事项
- 不要把应用私钥、支付宝公钥原文或用户敏感信息写入日志。
- 不要直接向用户展示内部异常堆栈。可以显示简洁提示,并在内部保留错误编号。
- 不要在前端生成最终金额。商品价格应由服务端重新计算。
- 不要因为前端提示失败就立即认定没有扣款,应查询服务端交易状态。
- 不要在用户连续点击时重复创建多笔交易,应增加防重复提交和订单幂等控制。
- 如果近期完全没有修改代码,应重点检查商户状态、证书配置、部署环境和订单数据,而不只是排查小程序页面。
- 不同 SDK、接口模式和商户接入方案返回的字段可能不同,应以实际响应和当前接入方式的官方接口文档为准。
备注:内容仅供参考。
