商户调用虚拟支付接口 wx.requestVirtualPayment 失败,一直收到 errCode: -15005, SIGNATURE_INVALID。已经反复核对过前、后端代码,自认为签名生成逻辑完全符合官方文档。 我的关键信息
- Offer ID: 1450523151
- 商户号: 1111636273
- AppID: wx20b352388bc992dd
- 环境:沙箱环境 (
env:0) - 签名算法:HMAC-SHA256 (按照虚拟支付文档)
- 签名密钥:已从后台复制沙箱 AppKey(z1rxyEm.....FAi4XEC,已确认大小写)
signdata 原文
{
"offerId": "1450523151",
"buyQuantity": 1,
"env": 1,
"currencyType": "CNY",
"productId": "8",
"goodsPrice": 10,
"outTradeNo": "20260509105144wx38749",
"attach": "{\"user_id\":\"5\",\"productId\":\"8\",\"quantity\":1}"
}
最终 signature:eba87d77528b131315773a0e0b23d16a092d9d1ac7780ac8f4e6998bbd53114a
paySig: 84F93E07D5024B422C9308FCA31A2741
诉求:请协助确认我的 Offer ID 状态、正式 AppKey 与商户号是否匹配,以及 env:1 沙箱环境下的道具配置是否已完全生效。我的后端代码是基于 HMAC-SHA256 算法实现的,自认无误,怀疑是商户后台配置问题导致签名验证失败。求官方技术专员协助排查。
public function createOrder(){
$user_id = $this->request->post('user_id', '', 'serach_in');
if(!$user_id) throw new ValidateException('参数错误');
$vip_id = $this->request->post('vip_id', '', 'serach_in');
if(!$vip_id) throw new ValidateException('参数错误');
$vip = Db::name("vip")->find($vip_id);
if (!$vip){
throw new ValidateException('会员不存在');
}
$user = Db::name("user")->find($user_id);
if(!$user){
throw new ValidateException('用户不存在');
}
$client_type = $this->request->post("client_type", "miniprogram");
$sessionKey = $this->request->post('sessionKey', '', 'serach_in');
// 查找未支付订单
$vip_order = Db::name("vip_order")
->where("user_id", $user_id)
->where("vip_id", $vip_id)
->where("vip_order_status", 0)
->order("createtime", "desc")
->find();
$config = Db::name("base_config")->where("tenant_id", $user['tenant_id'])->column("data","name");
// 根据client_type设置不同的appid
switch($client_type) {
case 'miniprogram':
$config['appid'] = $config['mini_appid'] ?? '';
if(empty($config['appid'])) throw new ValidateException('小程序appid未配置');
break;
case 'miniprogram_virtual': // 微信虚拟支付
$config['appid'] = $config['mini_appid'] ?? '';
if(empty($config['appid'])) throw new ValidateException('小程序appid未配置');
break;
case 'h5':
$config['appid'] = $config['h5_appid'] ?? '';
if(empty($config['appid'])) throw new ValidateException('H5 appid未配置');
break;
case 'douyin':
$config['appid'] = $config['douyin_appid'] ?? '';
if(empty($config['appid'])) throw new ValidateException('抖音appid未配置');
break;
default:
throw new ValidateException('不支持的客户端类型');
}
$base_url = $config['base_url'] ?? '';
if(empty($base_url)) throw new ValidateException('base_url未配置');
// 根据client_type生成不同的订单号前缀
if ($client_type != "douyin") {
$outTradeNo = doOrderSn("wx");
} else {
$outTradeNo = doOrderSn("dy");
}
$createtime = time();
if($vip_order){
// 更新未支付订单
Db::name("vip_order")
->where("vip_order_id", $vip_order['vip_order_id'])
->update([
"vip_order_no" => $outTradeNo,
"update_time" => $createtime,
"client_type" => $client_type
]);
$vip_order['vip_order_no'] = $outTradeNo;
$vip_order['client_type'] = $client_type;
} else {
// 创建新订单
$vip_order_id = Db::name("vip_order")->insertGetId([
"vip_id" => $vip_id,
"vip_order_no" => $outTradeNo,
"user_id" => $user['user_id'],
"vip_order_price" => $vip['vip_price'],
"vip_order_status" => 0,
"vip_order_type" => 1,
"client_type" => $client_type,
"createtime" => $createtime,
"tenant_id" => $user['tenant_id'],
"update_time" => $createtime
]);
$vip_order = [
"vip_order_id" => $vip_order_id,
"vip_id" => $vip_id,
"vip_order_no" => $outTradeNo,
"user_id" => $user['user_id'],
"vip_order_price" => $vip['vip_price'],
"vip_order_status" => 0,
"vip_order_type" => 1,
"client_type" => $client_type,
"createtime" => $createtime,
"tenant_id" => $user['tenant_id']
];
}
// 根据client_type获取对应的openid
$openIdField = '';
$openId = '';
switch($client_type) {
case "miniprogram":
$openIdField = 'mp_openid';
$openId = $user['mp_openid'] ?? '';
break;
case "miniprogram_virtual":
$openIdField = 'mp_openid';
$openId = $user['mp_openid'] ?? '';
break;
case "h5":
$openIdField = 'h5_openid';
$openId = $user['h5_openid'] ?? '';
break;
case "douyin":
$openIdField = 'douyin_openid';
$openId = $user['douyin_openid'] ?? '';
break;
}
if(empty($openId)) {
throw new ValidateException('用户未授权,无法获取' . $openIdField);
}
// 构建支付数据
$data = [
"body" => $vip['vip_name'] . "会员服务",
"out_trade_no" => $outTradeNo,
"total_fee" => intval($vip['vip_price'] * 100),
"notify_url" => $base_url . "/api/VipOrder/payCallback",
"openid" => $openId,
"trade_type" => 'JSAPI',
"attach" => json_encode([
'order_type' => 'vip',
'vip_id' => $vip_id,
'user_id' => $user_id,
'client_type' => $client_type
])
];
// 虚拟支付添加额外参数
if($client_type == 'miniprogram_virtual') {
$data['goods_tag'] = 'VIRTUAL';
$data['product_id'] = $vip_id;
}
// 根据client_type调用不同的支付服务
try {
$paymentResult = [];
switch($client_type) {
case 'miniprogram':
// 小程序普通支付
$paymentResult = PayService::jsapiPay($data, $user["tenant_id"], $config);
break;
case 'miniprogram_virtual':
// 小程序虚拟支付
$data['notify_url']=$base_url."/api/VipOrder/miniProgramVirtualPayCallback";
$paymentResult = PayService::miniProgramVirtualPay($data, $user["tenant_id"], $config);
break;
case 'h5':
// H5支付
$paymentResult = PayService::jsapiPay($data, $user["tenant_id"], $config);
break;
case 'douyin':
// 抖音支付
$data = [
"good_id"=>$vip['vip_id'],
"good_type"=>0,
"body"=>"会员订单支付",
"out_trade_no"=>$outTradeNo,
"total_amount"=>$vip_order['vip_order_price'] * 100,
"notify_url"=>$base_url."/api/VipOrder/dyPayCallback",
"image"=>checkHttpForStr($vip['vip_icon'],config("base_config.base_url"))
];
$paymentResult = DyPayService::generateOrder($data);
break;
default:
throw new ValidateException('不支持的支付方式');
}
if(empty($paymentResult) || !is_array($paymentResult)){
throw new ValidateException('支付参数生成失败');
}
// 构建基础返回数据
$responseData = [
'outTradeNo' => $outTradeNo,
'payParams' => $paymentResult,
'clientType' => $client_type
];
// 只有在微信虚拟支付时才返回 paySig 和 signature
if ($client_type == 'miniprogram_virtual') {
// 获取支付签名(paySign)
$paySig = $paymentResult['pay_params']['paySign'] ?? '';
// 👉 3. 组装参数,和你截图完全一致
$midasData = [
'offerId' => '1450523151', // 你的米大师offerId,不变
'buyQuantity' => 1,
'env' => 1, // 0正式环境,1沙箱测试
'currencyType' => 'CNY',
'productId' => $vip_id, // 正确的道具ID,和微信后台完全一致
'goodsPrice' => intval(round($vip['vip_price'] * 100)), // 转成分,符合要求
'outTradeNo' => $outTradeNo,
'attach' => json_encode([ // ✅ 正确!先对内部数组编码一次
'user_id' => $user_id,
'productId' => $vip_id,
'quantity' => 1,
], JSON_UNESCAPED_UNICODE)
];
// ✅ 第一步:URL解码,得到真实密钥
$sessionKey = urldecode($sessionKey);
//$midasData =$this->request->post('signdata', '', 'serach_in');
$signData = json_encode($midasData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 👉 4. 用sessionKey生成签名,这里就是你要的!
$signature = $this->generateUserSignature( $sessionKey,$signData);
// 添加虚拟支付特有字段
$responseData['paySig'] = $paySig;
$responseData['signature'] = $signature;
$responseData['signdata'] =$midasData;
}
return json([
'status' => 200,
'data' => $responseData,
"vip_order" => $vip_order
]);
} catch (\Exception $e) {
throw new ValidateException('支付创建失败: ' . $e->getMessage());
}
}

符合就不会报错了,自认为有啥用?直接贴完整代码片段
{
// 如果signData是数组,转换为JSON字符串
if (is_array($signData)) {
$signData = json_encode($signData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
//var_dump($signData);
// 使用HMAC-SHA256计算签名
$signature = hash_hmac('sha256', (string)$signData, $sessionKey);
// var_dump($signature);
// 返回小写十六进制签名
return strtolower($signature);
} 老师这个是HMAC-SHA256 算法实现,麻烦看看呢
this.$httpApi.createVipOrder({user_id: this.userinfo.user_id,vip_id:this.vipList[this.selectVipIndex].vip_id,client_type:"miniprogram_virtual",sessionKey: uni.getStorageSync('code'),}).then(res=>{let payParam = res.data;console.log(payParam,'payParam=====>')console.log(payParam.signdata,'signData=====>',JSON.stringify(payParam.signdata), payParam.paySig,payParam.signature)wx.requestVirtualPayment({signData: JSON.stringify(payParam.signdata),paySig: payParam.paySig,//"支付签名"signature: payParam.signature,//'用户态签名'mode: 'short_series_goods',success(res) {console.log('支付发起成功,等待发货', res)uni.showToast({title:"支付成功,道具稍后到账",icon:"success",duration:2000});},fail(err){console.error('支付失败', err)if (err.errCode === -15002) {uni.showToast({title:'订单已处理,请勿重复购买',icon: 'none'})} else {uni.showToast({title:'支付失败: ' + err.errMsg,icon: 'error'})}},})})