收藏
回答

虚拟支付 wx.requestVirtualPayment 持续报错 -15005 SIGNATUR

商户调用虚拟支付接口 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());
    }
}


最后一次编辑于  星期六 17:49
回答关注问题邀请回答
收藏

1 个回答

  • Memory (私信不回复)
    Memory (私信不回复)
    星期六 11:27

    符合就不会报错了,自认为有啥用?直接贴完整代码片段

    星期六 11:27
    有用
    回复 11
    • 且听风雨ᯤ⁶ᴳ
      且听风雨ᯤ⁶ᴳ
      星期六 14:19
      private function generateUserSignature($sessionKey, $signData)
          {
              // 如果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 算法实现,麻烦看看呢
      星期六 14:19
      回复
    • 人生海海 💫
      人生海海 💫
      星期六 14:22
      老师,前端代码如下,辛苦您看看什么问题
      星期六 14:22
      回复
    • Memory (私信不回复)
      Memory (私信不回复)
      星期六 15:58回复且听风雨ᯤ⁶ᴳ
      我需要看完整的代码片段,包含前后端对应这块功能的
      星期六 15:58
      回复
    • Memory (私信不回复)
      Memory (私信不回复)
      星期六 15:58回复人生海海 💫
      我需要看完整的代码片段,包含前后端对应这块功能的
      星期六 15:58
      回复
    • 且听风雨ᯤ⁶ᴳ
      且听风雨ᯤ⁶ᴳ
      星期六 16:34
      老师这个人生海海就是我们的前端代码哦。一个团队的哦。麻烦老师帮忙看看
      星期六 16:34
      回复
    查看更多(6)
登录 后发表内容