小程序虚拟支付 paySig 验不过:signData 不能重新序列化,iOS 走的是 Apple IAP
这套支付和 JSAPI 不是一回事
普通微信支付:服务端调 /v3/pay/transactions/jsapi 拿 prepay_id,前端 wx.requestPayment 唤起。虚拟支付相反:服务端只产出签名参数(signData / paySig / signature),真正的下单动作发生在客户端调用 wx.requestVirtualPayment 时。基础库要求 2.19.2 及以上,低版本用 wx.canIUse('requestVirtualPayment') 判断。
链路上不存在「服务端下单成功、用户没付」这种可以查单补救的中间态——服务端手里只有自己生成的 outTradeNo。
设备路由(同一套 API,平台自动分流):
| 设备类型 | 对接支付系统 |
|---|---|
| Android、鸿蒙、Windows | 微信支付 |
| iOS | Apple 支付 |
注意:Apple 支付不支持沙箱环境,仅支持现网环境。
两套签名,两把不同的密钥
paySig = to_hex(hmac_sha256(appKey, uri + '&' + signData))
signature = to_hex(hmac_sha256(sessionKey, signData))
Go 实现:
func calcPaySig(uri, signData, appKey string) string {
mac := hmac.New(sha256.New, []byte(appKey))
mac.Write([]byte(uri + "&" + signData))
return hex.EncodeToString(mac.Sum(nil))
}
func calcSignature(signData, sessionKey string) string {
mac := hmac.New(sha256.New, []byte(sessionKey))
mac.Write([]byte(signData))
return hex.EncodeToString(mac.Sum(nil))
}
uri 取值分开看:C 端(wx.requestVirtualPayment)固定填 requestVirtualPayment,不是 URL,也不带 appid 和问号参数;服务端接口用实际路径,比如 /xpay/query_order。
坑一:signData 必须逐字节原样透传
服务端算 paySig 用的那串 JSON,和客户端最终交给基础库的字符串必须一致。任何 JSON.parse → JSON.stringify 的往返都会改键序、改空格、把数字写成别的形态,签名直接失效,报 -15006 支付签名 paySig 错误。
做法:服务端把 signData 当成字符串拼好,签完名原样放进返回体,前端拿到后直接展开给 wx.requestVirtualPayment,中间不解析、不重排、不 trim。
坑二:AppKey 分沙箱和现网
MP 后台 虚拟支付 → 基本配置 → 基础配置 里有两个 AppKey。env = 0 对应现网 AppKey,env = 1 对应沙箱 AppKey。用错了不会提示「key 不对」,只会得到一个 -15006。本地和线上环境变量混用是这类报错最常见的原因。
坑三:session_key 会被 wx.login 刷掉
signature 用的密钥是 session_key,通过 auth.code2Session 拿到。用户再次 wx.login 时 session_key 可能重新生成,服务端存的旧值立刻失效。表现出来就是同一个人时好时坏、-15005 用户态签名错误 或 -15007 session_key 过期。
登录态更新后要覆盖写入数据库,不要给 session_key 设一个想当然的长 TTL,也不要拿它当用户标识。
错误码按「改哪里」分诊
| 错误码 | 含义 | 先改什么 |
|---|---|---|
| -15001 | 参数错误,具体原因看 err_msg | 打印 err_msg,按字段核对 |
| -15002 | outTradeNo 重复使用 | 换新单号重试;单号 8-32 字符,不能以下划线开头 |
| -15005 | 用户态签名 signature 错误 | 检查 session_key 是否最新 |
| -15006 | 支付签名 paySig 错误 | 检查 AppKey 与 env 配套、signData 是否被重排 |
| -15007 | session_key 过期 | 重新 wx.login 换 code2Session |
| -15008 | 二级商户进件未完成 | MP 后台查进件状态 |
| -15010 | 道具 productId 未发布 | 道具管理里发布到现网 |
| -15011 | 现网版本的 env 只能是 0 | 体验版/正式版不能填 1 |
| -15013 | goodsPrice 道具价格错误 | 客户端传的价格要与后台道具价格一致,单位分 |
| -15014 | 道具/代币发布未生效 | 大约 10 分钟后再生效 |
| -15016 | signData 格式有问题 | 多半是传了对象而不是字符串 |
| -15017 / -15019 | 商家违规被限制收款 / 商户受限 | 商户平台看原因 |
| -15021 | 小程序被限频交易 | 降频,不要重试刷单 |
-15011 值得单独说:上传体验版之后还传 env = 1,必然失败。沙箱只用于开发版调试。
发货:推送为主,查单兜底,幂等键别用 outTradeNo
支付成功后平台推 xpay_goods_deliver_notify 到 MP 后台配置的消息推送地址。关键字段:
Event=xpay_goods_deliver_notifyOpenId:发给谁OutTradeNo:商家业务单号WeChatPayInfo.MchOrderNo:平台单号,跟踪与去重以它为准GoodsInfo.ProductId/Quantity/ActualPrice:发哪件、几个、实付多少(分)
应答格式必须和推送格式一致。XML 推来就回 XML:
0
JSON 推来就回 {"ErrCode":0,"ErrMsg":"success"}。也可以直接返回空或 success。格式不对,微信会重推,最多 15 次。
两个容易踩的点:
- 幂等键用平台单号
MchOrderNo,不要用outTradeNo。用户取消后重新下单,商家侧可能复用同一单号,按outTradeNo去重会把第二次的真单当成重复事件,直接漏发。 - 顺序是「查重 → 处理 → 落标记」。先落标记再处理,一旦处理中途失败,重推会被当成重复丢掉,等于永久丢单。
另外,前端 success 回调会丢——用户支付成功后直接杀掉微信就收不到。所以发货只认服务端推送,success 只用来刷新 UI。
兜底按 5 分钟一次调 POST /xpay/query_order 查未发货订单(带 pay_sig 签名,body 里 order_id 是商家单号)。这个接口只查现金单,代币余额走另一套接口。
推送长期打不通、订单卡在未发货时,可以用 POST /xpay/notify_provide_goods 手动把单置为已发货,它也只能处理现金单。
退款不是一次调用就完事
POST /xpay/refund_order 只是启动退款任务。启动成功后要持续调 /xpay/query_order 查退款单状态,变成退款完成才算结束;最终态也会通过 xpay_refund_notify 推过来。只调用一次就置本地为已退款,是常见的账实不符来源。
iOS 侧还有一条 xpay_subscribe_ios_refund_query_notify:Apple 发起退款问询时需要快速应答(云开发文档写的是 3 秒内),这条按平台要求实现,超时基本等于默认同意退款。
iOS 的四个前置条件
用户要同时满足:iPhone/iPad 且 iOS 15 及以上、微信客户端 8.0.68 及以上、单笔最低 1 元、App Store 账户在中国大陆。
因为 Apple 支付不支持沙箱,任何「用 env = 1 在 iOS 上跑通」的做法都不成立。真机联调的路径是 Android 走沙箱、iOS 走现网小额真单。
联调清单
- 关掉模拟器,支付必须真机
- 服务端把最终发出的
signData原串打进日志(别把session_key打出去),与签名输入比对一次 - 确认当前构建的
env,与使用的 AppKey 配套 - 确认道具已在后台发布到现网(不是只上传开发版)
- 发货接口先上线、先幂等,再放量;查单定时任务同一天上线
- 退款用轮询等终态,不要看接口 200 就当完成
口径与未实测说明
字段、错误码、签名算法、推送重试次数均取自微信开放文档(wx.requestVirtualPayment、虚拟支付接入、notify_provide_goods),代码片段是按文档伪代码改写的示意实现,未在生产环境逐条跑过真实支付;标注的多条 iOS 前置条件与「3 秒应答」来自文档与云开发集成指南的描述,iOS 侧行为随微信版本变动,上线前请以当时文档为准。
参考:
- wx.requestVirtualPayment:https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestVirtualPayment.html
- 虚拟支付(企业/个体户):https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
- 通知已发货完成:https://developers.weixin.qq.com/miniprogram/dev/server/API/VirtualPayment/api_notify_provide_goods.html
- 云开发虚拟支付集成指南:https://docs.cloudbase.net/integration/wechat-virtual-pay