代码 小程序虚拟支付 paySig 验不过:signData 不能重新序列化,iOS 走的是 Apple IAP

2026-11-06 09:01:25

小程序虚拟支付 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微信支付
iOSApple 支付

注意: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,按字段核对
-15002outTradeNo 重复使用换新单号重试;单号 8-32 字符,不能以下划线开头
-15005用户态签名 signature 错误检查 session_key 是否最新
-15006支付签名 paySig 错误检查 AppKey 与 env 配套、signData 是否被重排
-15007session_key 过期重新 wx.login 换 code2Session
-15008二级商户进件未完成MP 后台查进件状态
-15010道具 productId 未发布道具管理里发布到现网
-15011现网版本的 env 只能是 0体验版/正式版不能填 1
-15013goodsPrice 道具价格错误客户端传的价格要与后台道具价格一致,单位分
-15014道具/代币发布未生效大约 10 分钟后再生效
-15016signData 格式有问题多半是传了对象而不是字符串
-15017 / -15019商家违规被限制收款 / 商户受限商户平台看原因
-15021小程序被限频交易降频,不要重试刷单

-15011 值得单独说:上传体验版之后还传 env = 1,必然失败。沙箱只用于开发版调试。

发货:推送为主,查单兜底,幂等键别用 outTradeNo

支付成功后平台推 xpay_goods_deliver_notify 到 MP 后台配置的消息推送地址。关键字段:

  • Event = xpay_goods_deliver_notify
  • OpenId:发给谁
  • OutTradeNo:商家业务单号
  • WeChatPayInfo.MchOrderNo:平台单号,跟踪与去重以它为准
  • GoodsInfo.ProductId / Quantity / ActualPrice:发哪件、几个、实付多少(分)

应答格式必须和推送格式一致。XML 推来就回 XML:

0

JSON 推来就回 {"ErrCode":0,"ErrMsg":"success"}。也可以直接返回空或 success。格式不对,微信会重推,最多 15 次。

两个容易踩的点:

  1. 幂等键用平台单号 MchOrderNo,不要用 outTradeNo。用户取消后重新下单,商家侧可能复用同一单号,按 outTradeNo 去重会把第二次的真单当成重复事件,直接漏发。
  2. 顺序是「查重 → 处理 → 落标记」。先落标记再处理,一旦处理中途失败,重推会被当成重复丢掉,等于永久丢单。

另外,前端 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

推荐文章

程序员茄子在线接单