微信支付退款 ABNORMAL:钱已从基本户扣出,停在退款中间账户
退款单状态为 ABNORMAL 时,钱其实已经从商户基本户扣出去了,只是停在退款中间账户,既没到用户手里,也没回到商户账上。它不是“退款失败”的同义词,而是一个需要继续处理的中间状态。把它当失败收尾,账上就会一直挂着一笔钱。
退款单共有四个状态:SUCCESS 退款成功、CLOSED 退款关闭、PROCESSING 退款处理中、ABNORMAL 退款异常。仅仅因为口语里都说“退款异常”,这三种情况在实际处理时完全不同。
三种“退款异常”,处理方式完全不同
申请阶段 403 USER_ACCOUNT_ABNORMAL:没扣钱
调用申请退款接口时返回 403 USER_ACCOUNT_ABNORMAL(用户账号注销),表示退款申请没有被受理。此时钱没有从基本户扣出,也没有产生 ABNORMAL 状态的退款单。它属于请求失败,不是退款单异常。
申请退款接口的常见错误码:
400 INVALID_REQUEST401 SIGN_ERROR403 NOT_ENOUGH(余额不足,充值后用原单原参数重试)403 USER_ACCOUNT_ABNORMAL404 MCH_NOT_EXISTS404 RESOURCE_NOT_EXISTS429 FREQUENCY_LIMITED500 SYSTEM_ERROR
429 表示请求已经受理中。应先查单,然后降低请求频率,并用原单号重试,不要更换单号。同一个 out_refund_no 多次请求只会退一笔;申请失败重试必须使用原商户退款单号。一笔订单最多支持 50 次部分退款,多次部分退款必须更换单号,并且间隔 1 分钟。
CLOSED:7 天 + 余额不足,终态
退款进入 PROCESSING 后,如果受理超过 7 天且出资账户余额不足,状态会流转到 CLOSED。这是终态,原单号无法继续使用,重新发起需要补足资金并生成新的商户退款单号。
另外,支付成功后 365 天内可以退款;amount.total 填的是原订单金额,不是本次退款金额。
ABNORMAL:原路退卡失败,零钱也注销了
PROCESSING 期间,如果用户账户异常(银行卡作废或冻结)导致无法原路退回,微信支付会优先把退款金额转入用户的微信零钱。只有当用户的微信零钱账户也已注销时,状态才会流转为 ABNORMAL。此时资金停留在退款中间账户,商户可以在商户平台手动处理,也可以调用发起异常退款接口,成功后流转为 SUCCESS。
状态流转与终态判断
申请退款受理成功 → PROCESSING。从 PROCESSING 出发有两个分支:超过 7 天且余额不足 → CLOSED;原路退卡失败但零钱可用 → 退零钱成功;原路退卡失败且零钱也注销 → ABNORMAL。ABNORMAL 通过异常退款接口处理后 → SUCCESS。
终态只有 SUCCESS 和 CLOSED 两个。所以 ABNORMAL 必须被处理,不能当成失败收尾;它没有被处理完,退款这件事就还没有结束。
发起异常退款接口
两个路径,按商户形态选择:
- 普通商户:
POST https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund - 平台商户 / 收付通:
POST https://api.mch.weixin.qq.com/v3/ecommerce/refunds/{refund_id}/apply-abnormal-refund(body 需带sub_mchid)
path 参数 refund_id 是微信支付退款单号,也就是申请退款受理成功时返回的那个单号,不是商户自己的单号。
body 关键字段:
out_refund_no必填,传商户申请退款时传入的原退款单号。type必填,枚举USER_BANK_CARD/MERCHANT_BANK_CARD。bank_type:退款至用户时必填,仅支持 15 家直联银行借记卡——招行、交通、农行、建行、工商、中行、平安、浦发、中信、光大、民生、兴业、广发、邮储、宁波。bank_account与real_name:需用微信支付公钥加密,Wechatpay-Serial头传公钥 ID,也支持平台证书公钥。
接口请求频率 150qps。个人收款只支持退款至用户银行账户。
应答里的 channel 有四个取值:ORIGINAL 原路退款、BALANCE 退回余额、OTHER_BALANCE 原账户异常退到其他余额账户、OTHER_BANKCARD 原银行卡异常退到其他银行卡(发起异常退款成功后返回)。
Go 侧状态分流(示意,未实测)
// 示意代码:仅表达状态分流逻辑,未实测
switch refund.Status {
case "SUCCESS":
// 终态,收尾
case "CLOSED":
// 终态,且不可复用原单号:补足资金后生成新的 out_refund_no 重新发起
case "PROCESSING":
// 非终态:查单,1 分钟后一次,超过 5 分钟仍 PROCESSING 按 5/10/20/30 分钟衰减
case "ABNORMAL":
// 非终态,必须处理:资金停在退款中间账户
body := map[string]any{
"out_refund_no": refund.OutRefundNo, // 必须是原商户退款单号
"type": "MERCHANT_BANK_CARD", // 或 USER_BANK_CARD
}
// POST /v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund
// refund_id 为微信支付退款单号
}
产品取舍:退到结算户,对账口径就变了
type=MERCHANT_BANK_CARD 的效果是把钱退回商户结算银行账户,再由商户线下退给用户;USER_BANK_CARD 则是退到用户指定的银行卡。前者绕开了用户零钱账户已注销的死结,但代价是这笔钱在对账上不再走“退款给用户”的路径,而是回到自己的结算户,退款口径和流水匹配方式都要跟着调整。
供应商侧还有一个开关:商户号开启「异常退款默认退商户」功能后,退款异常时系统自动退回商户结算银行账户,商户不需要再手动处理或调接口。
开关之外,还需要注意的几件事
- 分账订单退款不会自动回退已分账金额,出资为商户基本账户可用余额。
- 合单支付只能用子单退款。
- 退款回调地址至少配置一处:申请时传
notify_url,或在商户平台交易中心-退款配置。接口传入后平台配置不再生效。 - 未收到回调时可查单:建议 1 分钟后查一次,超过 5 分钟仍为
PROCESSING,按 5/10/20/30 分钟衰减。
有一个点需要自行验证:部分渠道下 channel 的实际取值要以接口应答为准,文档枚举与实际返回之间存在差异的可能,接入时最好把应答原样落库再判断。