跳到主要内容微信小程序虚拟支付接入与 ThinkPHP 核心实现 | 极客日志PHPWeChatPay大前端
微信小程序虚拟支付接入与 ThinkPHP 核心实现
综述由AI生成微信小程序虚拟支付的接入流程及基于 ThinkPHP 的核心后端实现。内容包括官方 API 接口说明(查询余额、扣减代币、订单查询等)、前端 JS 调用方法 wx.requestVirtualPayment 以及完整的 ThinkPHP 服务类代码示例。重点讲解了签名规则、订单状态映射及错误处理机制,帮助开发者合规完成虚拟商品支付功能集成。
moshang19 浏览 业务规范与接入指引
微信小程序现已全面支持 iOS 端虚拟支付服务,为虚拟支付业务相关的开发者提供更广阔的用户覆盖。目前 iOS 端虚拟支付享受 15% 优惠费率,极大降低开发者的运营成本。
为保障用户权益,提高交易安全,开发者在小程序内提供的虚拟商品、购买和支付现均需接入小程序虚拟支付。若你的小程序内涉及虚拟支付业务,请在 4 月 1 日前全终端 (包括 iOS 端、安卓端、Windows 与鸿蒙端) 接入虚拟支付,到期未接入将被判定为违规,根据违规程度将对该小程序采取风险提醒、限制功能直至暂停或终止提供服务等措施,请广大开发者及时对照以下接入指引、运营规范等文件业务,确保合规经营。
什么是虚拟支付业务: 虚拟支付业务是指购买非实物商品,比如:VIP 会员、充值代币、录制课程、录制音频视频等虚拟产品。
接入指引:小程序虚拟支付接入指引
运营规范:小程序虚拟支付行为运营规范
基于官方文档,你需要实现以下关键服务器 API。所有接口请求方式均为 POST,Content-Type: application/json,且需在 URL 中携带 access_token 和对应的签名。
| 接口功能 | 官方接口地址 | 必要签名 | 核心用途 |
|---|
| 查询代币余额 | /xpay/query_user_balance | pay_sig + signature | 查询用户剩余代币 |
| 扣减代币 | /xpay/currency_pay | pay_sig + signature | 使用代币支付道具 |
| 查询现金订单 | /xpay/query_order | pay_sig | 查询支付单状态(核心轮询接口) |
| 代币退款 | /xpay/cancel_currency_pay | pay_sig + signature | 退还已扣减的代币 |
| 通知发货完成 | /xpay/notify_provide_goods | pay_sig | 手动通知微信已发货 |
前端 JS 发起支付
wx.requestVirtualPayment(Object object)
基础库 2.19.2 开始支持,低版本需做兼容处理。
以 Promise 风格调用:不支持
小程序插件:不支持
功能描述
发起米大师虚拟支付。
参数 Object object:
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|---|
| signData | Object | - | 是 | 具体支付参数见 signData, 该参数需以 string 形式传递 |
| paySig | string | - | 是 | 支付签名 |
| signature | string | - | 是 | 用户态签名 |
| mode | string | - | 是 | 支付的类型 |
| 属性 | 类型 | 必填 | 说明 |
|---|
| offerId | string | 是 | 在米大师侧申请的应用 id |
| buyQuantity | number | 是 | 购买数量 |
| env | number | 否 | 环境配置,0 米大师正式环境,1 米大师沙箱环境,默认为 0 |
| currencyType | string | 是 | 币种 (合法值:CNY 人民币) |
| productId | string | 否 | 道具 ID,mode=short_series_goods 时需要必填 |
| goodsPrice | number | 否 | 道具单价 (分),mode=short_series_goods 时需要必填 |
| activitySellingPrice | number | 否 | 道具优惠价格(分),非必填 |
| outTradeNo | string | 是 | 业务订单号,每个订单号只能使用一次 |
| attach | string | 是 | 透传数据 |
| 值 | 说明 |
|---|
| short_series_goods | 道具直购 |
| short_series_coin | 代币充值 |
- success(res): 接口调用成功的回调函数
- fail(err): 接口调用失败的回调函数
| 错误码 | 错误信息 | 说明 |
|---|
| 1001 | - | 参数错误 |
| -1 | - | 支付失败 |
| -15002 | outTradeNo 重复使用 | 请换新单号重试 |
| -15007 | session_key 过期 | - |
- 目前只有 >= v2.19.2 的基础库支持该接口,建议开发者这样判断:当前用户的基础库版本 >= v2.19.2 时可以直接用 wx.requestVirtualPayment,小于 v2.19.2 时,用 wx.canIUse('requestVirtualPayment') 来判断接口是否可用。
示例代码
function compareVersion(_v1, _v2) {
if (typeof _v1 !== 'string' || typeof _v2 !== 'string') return 0;
const v1 = _v1.split('.');
const v2 = _v2.split('.');
const len = Math.max(v1.length, v2.length);
while (v1.length < len) { v1.push('0'); }
while (v2.length < len) { v2.push('0'); }
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10);
const num2 = parseInt(v2[i], 10);
if (num1 > num2) { return 1; }
else if (num1 < num2) { return -1; }
}
return 0;
}
const SDKVersion = wx.getSystemInfoSync().SDKVersion;
if (compareVersion(SDKVersion, '2.19.2') >= 0 || wx.canIUse('requestVirtualPayment')) {
wx.requestVirtualPayment({
signData: JSON.stringify({
offerId: '123',
buyQuantity: 1,
env: 0,
currencyType: 'CNY',
productId: 'testproductId',
goodsPrice: 10,
outTradeNo: 'xxxxxx',
attach: 'testdata'
}),
paySig: 'd0b8bbccbe109b11549bcfd6602b08711f46600965253a949cd6a2b895152f9d',
signature: 'd0b8bbccbe109b11549bcfd6602b08711f46600965253a949cd6a2b895152f9d',
mode: 'short_series_goods',
success(res) {
console.log('requestVirtualPayment success', res);
},
fail({ errMsg, errCode }) {
console.error(errMsg, errCode);
}
});
} else {
console.log('当前用户的客户端不支持 wx.requestVirtualPayment');
}
ThinkPHP 核心代码实现
1. 查询用户代币余额
public function queryUserBalance($openid, $userIp = '') {
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return ['errcode' => -1, 'errmsg' => '获取 access_token 失败'];
}
$timestamp = time();
$nonce = $this->generateNonce();
$outTradeNo = $this->generateOutTradeNo();
$paySig = $this->generatePaySig('/xpay/query_user_balance', [
'offerId' => $this->config['offer_id'],
'timestamp' => $timestamp,
'nonce' => $nonce,
'outTradeNo' => $outTradeNo,
]);
$signature = $this->generateSignature($this->getSessionKey($openid), $openid, $outTradeNo);
$url = "https://api.weixin.qq.com/xpay/query_user_balance";
$url .= "?access_token={$accessToken}&pay_sig={$paySig}&signature={$signature}";
$params = [
'openid' => $openid,
'env' => $this->config['env'],
'user_ip' => $userIp ?: request()->ip(),
];
$result = $this->httpPost($url, json_encode($params, JSON_UNESCAPED_UNICODE));
Log::record("[虚拟支付] 查询余额响应:" . json_encode($result));
return $result;
}
2. 扣减代币(代币支付)
public function currencyPay($openid, $amount, $orderId, $payItem = []) {
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return ['errcode' => -1, 'errmsg' => '获取 access_token 失败'];
}
$timestamp = time();
$nonce = $this->generateNonce();
$outTradeNo = $this->generateOutTradeNo();
$paySig = $this->generatePaySig('/xpay/currency_pay', [
'offerId' => $this->config['offer_id'],
'timestamp' => $timestamp,
'nonce' => $nonce,
'outTradeNo' => $outTradeNo,
]);
$signature = $this->generateSignature($this->getSessionKey($openid), $openid, $outTradeNo);
$url = "https://api.weixin.qq.com/xpay/currency_pay";
$url .= "?access_token={$accessToken}&pay_sig={$paySig}&signature={$signature}";
$params = [
'openid' => $openid,
'env' => $this->config['env'],
'user_ip' => request()->ip(),
'amount' => $amount,
'order_id' => $orderId,
'payitem' => json_encode($payItem, JSON_UNESCAPED_UNICODE),
'remark' => '代币购买道具',
];
$result = $this->httpPost($url, json_encode($params, JSON_UNESCAPED_UNICODE));
Log::record("[虚拟支付] 扣减代币响应:" . json_encode($result));
return $result;
}
3. 查询现金订单(核心轮询接口)
public function queryOrder($outTradeNo = '', $wxOrderId = '') {
if (empty($outTradeNo) && empty($wxOrderId)) {
return ['errcode' => -1, 'errmsg' => '订单号不能为空'];
}
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return ['errcode' => -1, 'errmsg' => '获取 access_token 失败'];
}
$timestamp = time();
$nonce = $this->generateNonce();
$paySig = $this->generatePaySig('/xpay/query_order', [
'offerId' => $this->config['offer_id'],
'timestamp' => $timestamp,
'nonce' => $nonce,
'outTradeNo' => $outTradeNo ?: $wxOrderId, // 用任一单号生成签名
]);
$url = "https://api.weixin.qq.com/xpay/query_order";
$url .= "?access_token={$accessToken}&pay_sig={$paySig}";
$params = ['env' => $this->config['env']];
if (!empty($outTradeNo)) {
$params['order_id'] = $outTradeNo;
} else {
$params['wx_order_id'] = $wxOrderId;
}
$result = $this->httpPost($url, json_encode($params, JSON_UNESCAPED_UNICODE));
if (isset($result['errcode']) && $result['errcode'] == 0 && !empty($result['order'])) {
$this->syncOrderStatus($result['order']);
}
return $result;
}
4. 通知发货完成
public function notifyProvideGoods($outTradeNo) {
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return ['errcode' => -1, 'errmsg' => '获取 access_token 失败'];
}
$timestamp = time();
$nonce = $this->generateNonce();
$paySig = $this->generatePaySig('/xpay/notify_provide_goods', [
'offerId' => $this->config['offer_id'],
'timestamp' => $timestamp,
'nonce' => $nonce,
'outTradeNo' => $outTradeNo,
]);
$url = "https://api.weixin.qq.com/xpay/notify_provide_goods";
$url .= "?access_token={$accessToken}&pay_sig={$paySig}";
$params = [
'order_id' => $outTradeNo,
'env' => $this->config['env'],
];
$result = $this->httpPost($url, json_encode($params, JSON_UNESCAPED_UNICODE));
Log::record("[虚拟支付] 通知发货响应:" . json_encode($result));
return $result;
}
关键注意事项
- 签名规则:文档明确要求 支付签名
pay_sig 和 用户态签名 signature 需加在 URL 的 Query 中(如 ?access_token=xxx&pay_sig=xxx&signature=xxx),而业务参数在 POST Body 中。请严格区分。
- 订单状态映射:微信返回的订单状态(
status字段 0-10)需映射到你本地数据库的状态。特别是 2-已支付待发货 状态,是触发你发货逻辑的关键点。
- 错误处理:注意处理文档中列出的错误码,如
-15002(outTradeNo 重复)、268490009(session_key 过期)等,并在代码中做好重试或补偿机制。
- 环境隔离:务必使用
env 参数区分沙箱(1)和正式(0)环境。测试期间使用沙箱环境,避免真实扣费。
相关免费在线工具
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online