微信小程序虚拟支付接入全指南:从配置到上线的完整流程
2025年起,微信小程序对短剧、会员、道具等虚拟商品类目强制要求使用虚拟支付(wx.requestVirtualPayment),替代原有的 wx.requestPayment。本文以实际项目为例,完整记录从配置到上线的全流程。
一、开通虚拟支付
前提条件
小程序类目属于:短剧、小说、音乐、会员、虚拟道具等。在 MP 后台(mp.weixin.qq.com)→ 虚拟支付 → 完成签约。
基本配置
在 MP 后台「虚拟支付 → 基本配置」中获取:
| 配置项 | 说明 |
|---|---|
| offerId | 虚拟支付商户号 |
| appKey | 签名密钥,分沙箱和现网两个 |
创建道具
在「虚拟支付 → 道具管理」中创建并发布道具:
| 字段 | 示例 | 说明 |
|---|---|---|
| productId | VIP_Y_01 | 道具唯一标识 |
| 价格 | 1元 | 道具单价 |
| 状态 | 已发布 | 必须发布后才能使用 |
踩坑:道具创建后需手动发布,发布后约 10 分钟才生效。
二、消息推送配置
虚拟支付的发货通知通过消息推送发送,需要配置推送地址和加密方式。
| 配置项 | 说明 | 建议 |
|---|---|---|
| URL | 接收推送的接口地址 | https://domain/api/.../notify |
| Token | 签名验证用,自定义字符串 | 与后端配置一致 |
| EncodingAESKey | 43位密钥,点击随机生成 | 与后端配置一致 |
| 加密方式 | 明文/兼容/安全模式 | 推荐安全模式 |
| 数据格式 | XML / JSON | 选择 JSON |
重要:安全模式下,无论选择 XML 还是 JSON 格式,推送消息体都会被 AES 加密。区别仅在于外层包装。
URL 握手验证
配置保存时,微信会发送 GET 请求验证:携带参数 signature、timestamp、nonce、echostr。后端校验 signature == SHA1(sort(token, timestamp, nonce)),校验通过后原样返回 echostr。
三、后端实现
YAML 配置
wechat:
miniapp:
offer-id: 你的offerId
sandbox-app-key: 沙箱appKey
prod-app-key: 现网appKey
vip-product-id: VIP_Y_01
vip-goods-price: 100 # ⚠️ 单位:分!
virtual-pay-env: 0 # 0=现网 1=沙箱
push-token: 与MP后台一致
encoding-aes-key: 与MP后台一致
最常见的坑:goodsPrice 单位是分,不是元。MP 后台道具价格 1 元 → 这里填 100。
签名计算
虚拟支付需要两个签名:
paySig = HMAC-SHA256(appKey, "requestVirtualPayment&" + signData)
signature = HMAC-SHA256(sessionKey, signData)
signData 是如下 JSON 字符串:
{
"offerId": "商户号",
"buyQuantity": 1,
"env": 0,
"currencyType": "CNY",
"productId": "VIP_Y_01",
"goodsPrice": 100,
"outTradeNo": "订单号",
"attach": "透传数据"
}
Java 签名实现:
public String calcPaySig(String signData) {
String needSignMsg = "requestVirtualPayment&" + signData;
return hmacSha256Hex(getAppKey(), needSignMsg);
}
public String calcSignature(String signData, String sessionKey) {
return hmacSha256Hex(sessionKey, signData);
}
private String hmacSha256Hex(String key, String data) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(data.getBytes(UTF_8)));
}
发货推送处理
安全模式下推送消息需要解密,处理流程:
Step 1 — 提取密文:检测 URL 参数 msg_signature 是否存在。存在 → 需要解密(body 以 < 开头 → XML 格式,以 { 开头 → JSON 格式)。不存在 → 明文模式。
Step 2 — AES 解密:密钥 = Base64Decode(EncodingAESKey + "=") → 32字节,IV = 密钥前 16 字节,算法 AES-256-CBC,PKCS#7 填充。解密后结构:random(16B) + msg_len(4B) + msg + appid。
Step 3 — 处理发货:解析解密后的 JSON,当 Event == "xpay_goods_deliver_notify" 时,执行订单状态更新和 VIP 激活。
四、前端实现
调起支付
wx.requestVirtualPayment({
signData: params.signData,
paySig: params.paySig,
signature: params.signature,
mode: 'short_series_goods', // 道具直购
success: () => this.pollVipActivation(),
fail: (err) => {
if (err.errMsg?.includes('cancel')) {
wx.showToast({ title: '支付已取消' })
} else {
wx.showModal({
title: '支付失败',
content: `错误码: ${err.errno}\n${err.errMsg}`
})
}
},
})
支付后轮询确认
微信发货推送是异步的,支付成功后需轮询后端确认状态:
for (let i = 0; i < 6; i++) {
await sleep(1500)
const status = await getVipStatus()
if (status.active) return
}
// 超时:提示用户稍后下拉刷新
五、常见错误码
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| -15011 | 沙箱未授权 | 检查白名单,或切 env=0 |
| -15003 | 价格不匹配 | goodsPrice 单位是分 |
| -15002 | 道具不存在 | 确认已发布 |
| -15001 | 参数错误 | 检查 signData 字段完整性 |
| -15005 | 未开通虚拟支付 | MP 后台完成签约 |
六、沙箱测试
- MP 后台「虚拟支付 → 沙箱配置」添加测试微信号
- 后端设置
virtual-pay-env: 1,使用 sandbox-app-key - 测试通过后改为
env: 0,使用 prod-app-key
注意:沙箱配置仅在虚拟支付功能已签约后才可见。
七、关键注意事项
- session_key 必须有效:signature 依赖用户的 session_key,过期需重新登录获取
- 幂等处理:发货推送可能重复发送,必须做幂等校验(检查订单状态)
- GET + POST 双路由:推送地址需同时支持 GET(握手验证)和 POST(消息推送)
- 安全模式兼容:JSON 数据格式下,安全模式的消息体也有 Encrypt 字段,不要当成明文直接解析
- goodsPrice 单位是分:这是最常见的踩坑点,1元 = 100分
总结
微信小程序虚拟支付的接入流程:开通签约 → 创建道具 → 配置消息推送 → 后端签名+解密 → 前端调起支付 → 轮询确认 → 沙箱验证 → 上线。每个环节都有坑,但按流程走就能避开大部分问题。
参考文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html