做支付接入的人,第一课往往是被"退款接口返回 200 就以为钱退完了"坑一次。
申请退款接口返回 200,只是微信支付受理了你的退款单。它既不代表钱已经退到用户账户,也不代表这笔退款一定成功。真正的结果要看退款单状态机,以及微信主动推过来的退款结果回调。
这篇把退款链路里那些文档写了、但初次对接容易忽略的边界过一遍。都是踩过再整理出来的,能省你上线的第一轮对账事故。
先看状态机,别拿"受理"当"成功"
微信支付退款单只有这几个状态,全部要你在业务侧正确映射:
| 状态 | 含义 | 你的业务怎么处理 |
|---|---|---|
PROCESSING | 退款处理中,已受理未完成 | 标记"退款中",等回调/查单 |
SUCCESS | 退款成功,钱已原路退回 | 终态,释放退款单 |
CLOSED | 退款关闭(多为超时或不允许退) | 终态,按失败处理,可重新发起 |
ABNORMAL | 退款异常,原路退回失败(如银行卡冻结/作废) | 需人工介入或走异常退款接口 |
最容易写错的点:不要把申请退款接口的 200 当成 SUCCESS。受理成功到真正到账是有延时的——零钱支付的退款约 20 分钟内到账,银行卡支付可能要到 3 个工作日后才落账。业务状态里"退款成功"必须由 SUCCESS 触发,不是由 HTTP 200 触发。
out_refund_no 是幂等命门,也是资损元凶
申请退款接口是幂等的,幂等键就是商户退款单号 out_refund_no。
两条硬规则:
- out_refund_no 在商户号下全局唯一。同一个单号配不同订单号或不同金额,会直接报
INVALID_REQUEST / 支付单号校验不一致。也正因为唯一,重复提交同一个单号只退一笔,这是防重复退款的设计。 - 重试必须用原单原参数。网络超时、返回 5xx、
FREQUENCY_LIMITED时,都该用同一个 out_refund_no 和同一组参数重试,而不是新造一个单号——否则一旦第一次其实受理成功了,第二次就成了新退款,资金重复流出。
反直觉的点:申请退款接口具备幂等,但官方明确不建议拿它当查询接口用。确认退款单状态,请调查询单笔退款接口,别靠"再提交一次看返回"来判断。
金额是分,别用元;并发退款要加锁
- 所有金额字段单位是分,整数,不能带小数点。传错会报
REFUND_FEE_MISMATCH(金额与之前请求不一致)。 - 退款金额不能超过订单原支付金额。
- 部分退款最多 50 次。第 50 次之后要再退,得换 out_refund_no,并且间隔 1 分钟后再调。
- 并发场景是真坑:两个线程同时对同一订单发起部分退款,各自校验"我退的这单不超",但累加后就超了。微信侧会拦,但你的状态可能已经写脏。
正确做法是调用前在本地加分布式锁,并校验「已退款金额 + 本次退款金额 <= 订单总金额」。服务端能兜底,但你不该让脏状态先落地。
回调:要验签、要应答、要重入
退款结果变了,微信会 POST 到你配置的 notify_url(或商户平台退款配置里的地址)。注意:
- 申请退款时传的 notifyUrl,优先级高于商户平台配置的全局地址。
- 5 秒内完成验签并应答。验签通过回 HTTP 200/204(无需报文);验签失败回 4xx/5xx 带应答报文。不答或答错,微信会按
15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h的重试节奏,最多重发 15 次,累计约 24 小时。 - 同一通知可能重复到达。收到通知先查本地退款单状态,已处理过就直接回 200,别再执行一遍,否则资金/状态重复。处理前建议上数据锁防函数重入。
还有个小坑:微信偶尔会发 Wechatpay-Signature 以 WECHATPAY/SIGNTEST/ 开头的签名探测流量,用来验证你的验签逻辑。别把它当真回调,按验签失败处理即可。
主动查单兜底:别只靠回调
回调不是 100% 到达(微信自己也声明"不保证通知最终成功")。所以退款状态的最终一致性要靠主动查询兜底。
推荐节奏:回调没来时,每 1 分钟查一次查询单笔退款接口;超过 5 分钟仍是 PROCESSING,逐步衰减频率(5 分钟、10 分钟、20 分钟、30 分钟……)。别高频猛刷——退款接口失败时频率限制只有 6 QPS,成功时是 150 QPS,查单同样要克制。
错误码速查
| HTTP | 错误码 | 含义 | 处理 |
|---|---|---|---|
| 400 | INVALID_REQUEST | 参数符合格式但不符合业务规则 | 看具体 message |
| 401 | SIGN_ERROR | 签名错误 | 查签名算法/私钥/证书 |
| 403 | NOT_ENOUGH | 商户账户余额不足 | 充值后原单原参数重试 |
| 404 | RESOURCE_NOT_EXISTS | 订单不存在/未支付 | 确认单号与支付状态 |
| 429 | FREQUENCY_LIMITED | 频率受限或退款受理中 | 降频,原单重试,勿换单号 |
| 500 | SYSTEM_ERROR | 微信侧超时 | 原单原参数重试 |
注意官方一句提醒:错误描述可能因业务调整变更,别拿它做自动化判断。逻辑要锚定在错误码和状态字段上,message 只给人看。
ABNORMAL 退款异常:自动不了的才显真章
退款到银行发现用户卡被冻结/作废,原路退回失败,退款单进入 ABNORMAL。这时没有自动解法,两条路:
- 商户平台「交易中心」人工审核退款;
- 调发起异常退款接口,退到用户其他账户或退到商户自己的账户再线下转。
ABNORMAL 这块建议直接做成告警 + 人工队列,别试图全自动,涉及资金的事,留人审比留代码更稳。
一句话总结的判断
- 接口返回 200 = 受理,不是成功。
- 退款成功唯一标准是状态机
SUCCESS,靠回调 + 查单双通道确认。 - out_refund_no 是幂等键,重试永远原单原参数。
- 金额单位分、部分退款 50 次上限、并发要加锁校验累计金额。
- ABNORMAL 走人工/异常退款接口,别写死全自动。
(错误码、频率限制、重试节奏均对照微信支付官方退款接口/退款最佳实践文档整理,具体以你的商户类型——直连 or 服务商——对应的接口文档为准。文中未实测的部分为文档结论。)