支付超时之后:微信用原单号重试,支付宝预下单却要换新单号
调用支付接口时网络超时,或者日志里躺着一条 HTTP 500,订单到底是成功了还是失败了?这个问题的答案不能靠猜。
支付宝官方在异常处理文档里的措辞是:「在调用支付宝接口时,可能会遇到网络超时或支付宝未知异常(接口返回 code=20000,sub_code=isp.unknow-error 或 ACQ.SYSTEM_ERROR),此时业务处理结果是未知的」。超时、连接被重置、HTTP 500/501/503,都只说明结果未知,不代表失败,也不代表成功。
微信支付 v3:状态码与错误码
微信支付 v3 的 HTTP 状态码有明确语义:处理成功且有应答体返回 200,无应答体返回 204;已被成功接受待处理返回 202;请求处理失败(缺参数、余额不足等)返回 4xx;微信侧服务系统错误返回 500/501/503(较少见)。
响应体的结构化错误为 code(错误码)、message(描述)、detail{field, value, issue, location},其中 field 是 JSON Pointer,比如 /amount/currency,location 取值 body/url/query。公共错误码包括 PARAM_ERROR、INVALID_REQUEST(HTTP 请求不符合 APIv3 接口规则)、SIGN_ERROR(验证不通过)、SYSTEM_ERROR(系统异常请稍后重试)。业务错误码示例有 NO_AUTH(商户无权限)、OUT_TRADE_NO_USED(商户订单号重复)。
JSAPI 下单错误码表里值得记住的分组:400 对应 PARAM_ERROR/INVALID_REQUEST/APPID_MCHID_NOT_MATCH/MCH_NOT_EXISTS/ORDER_CLOSED;401 SIGN_ERROR;403 NO_AUTH/OUT_TRADE_NO_USED/RULE_LIMIT/TRADE_ERROR/ACCOUNT_ERROR;404 ORDER_NOT_EXIST;429 FREQUENCY_LIMITED;500 对应 BANK_ERROR「银行系统异常,请用相同参数重新调用」、INVALID_TRANSACTIONID、OPENID_MISMATCH、SYSTEM_ERROR「系统异常,请用相同参数重新调用」。注意 500 系列官方给的方案一致:用相同参数重新调用。
商户订单号:可复用,但有边界
微信商户订单号由商户自定义,只支持字母、数字、中划线 -、下划线 _、竖线 |、星号 * 的英文半角组合,不要用汉字或全角;要求唯一(建议系统时间 + 随机序列)。重新发起一笔支付要用原订单号,避免重复支付;已支付过、或已调用关单/撤销的订单号不能重新发起支付。JSAPI/合作伙伴文档中 out_trade_no 标注为 string(32),同一商户号下唯一。
也就是说:原单号重试是幂等的、安全的——只要这笔单没有被支付成功、没有被关单或撤销。但一旦支付成功或已关单,同一个单号就再也不能发起支付。
退款与查单:先确认受理,再按错误码处理
微信退款最佳实践要求:调用申请退款 API 若应答不为 200 OK,需先调用查询单笔退款接口确认退款单是否受理,再按错误码处理。受理成功就按查询状态处理(退款单不是 CLOSED 即视为已受理);受理失败返回 RESOURCE_NOT_EXISTS(退款单不存在),用原单原参数重试。
错误码表:400 INVALID_REQUEST;401 SIGN_ERROR;403 NOT_ENOUGH(余额不足,充值后原单原参数重试)、USER_ACCOUNT_ABNORMAL;404 MCH_NOT_EXISTS、RESOURCE_NOT_EXISTS;429 FREQUENCY_LIMITED(受理中,调查单接口确认或降频原单重试,勿换单号);500 SYSTEM_ERROR(系统超时,使用原单原参数重试)。文档同时强调:应用程序不得依赖错误描述做自动化处理,错误描述可能因业务调整而变化。退款结果查询:未收到回调时推荐每 1 分钟查一次,超过 5 分钟仍是处理中则开始衰减(5/10/20/30 分钟……)。
查单兜底的规则是:无论前端返回「成功」还是「报错」,商户都要调用查单接口确认;未收到异步通知应主动调查单接口同步。判定支付成功的条件是收到查单响应、验签成功、且 trade_state == SUCCESS。轮询节奏以下单成功时间为基准(或以前端返回后第一次查单未成功的时间为基准),每隔 5 秒 / 30 秒 / 1 分钟 / 3 分钟 / 5 分钟 / 10 分钟 / 30 分钟查询。定时任务版本是每 30 秒启动一次,找最近 10 分钟内创建且未支付的订单查单,记录查询次数,10 次后仍未支付成功则停止查询并调关单接口关闭。若查单返回未支付,提醒用户勿重复发起支付;用户再次支付时要用原单号。
支付宝:四个接口,四套异常
统一背景是网络超时或未知异常(code=20000,sub_code=isp.unknow-error 或 ACQ.SYSTEM_ERROR / aop.ACQ.SYSTEM_ERROR)时结果未知,但不同接口的处理不同:
- 查询接口
alipay.trade.query和撤销接口alipay.trade.cancel调用异常:立即重试一分钟,仍超时/未知则记录异常交易并走人工处理。 - 预下单接口
alipay.trade.precreate调用异常:使用新的商户订单号out_trade_no重新调用预下单接口——这跟微信「保持原单号」的做法相反。 - 退款接口
alipay.trade.refund调用异常:使用相同参数重试一分钟,仍异常则记录并人工处理,不能简单推断退款成功或失败。 - 支付接口
alipay.trade.pay调用异常:立即调用查询接口;若查询到交易不存在(ACQ.TRADE_NOT_EXIST),使用相同参数重新调用支付接口;若网络超时或未知异常,继续查询一分钟,仍异常则记录并人工处理。
撤销接口的语义需要单独说清:只有在支付交易返回失败、支付系统超时或支付结果未知时才调用撤销——用户支付失败则关闭订单,用户支付成功则资金退还用户。正常支付的单要退款请用退款接口,不要用撤销。撤销前先调查询订单 API,没有明确结果再撤销。超过 24 小时的订单无法撤销。撤销接口返回 action 字段:close 表示交易未支付触发关闭,refund 表示交易已支付触发退款。
支付宝的 SYSTEM_ERROR 往往不是服务端抖动
一个反直觉的点:官方明确「接口报错 SYSTEM_ERROR,无论 sub_msg 描述为什么,都是因为参数有误导致」。常见原因是参数格式错误、biz_content 最后一个参数多了逗号、并发过高、缺少权限(比如花呗分期不支持刷脸付/周期扣款/IoT 小程序支付)。也就是说支付宝的 SYSTEM_ERROR 经常不是服务端抖动,盲目重试没有用。这和微信把 SYSTEM_ERROR 当作「请用相同参数重新调用」的处理方式,语义并不一致。
支付宝另外几条建议:未支付订单及时用撤销接口关闭(超 24 小时无法撤销);为每笔订单设超时自动关闭;不要在没有拿到交易结果时要求用户再次付款(用户可能已付成功,应先退款再让用户重付);建议轮询总时间 30 秒、间隔 3 秒;新建订单都要改订单号(out_trade_no 长度 1–64)。
落到代码里的判定顺序
请求前先把「待支付/处理中」订单落库,out_trade_no 作为幂等键。之后按四类结果分流:
type Result int
const (
NotSent Result = iota // 本地未发出:DNS 失败、连接建立失败,可安全重试
Unknown // 已发出后超时/连接重置/5xx:结果未知,走查询兜底
ChannelErr // 收到业务错误码:按码分类
Confirmed // 查询接口确认了最终状态
)
func resolve(ctx context.Context, order *Order) Result {
resp, err := createPayment(ctx, order) // 微信 /v3/pay/...;支付宝 alipay.trade.pay
switch {
case isDialError(err):
return retrySameOrder(ctx, order) // 原单原参数,指数退避+抖动
case isTimeoutOr5xx(err):
// 不判定失败,先以查询结果为准回填状态机
if st, ok := queryTrade(ctx, order.OutTradeNo); ok {
return fillState(order, st)
}
return Unknown // 标记异常单,进人工队列
case isBizCode(resp.Code):
if retryable(resp.Code) { // SYSTEM_ERROR / BANK_ERROR / FREQUENCY_LIMITED
return retrySameOrder(ctx, order)
}
return ChannelErr // 4xx 参数类不重试
}
return Confirmed
}
几条约束:只有可重试的错误码才用原单原参数重试,并限制次数;超过重试预算仍未知就标记异常单,走人工队列(官方热线/工单);再定时跑对账任务兜底,把「本地处理中但渠道已成功」的单捞回来。
还没想清楚的地方
微信 500 系列一律「用相同参数重新调用」,而支付宝的 ACQ.SYSTEM_ERROR 却是参数问题的信号——同一类错误码在两个渠道里,重试的价值完全相反。如果对账任务捞回一笔已经 SUCCESS 的单,而此时用户又在前端点了一次支付,前端和后端应该在哪一层拦截?还有一个更现实的:撤销接口有 24 小时窗口,微信侧关单也有自己的边界,跨天未支付的单子最后到底该关还是该留?
参考文档
- 微信支付 基本规则 错误信息:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
- 微信支付 跨境文档:https://pay.weixin.qq.com/doc/global/v3/zh/4012354970
- 微信支付 商户订单号规则:https://pay.weixin.qq.com/doc/v3/merchant/4012068676
- 微信支付 JSAPI 下单错误码表:https://pay.wechatpay.cn/doc/v3/merchant/4012525167
- 微信支付 退款最佳实践:https://pay.wechatpay.cn/doc/v3/merchant/4014959631
- 微信支付 支付回调和查单实现指引:https://pay.weixin.qq.com/docs/merchant/products/query-order/callback-and-query-order.html
- 支付宝 异常处理:https://opendocs.alipay.com/open/318/106386
- 支付宝 接入注意事项:https://opendoc.alipay.com/open/069hih
- 支付宝 ACQ.SYSTEM_ERROR 说明:https://opendocs.alipay.com/support/01raxs
- 支付宝 撤销接口:https://opendocs.alipay.com/mini/05xunj