写在最前面
- 这篇内容定位是“实战接入经验 + 易错点总结”,尽量让第一次接入的同学少走弯路。
- 文中信息基于 2026-02-27 的官方文档与公告整理,后续如有规则更新,请以官方最新文档为准。
- 如有问题欢迎交流,能帮到的我会尽量帮。
- 观点欢迎讨论,建议优先围绕“问题复现、官方依据、可执行方案”交流。
- 接入过程中只遇到了
-15011这一个问题,当前提供的QA有限,后续根据相关提问进行补充
据腾讯 2026-02-27 公告:若小程序涉及虚拟支付业务,需在 4 月 1 日前完成全终端(iOS/安卓/Windows/鸿蒙)接入。未按要求接入可能被判定违规,并按程度采取风险提醒、限制功能直至暂停或终止服务。公告原文:点我查看
虚拟支付业务:在小程序内购买非实物商品,包括但不限于 VIP、代币充值、课程内容、音视频内容等。
更新记录
- 2026.03.06 新增 java、go 语言签名示例
1. 产品 or 运营
1.1 官方开发文档地址
- 虚拟支付总文档:
https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html - 小程序端 API:
https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestVirtualPayment.html
建议:先通读总文档目录,重点看“开发流程 / wx API / 服务端 API / 签名详解 / 回调事件”。
1.2 虚拟支付开通前置条件
官方文档给出的核心条件(以官方页面为准):
- 主体类型为已认证小程序
- 小程序类型为企业小程序
- 小程序主体信息完备
- 小程序运营资质属于“文娱、工具、社交、资讯、深度合成”等相关类目
实践补充(个人经验,非官方承诺,毕竟官方产品线下说的也不一定作数):
- 个体户主体在部分场景下可开通,但请以后台实际校验和审核结果为准。
- 对公账户能力是关键前提,具体以平台要求为准。
更新:已支持个体户主体申请开通虚拟支付
1.3 虚拟支付商户开通流程
此文先不展开开户操作步骤(官方文档有流程),重点聚焦“已开通后的接入与落地”。
建议:先完成后台开通,再推进开发,避免前后端联调时被资质状态阻塞。
1.4 基础配置、代币与道具配置
1.4.1 基础配置
基础配置是开发输入源,重点确认:
AppIDOfferID- 沙箱
AppKey - 现网
AppKey - iOS 支付开关状态(如已启用,注意评估影响)
建议:
- 建立“沙箱/现网参数表”并分环境存放,禁止人工口口相传。
1.4.2 代币配置
代币兑换比例常见有 1:1、1:10、1:100。
重要提醒:
- 代币兑换比例配置后不可修改(请务必上线前评审)。
实践建议:
- 大多数业务更适合
1:10或1:100,金额粒度更友好。 - 若选
1:1,常见会遇到金额粒度受限(小数金额不好处理)。
1.4.3 道具配置
实践观察:
- 道具发布需要审核
- 道具发布后可编辑部分信息,但 ID 不可改
- 道具量小可直接后台维护,道具量大建议走 API 化管理
建议:
- 在运营侧建立“道具命名/价格/有效期”规范,避免多版本混乱。
1.5 虚拟支付代币与道具的选择
先给一个“够用且不绕”的结论:
低频单次交易优先道具,高频复购交易优先代币。
1.5.1 一张表先判断
| 维度 | 代币模式(short_series_coin) |
道具模式(short_series_goods) |
|---|---|---|
| 适用场景 | 高频、多次、小额消费 | 单次购买、单次交付 |
| 用户路径 | 先充值,再消费 | 直接购买,直接到账 |
| 运营能力 | 强(首充/赠送/礼包) | 中(单品促销) |
| 研发复杂度 | 高(余额/流水/补偿) | 中(订单/发货/退款) |
| 上线速度 | 中 | 快 |
| 典型风险 | 兑换比例与账务一致性 | 价格一致性与商品管理 |
1.5.2 常见业务怎么选
| 业务场景 | 推荐模式 | 备注 |
|---|---|---|
| 剧集/章节逐步解锁 | 代币优先 | 高频消费更适合余额体系 |
| 单次会员购买 | 道具优先 | 链路短,用户理解成本低 |
| 直播/内容打赏 | 代币优先 | 高频小额典型场景 |
| 素材包/单品资源购买 | 道具优先 | 单次交付更直接 |
| 时间紧、先合规上线 | 道具优先 | 先稳后扩展 |
1.5.3 三条硬提醒(避免返工)
- 代币兑换比例发布后不可修改,上线前必须评审并沙箱验证。
- 不要把“前端支付 success”当最终成功,必须以后端状态为准。
- 不管选哪种模式,都要做“回调 + 查单”双通道兜底。
2. 开发
2.1 开发接入总流程(不含开商户)
- 前端点击购买/充值
- 前端请求服务端创建订单
- 服务端组装
signData并生成paySig/signature - 前端调用
wx.requestVirtualPayment - 服务端通过回调 + 查单确认最终状态
- 发货/入账
- 更新订单终态并可查询
核心原则:
- 前端
success只算弱确认 - 最终成功以后端状态为准
2.2 小程序端接入要点(wx.requestVirtualPayment)
2.2.1 必要参数(以官方文档为准,本文可能存在更新滞后性)
wx.requestVirtualPayment 关键入参:
mode:short_series_goods(道具直购)short_series_coin(代币充值)
signData:字符串paySigsignaturesuccess/fail/complete
signData 常见通用字段:
offerIdbuyQuantityenv(0 正式 / 1 沙箱)currencyType(CNY)outTradeNoattach
short_series_goods 额外字段:
productIdgoodsPrice(分)activitySellingPrice(可选,需与goodsPrice配合)
2.2.2 前端建议
- 每次支付前重新
wx.login - 请求设置超时,避免卡死
- 打印完整日志(请求、响应、耗时)
- 明确给用户展示“支付中/确认中/已到账/失败”
2.3 服务端接入要点
2.3.1 服务端必须做什么
- 生成唯一订单号
- 组装
signData - 计算
paySig、signature - 管理订单状态机
- 处理回调 + 主动查单补偿
2.3.2 签名参考(官方算法 + Node.js / PHP / Java / Go 示例)
官方签名核心:
paySig = hex(hmac_sha256(appKey, uri + '&' + signData))
signature = hex(hmac_sha256(session_key, signData))
其中 wx.requestVirtualPayment 场景 uri 固定为 requestVirtualPayment。
Node.js 示例:
import crypto from 'crypto';
function hmacSha256Hex(key, data) {
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex');
}
function calcVirtualPaymentSign({ signData, sessionKey, env, appKeyProd, appKeySandbox }) {
const appKey = env === 1 ? appKeySandbox : appKeyProd;
const paySig = hmacSha256Hex(appKey, `requestVirtualPayment&${signData}`);
const signature = hmacSha256Hex(sessionKey, signData);
return { paySig, signature };
}
PHP 示例:
function calcVirtualPaymentSign(string $signData, string $sessionKey, int $env, string $appKeyProd, string $appKeySandbox): array
{
$appKey = $env === 1 ? $appKeySandbox : $appKeyProd;
$paySig = hash_hmac('sha256', 'requestVirtualPayment&' . $signData, $appKey);
$signature = hash_hmac('sha256', $signData, $sessionKey);
return ['paySig' => $paySig, 'signature' => $signature];
}
Java 示例:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public class VirtualPaymentSignDemo {
static String hmacSha256Hex(String key, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
static SignResult calcVirtualPaymentSign(
String signData,
String sessionKey,
int env,
String appKeyProd,
String appKeySandbox
) throws Exception {
String appKey = env == 1 ? appKeySandbox : appKeyProd;
String paySig = hmacSha256Hex(appKey, "requestVirtualPayment&" + signData);
String signature = hmacSha256Hex(sessionKey, signData);
return new SignResult(paySig, signature);
}
static class SignResult {
final String paySig;
final String signature;
SignResult(String paySig, String signature) {
this.paySig = paySig;
this.signature = signature;
}
}
}
Go 示例:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
type SignResult struct {
PaySig string
Signature string
}
func hmacSHA256Hex(key, data string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
func calcVirtualPaymentSign(signData, sessionKey string, env int, appKeyProd, appKeySandbox string) SignResult {
appKey := appKeyProd
if env == 1 {
appKey = appKeySandbox
}
return SignResult{
PaySig: hmacSHA256Hex(appKey, "requestVirtualPayment&"+signData),
Signature: hmacSHA256Hex(sessionKey, signData),
}
}
2.3.3 服务端接口签名示例(Node.js / PHP / Java / Go,参考官方 Python 脚本)
服务端 API(如 query_user_balance / query_order)签名规则:
pay_sig = HMAC_SHA256(appKey, uri + '&' + post_body)signature = HMAC_SHA256(session_key, post_body)
Node.js 示例:
import crypto from 'crypto';
function hmacSha256Hex(key, msg) {
return crypto.createHmac('sha256', key).update(msg, 'utf8').digest('hex');
}
function calcPaySig(uri, postBody, appKey) {
return hmacSha256Hex(appKey, `${uri}&${postBody}`);
}
function calcSignature(postBody, sessionKey) {
return hmacSha256Hex(sessionKey, postBody);
}
// 官方样例参数自校验
const uri = '/xpay/query_user_balance';
const appKey = '12345';
const sessionKey = '9hAb/NEYUlkaMBEsmFgzig==';
const postBody = '{"openid": "xxx", "user_ip": "127.0.0.1", "env": 0}';
const paySig = calcPaySig(uri, postBody, appKey);
const signature = calcSignature(postBody, sessionKey);
console.assert(paySig === 'c37809f27c6d7fd1837ad2500a04512b66b34fd793a39a385fade56dca89a4b5');
console.assert(signature === '089d9e8dc5d308977360c4b79ec600a93d736802802a807d634192328032f6c7');
console.log('self-test PASS');
PHP 示例:
<?php
function hmac_sha256_hex(string $key, string $msg): string
{
return hash_hmac('sha256', $msg, $key);
}
function calc_pay_sig(string $uri, string $postBody, string $appKey): string
{
return hmac_sha256_hex($appKey, $uri . '&' . $postBody);
}
function calc_signature(string $postBody, string $sessionKey): string
{
return hmac_sha256_hex($sessionKey, $postBody);
}
// 官方样例参数自校验
$uri = '/xpay/query_user_balance';
$appKey = '12345';
$sessionKey = '9hAb/NEYUlkaMBEsmFgzig==';
$postBody = '{"openid": "xxx", "user_ip": "127.0.0.1", "env": 0}';
$paySig = calc_pay_sig($uri, $postBody, $appKey);
$signature = calc_signature($postBody, $sessionKey);
if ($paySig !== 'c37809f27c6d7fd1837ad2500a04512b66b34fd793a39a385fade56dca89a4b5') {
throw new RuntimeException('pay_sig mismatch');
}
if ($signature !== '089d9e8dc5d308977360c4b79ec600a93d736802802a807d634192328032f6c7') {
throw new RuntimeException('signature mismatch');
}
echo "self-test PASS\n";
Java 示例:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public class XpayServerSignDemo {
static String hmacSha256Hex(String key, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
static String calcPaySig(String uri, String postBody, String appKey) throws Exception {
return hmacSha256Hex(appKey, uri + "&" + postBody);
}
static String calcSignature(String postBody, String sessionKey) throws Exception {
return hmacSha256Hex(sessionKey, postBody);
}
public static void main(String[] args) throws Exception {
String uri = "/xpay/query_user_balance";
String appKey = "12345";
String sessionKey = "9hAb/NEYUlkaMBEsmFgzig==";
String postBody = "{\"openid\": \"xxx\", \"user_ip\": \"127.0.0.1\", \"env\": 0}";
String paySig = calcPaySig(uri, postBody, appKey);
String signature = calcSignature(postBody, sessionKey);
if (!"c37809f27c6d7fd1837ad2500a04512b66b34fd793a39a385fade56dca89a4b5".equals(paySig)) {
throw new RuntimeException("pay_sig mismatch: " + paySig);
}
if (!"089d9e8dc5d308977360c4b79ec600a93d736802802a807d634192328032f6c7".equals(signature)) {
throw new RuntimeException("signature mismatch: " + signature);
}
System.out.println("self-test PASS");
}
}
Go 示例:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func hmacSHA256Hex(key, data string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
func calcPaySig(uri, postBody, appKey string) string {
return hmacSHA256Hex(appKey, uri+"&"+postBody)
}
func calcSignature(postBody, sessionKey string) string {
return hmacSHA256Hex(sessionKey, postBody)
}
func main() {
uri := "/xpay/query_user_balance"
appKey := "12345"
sessionKey := "9hAb/NEYUlkaMBEsmFgzig=="
postBody := "{\"openid\": \"xxx\", \"user_ip\": \"127.0.0.1\", \"env\": 0}"
paySig := calcPaySig(uri, postBody, appKey)
signature := calcSignature(postBody, sessionKey)
if paySig != "c37809f27c6d7fd1837ad2500a04512b66b34fd793a39a385fade56dca89a4b5" {
panic("pay_sig mismatch: " + paySig)
}
if signature != "089d9e8dc5d308977360c4b79ec600a93d736802802a807d634192328032f6c7" {
panic("signature mismatch: " + signature)
}
fmt.Println("self-test PASS")
}
请求地址示意:
https://api.weixin.qq.com/xpay/query_user_balance?access_token=ACCESS_TOKEN&pay_sig=PAY_SIG&signature=SIGNATURE
签名不一致排查清单:
uri是否正确(服务端 API 必须是实际路径,不能带 query)post_body是否与实际请求体逐字一致env与AppKey是否匹配session_key是否有效、是否属于当前用户- 编码是否统一(UTF-8)
- 序列化逻辑是否稳定
2.3.4 接口签名方式对照表(以“接口后备注”为准)
说明:
- 以下表格依据官方文档各接口章节末尾的“备注”整理。
- 判定规则以“接口后备注”为准,不以接口名称前的通用说明替代。
- 官方文档地址(query_user_balance 锚点):https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html#query-user-balance
| 接口 | 接口后备注(官方原意) | pay_sig |
signature |
|---|---|---|---|
query_user_balance |
需要用户态签名与支付签名 | 是 | 是 |
currency_pay |
使用用户态签名与支付签名 | 是 | 是 |
query_order |
使用支付签名 | 是 | 否 |
cancel_currency_pay |
需要支付签名与用户态签名 | 是 | 是 |
download_bill |
使用支付签名 | 是 | 否 |
refund_order |
使用支付签名 | 是 | 否 |
create_withdraw_order |
使用支付签名 | 是 | 否 |
query_withdraw_order |
使用支付签名 | 是 | 否 |
start_upload_goods |
使用支付签名 | 是 | 否 |
query_upload_goods |
使用支付签名 | 是 | 否 |
start_publish_goods |
使用支付签名 | 是 | 否 |
query_publish_goods |
使用支付签名 | 是 | 否 |
query_biz_balance |
使用支付签名 | 是 | 否 |
get_complaint_list |
使用支付签名 | 是 | 否 |
get_complaint_detail |
使用支付签名 | 是 | 否 |
get_negotiation_history |
使用支付签名 | 是 | 否 |
response_complaint |
使用支付签名 | 是 | 否 |
complete_complaint |
使用支付签名 | 是 | 否 |
upload_vp_file |
使用支付签名 | 是 | 否 |
get_upload_file_sign |
使用支付签名 | 是 | 否 |
2.3.5 环境配置管理(强烈建议)
- 沙箱:
env=1+ 沙箱 AppKey - 现网:
env=0+ 现网 AppKey
易错点:混用环境参数和密钥。
2.3.6 订单状态机建议
建议最少包含:
INITPAYINGPAIDDELIVERINGSUCCESSFAILED/CLOSEDREFUNDING/REFUNDED
2.4 注意事项与高频易错点
2.4.1 SIG_EMPTY
本质:signature 为空。常见原因:session_key 缺失/过期。
2.4.2 invalid code
本质:wx.login code 无效(复用/过期/参数不匹配)。
2.4.3 -15011(现网 env 填错)
错误含义(官方):
-15011:现网版本的env只能是0,不能填1(沙箱)。- iOS 端不要使用沙箱环境(
env=1)。 - 按实测经验,iOS 测试与现网都需要使用
env=0,否则会触发-15011报错。
建议对照:
| 终端与阶段 | env 建议值 |
说明 |
|---|---|---|
| iOS(测试) | 0 |
必须使用现网环境 |
| iOS(现网) | 0 |
必须使用现网环境 |
| 安卓/Windows/鸿蒙(沙箱测试) | 1 |
联调期建议沙箱 |
| 安卓/Windows/鸿蒙(现网) | 0 |
上线后统一现网 |
排查 checklist:
- 如果是 iOS,先确认
env是否为0。 - 是否误用了沙箱 AppKey。
- 对应道具/代币是否已在现网发布生效。
- 同一套参数在安卓可用、iOS 失败时,优先排查
env和 iOS 版本适配要求。
2.4.4 金额与数量换算(代币)
- 展示金额与下单数量要有明确换算规则
- 兑换比例上线前务必评审
- 即使收到回调,也建议至少查单一次再确认最终状态
2.4.5 回调、轮询与补偿
- 有回调优先回调
- 无回调或异常时主动查单
- 全链路幂等,避免重复发货/重复入账
2.5 上线前检查清单
2.6 最后建议
- 先确保“稳定到账”,再做复杂玩法
- 前端 success 不是最终成功
- 文档与配置要持续维护
- 规则变更请以官方文档为准

扣减代币/xpay/currency_pay,需要signature(session_Key),代币支付推送xpay_coin_pay_notify只能返回OpenId,这样还是没法回调是代币扣费啊,网站就是用于会员账号余额充值,web-view嵌套的h5做了个小程序支付页,现在requestPayment支付成功后如何不只用success?
支付成功后如何不只用success?
道具模式一笔订单只能支持单个道具的购买吧,那如果有复杂的多个道具的模式,并且价格的计算不是固定的,就不能用道具模式了,类似图中:
虚拟支付下只支持开通一个新的商户号吗? 商户号是否存在封禁的可能?一旦被封禁还可以申请新的商户号吗?遇到封禁的情况是不是影响支付业务?导致支付不可用? 像这种情况有没有备用方案,例如有多个商户号来备用? 还是说只能申诉?
wx.requestVirtualPayment要求高,iOS15和微信版本8.0.68及以上,可以低于这些跳转到wx.requestPayment?这样合规吧,总不能让用户付不了款
我是个体户,之前一直有安卓的虚拟商品,好多年了,结果后台突然接到通知要升级,结果显示个体户不能使用虚拟支付。
吓坏了,到处搜。
官方群:每人回复。
社区里:所有人都在问,没官方回复。
问负责审核的客服,给我转了两个其他客服,最后告诉我:以页面为准,小程序不算企业,不能使用虚拟支付。
当天就去找代注册公司、代理记账,昨天把合同签完了,钱交了。
今天,个体户TM的允许使用了?
气笑了。
你好,我们是企业,公司的微信支付商户号单笔最大限额6000元。所以小程序端购买会员的时候,超过6000元的订单,我们需要引导用户到浏览器打开链接拉起支付宝支付,或者引导对公转账。
想问下微信对于小程序内虚拟支付的订单,是否允许引导到外部?有这方面规则的详细文档吗?
虚拟支付,购买道具的接口是哪个?文档里面为何没有服务端的接口?
您好,请问道具发布一直提示频繁请问是什么问题,换过浏览器无痕模式,也试过间隔24H后再发布
我们想使用代币功能,按照文章的描述生成签名,一直报错15005,选的沙箱模式,请问这个写法有什么问题吗?