代码 微信小程序虚拟支付(个人主体)接入:paySig 与 signature 别搞混,发货别信前端 success

2026-10-11 09:01:10

微信小程序虚拟支付(个人主体)接入: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 分钟)→ 扫码签约。

签约完成后收集四个值:

信息位置
AppIDMP 后台 - 设置
OfferID虚拟支付 - 基本配置(支付账号)
现网 AppKey虚拟支付 - 基本配置(支付密钥)
productId / goodsPrice【道具管理】创建道具后记录

道具创建完要发布,价格要和后端下单用的 goodsPrice 一致。

iOS 支付额外两步:先在 MP 平台「账号设置-基本信息」配置小程序简称(Apple 支付的 display name),再到「虚拟支付-基本配置」开通苹果 IAP 支付。iOS 端还要求微信客户端 8.0.68 及以上,调用前需要做版本校验。

四、整体链路

  1. 前端请求自家服务器下单,服务器生成唯一 outTradeNo,落库为「待支付」;
  2. 服务器返回 payData,前端调 wx.requestVirtualPayment 拉起支付;
  3. 支付成功后发货:路径 A 收平台「发货推送」直接发货;路径 B 推送丢失时定时 query_order 补发;
  4. 前端查自家服务器订单状态,展示购买成功并开放权益。

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 / 验签失败」。

paySigsignature
密钥AppKeysessionKey
用途支付签名(C 端下单 + B 端接口)用户态签名
位置服务端计算服务端计算

paySig 计算规则(官方口径):

msg = uri + '&' + post_body
paySig = HMAC-SHA256(appkey, msg)

两个细节最容易翻车:

  1. uri 规则。C 端(wx.requestVirtualPayment 下单)时,uri 固定为字符串 requestVirtualPayment;B 端调服务端接口(/xpay/*)时,uri 是实际接口路径,例如 /xpay/query_order。注意不是带 ? 和查询参数的那种 URL。
  2. 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_notify
  • OpenId
  • OutTradeNo
  • WeChatPayInfo.MchOrderNo:平台单号 wx_order_id,幂等去重以此为准
  • GoodsInfo.ProductId
  • GoodsInfo.Quantity

处理顺序:

  1. 解析 XML;
  2. 取 wx_order_id;
  3. 幂等判断:该 wx_order_id 已发过货就直接返回成功,不重复发;
  4. 按 OpenId + 道具 ID 发货;
  5. 返回:

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 幂等,基本就绕过了大部分返工。

推荐文章

程序员茄子在线接单