微信小程序虚拟支付(个人主体)接入:paySig 与 signature 别搞混,发货别信前端 success
个人主体小程序能开虚拟支付,前提是类目和资质都对得上,这是少见的对个人开发者友好但坑很集中的支付通道。下面内容整理自微信开放文档,标注「官方口径」的部分是文档明确写的,标注「未实测」的是我按文档推导、但没在线上跑过的部分。
参考地址:
- 虚拟支付(个人)接入说明:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/person.html
- wx.requestVirtualPayment API: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/
一、开通条件与限额(官方口径)
- 主体为个人,持有居民身份证;
- 服务类目含「工具」;
- 小程序已完成认证、备案;
- 个人主体月支付限额 10 万元;
- 不支持需要电信业务资质的服务:邮件、语音信箱、存储转发、信息发布平台、信息搜索查询等。
二、费率(官方口径)
- Android 等终端:1%,腾讯技术服务费;
- iOS:12%,Apple 佣金。
三、开通后必须拿到的东西
流程:微信开放平台 → 支付与交易-虚拟支付 → 开通 → 填资料 → 审核(官方说约 5 分钟)→ 扫码签约。
签约完成后收集四个值:
| 信息 | 位置 |
|---|---|
| AppID | MP 后台 - 设置 |
| OfferID | 虚拟支付 - 基本配置(支付账号) |
| 现网 AppKey | 虚拟支付 - 基本配置(支付密钥) |
| productId / goodsPrice | 【道具管理】创建道具后记录 |
道具创建完要发布,价格要和后端下单用的 goodsPrice 一致。
iOS 支付额外两步:先在 MP 平台「账号设置-基本信息」配置小程序简称(Apple 支付的 display name),再到「虚拟支付-基本配置」开通苹果 IAP 支付。iOS 端还要求微信客户端 8.0.68 及以上,调用前需要做版本校验。
四、整体链路
- 前端请求自家服务器下单,服务器生成唯一
outTradeNo,落库为「待支付」; - 服务器返回
payData,前端调wx.requestVirtualPayment拉起支付; - 支付成功后发货:路径 A 收平台「发货推送」直接发货;路径 B 推送丢失时定时
query_order补发; - 前端查自家服务器订单状态,展示购买成功并开放权益。
outTradeNo 由商家自生成、必填、必须唯一;wx_order_id 是平台单号,跟踪订单、发货、对账都以它为准。
wx.requestVirtualPayment 的 payData 核心字段:
wx.requestVirtualPayment({
signData: '...', // 支付参数拼成的 JSON 字符串
offerId: '...',
buyQuantity: 1,
env: 0, // 固定 0,正式/现网
currencyType: 'CNY',
productId: '...',
goodsPrice: 100, // 道具单价,单位「分」,需与后台一致
outTradeNo: '...',
attach: '...', // 透传数据,必填
mode: 'short_series_goods',
success(res) { /* 支付成功。可能丢失,不能作为唯一发货依据 */ },
fail(err) { /* ... */ }
})
金额全程以「分」为单位,不要中途换算成元。env 固定填 0;env=1 是沙箱,对应沙箱 AppKey。mode 固定 short_series_goods(道具直购)。
五、坑一:paySig 和 signature 是两套签名
这两个名字长得像,密钥完全不同,用途也完全不同——混用最典型的症状是「本地自测通过,真机下单 400 / 验签失败」。
| paySig | signature | |
|---|---|---|
| 密钥 | AppKey | sessionKey |
| 用途 | 支付签名(C 端下单 + B 端接口) | 用户态签名 |
| 位置 | 服务端计算 | 服务端计算 |
paySig 计算规则(官方口径):
msg = uri + '&' + post_body
paySig = HMAC-SHA256(appkey, msg)
两个细节最容易翻车:
- uri 规则。C 端(
wx.requestVirtualPayment下单)时,uri 固定为字符串requestVirtualPayment;B 端调服务端接口(/xpay/*)时,uri 是实际接口路径,例如/xpay/query_order。注意不是带?和查询参数的那种 URL。 - post_body 必须是真正发出去的那份原始字符串。不重新格式化、不重排键顺序、不加空格。只要你序列化了两次、或者中间过了一层会规范化 JSON 的框架,签名就会对不上。稳妥做法是把最终 body 字符串先定下来,签名和
requests.post(data=body)都用同一个变量。
signature 计算规则(官方口径):
signature = HMAC-SHA256(session_key, post_body)
这里的 post_body 就是 signData 那个 JSON 字符串。sessionKey 通过 auth.code2Session 获取。
Python 版本,两个函数放一起:
import hashlib
import hmac
def make_pay_sig(appkey: str, uri: str, post_body: str) -> str:
"""
paySig:服务端计算,密钥是 AppKey
- C 端下单:uri 固定为 "requestVirtualPayment"
- B 端接口:uri 为实际路径,如 "/xpay/query_order"
- post_body 必须与实际发出的原始字符串完全一致
"""
msg = f"{uri}&{post_body}"
return hmac.new(
appkey.encode("utf-8"),
msg.encode("utf-8"),
hashlib.sha256,
).hexdigest()
def make_signature(session_key: str, sign_data_json: str) -> str:
"""
signature:服务端计算,密钥是 sessionKey
sign_data_json 即 payData 里的 signData 字符串
"""
return hmac.new(
session_key.encode("utf-8"),
sign_data_json.encode("utf-8"),
hashlib.sha256,
).hexdigest()
AppKey 用正式现网的那把,和 env=0 对应。
六、坑二:发货依据是推送 + 查单,不是前端 success
前端 success 回调会丢——用户杀进程、切后台、网络抖动都可能让回调不执行。拿它当发货依据,等于把订单状态交给客户端决定。
平台在支付成功后推送 XML 到你在「开发与服务-开发管理-消息推送」配置的 URL。推送字段(官方口径):
Event:固定xpay_goods_deliver_notifyOpenIdOutTradeNoWeChatPayInfo.MchOrderNo:平台单号wx_order_id,幂等去重以此为准GoodsInfo.ProductIdGoodsInfo.Quantity
处理顺序:
- 解析 XML;
- 取
wx_order_id; - 幂等判断:该
wx_order_id已发过货就直接返回成功,不重复发; - 按
OpenId+ 道具 ID 发货; - 返回:
0
返回非 0,平台会重试,最多 15 次。幂等这一步不能省:重试是平台行为,你无法假定只推一次。
兜底查单:推送丢失时主动查 POST /xpay/query_order(带 pay_sig 签名),请求体:
{"openid": "...", "env": 0, "order_id": "业务订单号 outTradeNo"}
官方建议每 5 分钟查一次。这条兜底路径要能覆盖「用户已完成支付但推送始终没到」的情况,也就是用 outTradeNo 反查平台状态,确认已支付就补发。
七、服务端接口清单
POST /pay/order:生成outTradeNo+signData+paySig;POST /pay/notify:接收发货推送;POST /pay/query:转发query_order。
八、退款、结算与对账(官方口径)
退款:Android 等终端由开发者主动发起,走 MP 后台「虚拟支付-交易订单」,或调用 refund_order 接口,完成后会收到 xpay_refund_notify 推送;iOS 由用户向 App Store 申请,开发者无法主动退款。手续费退还规则:支付 180 天以内的退款,平台退还手续费;超过 180 天不退还。
结算:Android T+3;iOS 约 45–60 天。提现在 MP 后台【虚拟支付】查看余额、每日账单并发起。发票在次月 5 号之后申请上月的腾讯技术服务费发票。
订单查询:MP 后台【虚拟支付-交易订单】按渠道切换「普通支付 / Apple 支付」,接口侧用 query_order。
九、上线前检查清单
- 开放条件确认:个人主体、工具类目、认证与备案;
- MP 后台虚拟支付已开通;
- AppID / OfferID / 现网 AppKey 已拿到;
- iOS 支付配置了小程序简称;
- 消息推送接收端已部署;
- MP 后台已配置发货推送 URL,并跑通一笔测试支付;
- 服务器验签逻辑通过;
- 发货推送已做幂等,按
wx_order_id去重; - 前端
wx.requestVirtualPayment已打通,含 iOS 8.0.68+ 版本校验; - 兜底查单
query_order就绪; - 退款、结算、费率已对业务方说明;
- 上线后用小额真单验证「支付 → 推送 → 发货 → 账单金额一致」。
最后一条属于操作建议,不是文档原文。整套链路里,签名对不上和漏发/重发是两个最耗时间的故障方向,把 paySig 的 post_body 单点收口、把发货入口统一到 wx_order_id 幂等,基本就绕过了大部分返工。