微信虚拟支付踩坑 8 天,这 6 个坑帮你全绕过去
接一个微信虚拟支付,踩了 6 个坑,折腾了 8 天。不是文档没写——是文档分散在微信开放平台、米大师独立文档站、开放社区、英文版四个地方,关键信息藏在不同页面的角落里。这篇把碎片拼成完整地图。
一句话结论
微信虚拟支付有两条并行的技术栈(xpay 2.0 和米大师 1.0),签名有两套公式、回调不走 HTTP、退款需要双签名、pay_sig 不存就退不了款。
一、两套支付体系,别混了
微信开放文档里有两个虚拟支付入口:虚拟支付 2.0(xpay) 和 米大师支付(midas)。不是新旧版本迭代,是不同的产品。
| 对比项 | xpay 2.0 | 米大师 1.0 |
|---|---|---|
| 负责团队 | 小程序团队 | 游戏团队 |
| 域名 | api.weixin.qq.com | api.midas.qq.com |
| 适用场景 | 会员、课程、虚拟工具 | 游戏、金币系统 |
| 客户端签名 | "requestVirtualPayment&" + JSON | 不同拼接规则 |
| 服务端签名 | uri + "&" + JSON | 额外包含 url_path |
| 回调方式 | 消息推送(免配公网) | HTTP 回调(需配公网地址) |
最常见的翻车:在 xpay 后台配密钥,却用米大师的 API 文档写服务端——签名永久报错,且看不到任何额外错误信息。
二、同一个 HMAC-SHA256,两套输入
客户端签名和服务端签名用同一个 HMAC-SHA256、同一个 appKey,但输入结构不同:
客户端:sign = HMAC-SHA256(appKey, "requestVirtualPayment" + "&" + JSON.stringify(signDataObj))
服务端:sign = HMAC-SHA256(appKey, uri + "&" + JSON.stringify(body))
客户端签名第一个参数是固定字符串 "requestVirtualPayment",服务端是请求路径 uri。同一笔订单算出来的结果完全不同。
签名错误 40066 不是 bug,是认知盲区。 建议:每写一个 HMAC-SHA256,代码上方加三行注释标注用途和公式。
三、沙箱和生产:同一个域名,两套密钥
xpay 2.0 的沙箱和生产用同一个域名 api.weixin.qq.com,区别在请求体的 env 字段:沙箱 env=1,生产 env=0。
// sandbox
app_key: process.env.WX_SANDBOX_APP_KEY
env: 1
// production
app_key: process.env.WX_PROD_APP_KEY
env: 0
切 env 时必须切 key,否则微信用沙箱密钥去验签你拿生产密钥算出的值——40066。
四、支付成功不代表回调到了
xpay 的回调不走 HTTP 回调地址,走的是消息推送——微信服务器内部通道,直接推送到小程序消息队列。不需要配公网回调地址,不需要验签。
注意:回调事件名和文档可能不一致——文档写的 xPayGoodsDelivery,实际推送可能是 xiaochengxu_pay_callback。以实际收到的推送事件名为准。消息推送触发器漏了开启,回调就永远收不到。
五、退款连着报错:一个操作,三层同时错
下单走 xpay,退款走米大师通用 API——两套路径、两套字段名、两套签名规则:
路径 → /api/common/refund(不是 /xpay/refund_order)
金额字段 → amt(不是 refund_fee),还要传 ts 和 action
签名 → Header 有额外校验字段,少传一个就过不了
三个维度同时出错时,报错信息看不出是哪一层没对齐。只能逐层试——修一层,发一次,看返回,再修下一层。
六、忘了存 pay_sig?退款全线报废
这是代价最大的坑。 测试环境退款全通,上线后全部失败。
支付下单时微信返回的响应里有一个 pay_sig 字段。当时以为它是下单阶段的中间变量,用完就扔了——数据库表里根本没存。
退款需要两个签名:
- Sign:自己用 appKey 算出来的
- pay_sig:支付时微信返回的,必须原样传回
两个签名缺一不可。已支付订单的 pay_sig 永久丢失,无法补救。
结论:支付回调里必须把 pay_sig 存入数据库(TEXT/VARCHAR(1024))。
上线前检查清单
云函数配置:
- timeout 设 15-20 秒(米大师 API 偶有延迟)
- 消息推送触发器手动开启(漏了 = 回调永远收不到)
- 上传选「云端安装依赖」
密钥管理:
- app_key 走 process.env,沙箱和生产各一套
- pay_sig 存入数据库
- 切生产时:改 body.env 的同时必须切换 app_key
定时兜底:
- cron 每 5 分钟扫已支付未发货超 2 分钟的订单,主动查单补发
- 退款中超 30 秒的主动查结果
沙箱验证:跑通下单 → 支付 → 回调 → 发货 → 退款全闭环。
审核准备:msgSecCheck 覆盖所有 UGC 入口 + 未成年人退款全流程 + 隐私政策 + 企业主体 + 商品已发布。
六坑汇总
| 坑 | 后果 | 耗时 |
|---|---|---|
| pay_sig 未存储 | 退款全废,已有订单无法补救 | 约3天 |
| 退款路径/字段/签名全错 | 退款 API 连续报错 | 约2天 |
| 回调事件名与文档不一致 | 回调不触发 | 半天 |
| 沙箱/生产 app_key 未分开 | 签名错误 40066 | 半天 |
| 退款双签名(Sign + pay_sig) | 退款失败 | 约1天 |
| xpay vs 米大师 API 混淆 | 签名报错 | 约1天 |
核心观点:坑不在文档内容上,在文档结构上。四个独立的文档站各自写了同一个支付系统的一部分,没有人把它们拼在一起。
接入路线(按顺序做,别跳步)
确认类目 → 开通虚拟支付获取 app_key → 沙箱跑通全链路 → 开启消息推送触发器 → 接入 msgSecCheck → 加 cron 兜底 → 切生产 → 提交审核。