代码 微信支付 V3 退款:接口 200 只是受理,别拿它直接置订单已退款

2026-09-05 09:02:53

微信支付 V3 退款:接口 200 只是受理,别拿它直接置订单已退款

做支付接入的人容易在第一版退款逻辑上踩同一个坑:把「申请退款接口返回成功」当成「这笔钱已经退回去了」。于是用户来问「我退款怎么还没到账」时一脸懵,甚至有人按 200 就置了订单已退款状态,结果钱还在微信侧中间账户里挂着。

微信支付的退款是一条典型的异步链路:接口 200 只代表受理,不代表到账,也不代表退款成功。下面按官方文档梳理这条链路的接口、状态机、幂等规则和容易出问题的点。

先记住:200 只表示受理,最终结果要靠回调或查单

POST /v3/refund/domestic/refunds 返回 200,只表示微信支付受理了你的退款单,钱从商户基本账户扣走、进到退款中间账户。这笔钱最终退回给用户、还是卡在中间账户变异常,都要靠下面两种方式之一拿最终结果:

  • 退款结果通知(异步回调)
  • 查询单笔退款(主动查单兜底)

很多业务逻辑「申请接口一返回就置成功」,问题就在没立起这个异步前提。官方原话:申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果。

退款单不是两个状态,是四个状态

状态含义是否终态
PROCESSING退款处理中否,继续等/轮询
SUCCESS退款成功(平台侧已完成,不等于用户已到账)
CLOSED退款关闭是(想再退需换新单号重新申请)
ABNORMAL退款异常,原路退回失败是(需手动/调异常退款接口处理)

几个容易想当然的流转点:

  • PROCESSING 不一定走向 SUCCESS。退款单处理中超过 7 天、且出资账户余额不足时,会流转为 CLOSED。处理方式是充够余额、生成新的商户退款单号再重新申请一次。
  • ABNORMAL 只在用户账户彻底没救时出现。原路退款银行卡失败(卡作废/冻结)时,微信会先尝试把退款转入用户微信零钱;只有当零钱账户也已注销、实在无处可退,才流转为 ABNORMAL。此时钱停在中间账户,要么去商户平台-交易中心手动处理,要么调「发起异常退款」接口退到用户指定银行卡或退回商户结算账户。
  • 平台 SUCCESS 不等于用户到账。微信侧状态成功,只说明退款从中间账户转出去了;用户实际收到还有渠道延迟:零钱支付的订单一般 5 分钟内到账,银行卡支付的订单一般 1-3 个工作日到账。给用户的提示文案、客服口径都要按渠道区分,别拿 success_time 跟用户说「已经到账了」。

幂等规则:out_refund_no 是唯一键

退款接口的幂等靠商户退款单号 out_refund_no。规则:同一退款单号多次请求,只退一笔。由此推出两个操作纪律:

  1. 申请退款失败后重试,不要换单号,用原单原参数重试。这是安全且幂等的,不会发起重复退款。官方在 SYSTEM_ERROR(系统超时)、BIZERR_NEED_RETRYFREQUENCY_LIMITED 等错误上反复强调「请不要更换商户退款单号,使用相同参数再次调用」。报错后先查单确认是否已受理,而不是闷头重发。如果查单返回 RESOURCE_NOT_EXISTS(退款单不存在),说明确实没受理成功,此时用原单号原参数重试也是安全的。
  2. 多次部分退款必须换单号。一笔订单最多支持 50 次部分退款,每次部分退款要换新的 out_refund_no,且同一笔订单多次退款请求需相隔 1 分钟。

三个接口与轮询节奏

申请退款

POST /v3/refund/domestic/refunds,核心参数:

  • out_trade_no / transaction_id:原支付订单,二选一
  • out_refund_no:商户退款单号(幂等键)
  • amount{ refund, total, currency },单位都是分
  • notify_url:退款结果回调地址,传了就覆盖商户平台-交易中心-退款管理里配的地址;选一个地方配即可,别两处都填了还奇怪为什么回调不进来

其他边界:一笔退款失败后若要重新提交,别换单号;申请退款总金额不能超过订单金额;下单超过一年的订单无法退款。

查询单笔退款

GET /v3/refund/domestic/refunds/{out_refund_no},补单核心。返回值得看字段:statussuccess_time(仅 SUCCESS 返回)、channelORIGINAL 原路退款 / BALANCE 退回余额 / OTHER_BANKCARD 异常退到他卡)、user_received_account

轮询节奏官方建议:提交退款申请后,每间隔 1 分钟查一次;若超过 5 分钟仍是 PROCESSING,开始逐步衰减频率(比如之后 5、10、20、30 分钟……查一次)。查询接口同一商户号限 300QPS,撞 FREQUENCY_LIMITED 就间隔 1 分钟再查。

退款结果通知

退款状态变更(SUCCESS / CLOSED / ABNORMAL)后,微信向 notify_url POST 一条 JSON,结构和支付通知一致:外层 event_type 区分事件(REFUND.SUCCESS / REFUND.CLOSED / REFUND.ABNORMAL),业务数据在 resource.ciphertext 里,需要用 APIv3 密钥按 AES-256-GCM 解密。解密后核心字段:

mchid, transaction_id, out_trade_no, refund_id, out_refund_no, refund_status, success_time, user_received_account, amount{total, refund, payer_total, payer_refund}

处理回调按支付同一套纪律:

  • 验签:验 Wechatpay-Signature 等头部,确认来源。
  • 幂等:按 out_refund_no 查自己库,已处理直接应答成功,加数据锁防并发重入。
  • 应答:验签通过、入库后尽快回 200/204,5 秒内;超时或非 2xx,微信按 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h 重发,最多 15 次。
  • 通知不是最终依据:回调链路可能整个丢,一段时间没收到要主动查单对齐。官方明确「商户系统不能仅依赖回调结果,需结合查询接口使用」。

建议的状态处理分支

无论回调事件还是查单结果,最后归一到一个按 status 分发的函数(查单的 status 与回调的 refund_status 字段名不同、含义一致):

  • SUCCESS → 置退款成功(终态),触发财务/对账/通知用户。到账时间按渠道口径提示。
  • CLOSED → 置退款关闭(终态)。如需继续退款,换新单号重新申请。
  • PROCESSING → 非终态,只落中间态,等待轮询/回调,不产生用户可见的「已退款」。
  • ABNORMAL → 置异常,标记需人工/异常退款接口介入,别自动重发。

最常见的两种误用:把 PROCESSING 当最终结果置成功;ABNORMAL 直接置失败却不去处理中间账户停的那笔钱。这两类最后对账都对不上——前者多发「已退款」,后者钱退了但系统里没有任何单能对上。

与支付宝异步模型的差异

支付宝退款默认同步返回 + 可配异步通知,靠 fund_change=Y 和退款状态判断;微信把「受理」和「结果」分得更彻底,状态机更明确。二者在 TRADE_CLOSED 语义、退款超时订单判断上有差别(见站内《支付宝回调里 TRADE_CLOSED 别再一刀切》)。跨渠道做退款,别把一套逻辑原样搬,先把各自幂等键、状态枚举、终态定义对一遍。

小结

  1. 申请接口 200 = 受理,不是退款成功,别在 200 就置终态。
  2. 最终结果只能来自退款结果通知 + 查单;二者要做幂等,并保留查单兜底。
  3. out_refund_no 是幂等键:失败重试不换号;部分退款换新号。
  4. 平台 SUCCESS 不等于用户已到账:银行卡到账 1-3 个工作日,提示文案分开。

未实测说明与官方文档

说明:字段与状态流转依据微信支付官方文档(V3 商户版),未做线上实测;接口枚举和频率限制以接入商户类型(普通商户/服务商)的文档为准。上线前建议用小额真实订单在测试环境完整走一遍回调与查单两条路径。

官方文档:

复制全文 生成海报 微信支付 退款 回调 APIv3 接口对接 异步

推荐文章

程序员茄子在线接单