代码 支付超时之后:微信用原单号重试,支付宝预下单却要换新单号

2026-09-22 09:01:35

支付超时之后:微信用原单号重试,支付宝预下单却要换新单号

调用支付接口时网络超时,或者日志里躺着一条 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/currencylocation 取值 body/url/query。公共错误码包括 PARAM_ERRORINVALID_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_TRANSACTIONIDOPENID_MISMATCHSYSTEM_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_EXISTSRESOURCE_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

推荐文章

程序员茄子在线接单