2026年2月27日,微信官方发布了小程序虚拟支付业务管理规范更新公告,要求所有涉及虚拟支付的微信小程序必须在2026年4月1日前完成合规接入。作为开发者,我们需要理解这次更新的核心变化:
在开始编码前,我们需要准备以下材料:
重要提示:虚拟支付接口与普通微信支付接口不同,不能混用。如果你的小程序之前使用的是普通支付接口,必须迁移到虚拟支付专用接口。
微信小程序虚拟支付的完整流程包含三个核心环节:
微信官方提供的时序图清晰地展示了虚拟支付的完整流程:
这个流程与普通微信支付的主要区别在于:
在虚拟支付中,以下几个参数尤为重要:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| offerId | string | 是 | 虚拟支付业务ID,在微信后台申请 |
| productId | string | 是 | 虚拟商品ID,需在后台配置 |
| buyQuantity | int | 是 | 购买数量 |
| goodsPrice | int | 是 | 商品价格,单位分 |
| outTradeNo | string | 是 | 商户订单号 |
| attach | string | 否 | 附加数据,会原样返回 |
首先我们需要创建一个配置类来管理所有必要的参数:
php复制class VirtualPayConfig
{
// 小程序相关配置
protected $appId = '你的小程序AppID';
protected $appSecret = '你的小程序AppSecret';
// 支付相关配置
protected $mchId = '微信支付商户号';
protected $virtualPayKey = '虚拟支付API密钥';
protected $offerId = '虚拟支付业务ID';
// 环境配置
protected $env = 0; // 0-正式环境 1-沙箱环境
public function getAppId() {
return $this->appId;
}
// 其他getter方法...
}
虚拟支付需要两种签名:支付签名(paySig)和用户签名(signature)。
php复制public function generatePaySig(string $uri, string $postBody): string
{
$signStr = $uri . '&' . $postBody;
return hash_hmac('sha256', $signStr, $this->config->getVirtualPayKey());
}
关键点说明:
php复制public function generateUserSignature(string $postBody, string $sessionKey): string
{
return hash_hmac('sha256', $postBody, $sessionKey);
}
用户签名需要用到小程序的session_key,这个需要通过wx.login获取code后,调用auth.code2Session接口获取。
php复制public function createVirtualPayment(array $params): array
{
// 验证必要参数
if (empty($params['openid']) || empty($params['product_id']) ||
empty($params['amount']) || empty($params['order_no'])) {
throw new InvalidArgumentException('缺少必要参数');
}
// 构造签名数据
$signData = [
'offerId' => $this->config->getOfferId(),
'buyQuantity' => $params['quantity'] ?? 1,
'env' => $this->config->getEnv(),
'currencyType' => 'CNY',
'productId' => $params['product_id'],
'goodsPrice' => (int)$params['amount'],
'outTradeNo' => $params['order_no'],
'attach' => $params['attach'] ?? '',
];
$signDataJson = json_encode($signData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return [
'mode' => 'short_series_goods', // 固定值
'offerId' => $this->config->getOfferId(),
'productId' => $params['product_id'],
'signData' => $signDataJson,
'paySig' => $this->generatePaySig('requestVirtualPayment', $signDataJson),
'signature' => $this->generateUserSignature($signDataJson, $params['session_key']),
];
}
javascript复制wx.requestVirtualPayment({
mode: 'short_series_goods',
offerId: '123456',
productId: 'product_001',
signData: JSON.stringify(signData),
paySig: '生成的paySig',
signature: '生成的用户签名',
success(res) {
console.log('支付成功', res);
// 这里建议再查询一次订单状态确认
},
fail(err) {
console.error('支付失败', err);
}
});
支付完成后,我们需要主动查询订单状态:
php复制public function queryOrderStatus(string $openid, string $orderNo): array
{
$accessToken = $this->getAccessToken();
$queryData = [
'openid' => $openid,
'env' => $this->config->getEnv(),
'order_id' => $orderNo,
];
$queryDataJson = json_encode($queryData, JSON_UNESCAPED_UNICODE);
$paySig = $this->generatePaySig('/xpay/query_order', $queryDataJson);
$url = sprintf(
'https://api.weixin.qq.com/xpay/query_order?access_token=%s&pay_sig=%s',
$accessToken,
$paySig
);
$response = $this->httpClient->post($url, [
'body' => $queryDataJson,
'headers' => ['Content-Type' => 'application/json']
]);
return json_decode($response->getBody(), true);
}
签名错误:
版本兼容问题:
javascript复制// 版本检测示例
function checkVirtualPaySupport() {
const SDKVersion = wx.getSystemInfoSync().SDKVersion;
const [major, minor, patch] = SDKVersion.split('.').map(Number);
return major > 2 || (major === 2 && minor >= 19 && patch >= 2);
}
php复制public function getAccessToken(): string
{
$cacheKey = 'wx_access_token_' . $this->config->getAppId();
if ($token = $this->cache->get($cacheKey)) {
return $token;
}
$url = sprintf(
'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s',
$this->config->getAppId(),
$this->config->getAppSecret()
);
$response = $this->httpClient->get($url);
$data = json_decode($response->getBody(), true);
$this->cache->set($cacheKey, $data['access_token'], 7000); // 提前100秒过期
return $data['access_token'];
}
敏感信息保护:
防重复支付:
金额校验:
php复制// 金额校验示例
public function validateAmount(string $productId, int $amount): bool
{
$expectedAmount = $this->productRepository->getPrice($productId);
return $expectedAmount === $amount;
}
在实际项目中接入微信小程序虚拟支付,最耗时的往往不是技术实现,而是对业务逻辑的梳理和异常情况的处理。建议在开发阶段充分测试各种边界情况,特别是网络中断、支付超时等场景。支付模块上线后,要建立完善的监控机制,确保能及时发现和处理问题。