• 〔支付接入〕微信的 h5 支付和 jsapi 支付


    🎈 申请商户号#

    • 申请地址: https://pay.weixin.qq.com/
    • 如果你还没有微信商户号,请点击上面的链接进行申请,如果已经有了,可以跳过这一步

    🎈 申请商户证书#

    申请API证书

    🎈 设置APIv3密钥#

    • 首先点击 账户中心API安全设置APIv3密钥设置
    • 会看到有两个密钥,分别是 APIv2密钥APIv3密钥,由于 APIv2密钥 已经逐渐废弃了,所以只需要申请 APIv3密钥 即可
    • 密钥可由数字大小写字母组合,输入任意的 32 位字符,该密钥需要保存好,供后面使用
    申请APIv3密钥
    设置APIv3密钥
    // 生成32位的APIv3随机密钥
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    
    echo substr(str_shuffle($chars), 0, $length);
    

    🎈 下载 SDK 开发包#

    # 初始化文件夹
    composer init
    
    # 推荐使用 PHP 包管理工具 Composer 安装 SDK
    composer require wechatpay/wechatpay
    

    🎈 下载平台证书#

    • 平台证书跟上面申请的商户证书不是同一个东西,在后期请求中,平台证书和商户证书都要带上
    • 上面命令执行完之后,会有一个 vendor/bin/CertificateDownloader.php 文件
    • 如果你是第一次申请平台证书,需要执行命令:php CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
    • -k: apiv3 秘钥,上面自己设置的32位数的密钥
    • -m: 商户号,微信商户平台可以查询
    • -f: 微信商户API私钥文件目录,也就是第二步申请商户证书里面生成的 apiclient_key.pem 路径
    • -s: 证书序列号,在 账户中心API安全管理证书 中可以看见,如果有多个证书,找到自己正在使用的证书序列号
    • -o: 生成后的证书保存地址
    cd vendor/bin/
    
    php CertificateDownloader.php -k 241xxxxxxxxxxxxxxxxx44 -m 1xxxxxxx1 -f ../../cert/merchant/apiclient_key.pem -s Wxxxxxxxxxxxxxxxx4 -o  ../../cert/wechatpay/
    
    获取证书序列号

    🎈 关联 AppID 账号#

    • 因为使用的是微信支付,所以用户支付后,需要通过微信号通知用户支付的一些信息,所以需要在商户号下至少关联一个公众号
    关联公众号

    🎈 开通 H5 支付#

    • 点击 产品中心我的产品H5支付点击开通
    • 开通后,选择 开发配置H5支付域名 申请添加 H5支付域名
    • 申请支付域名需要先做好产品的页面,申请的时候需要有页面的截图,截图中还要 截取到域名,支付的审核算是很严格的,如果申请不过,驳回后再申请,审核通过的时间会越来越长,所以最好一次性就把材料收集好,另外还要域名的备案的 IPC 截图
    • IPC 备案查询地址: https://beian.miit.gov.cn/
    • 关于域名的填写,如果只填写域名不填写具体域名路径,微信在支付的时候就只会校验域名,这也是最方便的,因为域名下有多个项目有支付功能的话,就不需要重复添加了
    开通 H5 支付
    申请支付域名

    🎈 H5支付流程#

    • H5支付是在微信以外的浏览器使用的,如果是微信内的话,使用的是 jsapi 支付
    • 所以一般用户进入页面的第一件事,就是检测用户使用的环境是微信浏览器还是其他浏览器
    • 前端传一些用户挑选商品后的参数,并请求后端处理接口,后端应该将一些参数进行入库,顺便请求 H5 支付接口
    • 接口应该返回跳转链接 h5_url,如果你想用户付款之后到结果页面,需要添加 redirect_url 参数,这个参数一定要用 encodeURIComponent 进行处理
    • 由于官方在 jssapi 支付中说明,不要相信前端的 success 结果,所以需要在结果页中,让用户自动触发查询结果,因此需要返回后端生成的订单号,用作在结果页的用户手动点击查询
    // 判断是否微信浏览器
    function isWeChat() {
        var ua = navigator.userAgent.toLowerCase();
        if (ua.match(/MicroMessenger/i) == 'micromessenger') {
            return true;
        } else {
            return false;
        }
    }
    
    if(isWeChat()) {
        // 是微信中打开的产品页面
        alert('微信内不支持h5支付,请在外部浏览器打开页面');
    } else {
        // 非微信内打开的产品页面,请求接口,获取支付的跳转链接
        // 前端用户选的产品,以及产品的金额,传一些参数过去
        let params = {
            total: 2, // 单位:元
            description: 'Image形象店-深圳腾大-QQ公仔' // 产品的介绍
            // ....更多入库参数
        };
    
        $.getJSON('后端接口地址/h5?' + $.param(params) + '&callback=?', function(res) {
            // 拉起微信支付界面,成功后会跳转到redirect_url链接
            $(location).attr("href", res.data.h5_url + "&redirect_url=" + encodeURIComponent(`https://xxxxxx/finish?out_trade_no=${res.data.out_trade_no}`))
        });
    }
    
    
    // 仅仅用作展示,不可直接复制使用
    require_once('../vendor/autoload.php');
    
    use WeChatPay\Builder;
    use WeChatPay\Crypto\Rsa;
    use WeChatPay\Util\PemUtil;
    
    // 接受参数,相当于原生的$_GET
    $input = $request->only(['name', 'total', 'description', 'phone']);
    
    // 生成商户订单号
    $out_trade_no = getOutTradeNo();
    
    // 处理金额
    // 由于微信使用的是分作为单位,所以前端传的是元的话,需要转换一下
    $total = $input['total'] * 100;
    
    // 商户号
    $merchantId = '1xxxxxx1';
    
    // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
    $merchantPrivateKeyFilePath = 'file://../cert/merchant/apiclient_key.pem';
    $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
    
    // 「商户API证书」的「证书序列号」
    $merchantCertificateSerial = '1xxxxxxxxxxxxxxxxxxxxx91';
    
    // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
    $platformCertificateFilePath = 'file://../cert/wechatpay/wechatpay_4xxxxxxxxxxxxxxxxxxx9.pem';
    $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
    
    // 从「微信支付平台证书」中获取「证书序列号」
    $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
    
    // 构造一个 APIv3 客户端实例
    $instance = Builder::factory([
        'mchid'      => $merchantId,
        'serial'     => $merchantCertificateSerial,
        'privateKey' => $merchantPrivateKeyInstance,
        'certs'      => [
            $platformCertificateSerial => $platformPublicKeyInstance,
        ],
    ]);
    
    
    try {
        $resp = $instance
            ->chain('v3/pay/transactions/h5')
            ->post(['json' => [
                'mchid'        => $merchantId, // 商户号
                'out_trade_no' => $out_trade_no, // 商户订单号
                'appid'        => '********换成跟商户号绑定的公众号APPID**********',
                'description'  => $input['description'], //商品描述
                'notify_url'   => 'https://xxxxx/notify', // 用户支付后的回调地址,在这里修改订单的状态
                'amount'       => [
                    'total'    => $total, // 微信处理的单位是分
                    'currency' => 'CNY'
                ],
                'scene_info' => [
                    'payer_client_ip' => getClientIP(), // 有些框架有自带获取获取客户端IP
                    'h5_info' => [
                        'type' => 'Wap'
                    ]
                ]
            ]]);
    
       // 如果请求成功,需要将一些参数进行入库,这里仅作演示,非正式数据入库
       $response = Db::table('order')->insert([
           'name' => $input['name'],
           'description' => $input['description'],
           'total' => $input['total'],
           'phone' => $input['phone'],
           'trade_state' => 'START',
       ]);
    
       // 入库成功后,将跳转链接和订单号传给前端,前端拿到跳转地址跳转即可
       if($response) {
           return jsonp([
            'code' => 200,
            'msg' => '操作成功',
            'data' => [
                  'out_trade_no' => $out_trade_no,
                  'h5_url' => json_decode($resp->getBody(), true)['h5_url']
               ]
            ]);
       } else {
           return jsonp([
            'code' => 100,
            'msg' => '操作失败'
           ]);
       }
    } catch (\Exception $e) {
        // 进行错误处理
        if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
            $r = $e->getResponse();
            echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        }
    }
    
    
    // 生成唯一商户订单号,订单号不能超过32位,并且在同一个商户下订单号不能重复
    // 如果并发不高,基本这样生成就可以,不会有重复的情况出现的
    function getOutTradeNo()
    {
        $out_trade_no = date('ymdHis') . mt_rand(1000, 9999) . uniqid();
        return mb_substr($out_trade_no, 0, 32);
    }
    
    // 获取客户端的IP
    function getClientIP()
    {
        if (@$_SERVER["HTTP_ALI_CDN_REAL_IP"]) {
            $ip = $_SERVER["HTTP_ALI_CDN_REAL_IP"];
        } elseif (@$_SERVER["HTTP_X_FORWARDED_FOR"] ?: false) {
            $ips = explode(',', $_SERVER["HTTP_X_FORWARDED_FOR"]);
            $ip = $ips[0];
        } elseif (@$_SERVER["HTTP_CDN_SRC_IP"] ?: false) {
            $ip = $_SERVER["HTTP_CDN_SRC_IP"];
        } elseif (getenv('HTTP_CLIENT_IP')) {
            $ip = getenv('HTTP_CLIENT_IP');
        } elseif (getenv('HTTP_X_FORWARDED')) {
            $ip = getenv('HTTP_X_FORWARDED');
        } elseif (getenv('HTTP_FORWARDED_FOR')) {
            $ip = getenv('HTTP_FORWARDED_FOR');
        } elseif (getenv('HTTP_FORWARDED')) {
            $ip = getenv('HTTP_FORWARDED');
        } else {
            $ip = $_SERVER['REMOTE_ADDR'];
        }
    
        $ip = str_replace(['::ffff:', '[', ']'], ['', '', ''], $ip);
        return $ip;
    }
    
    
    // 回调处理,当用户支付订单后,微信会请求该接口,也就是上面在notify_url中填写的接口
    // 在这里我们可以修改订单的状态啥的
    public function notify()
    {
        // 获取参数
        $inBody = file_get_contents('php://input');
    
        // APIv3密钥
        $apiv3Key = 'xxxxxxxxxxxx';
    
        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array)json_decode($inBody, true);
    
        // 加密文本消息解密
        $inBodyResource = AesGcm::decrypt(
            $inBodyArray['resource']['ciphertext'],
            $apiv3Key,
            $inBodyArray['resource']['nonce'],
            $inBodyArray['resource']['associated_data']
        );
    
        // 把解密后的文本转换为PHP Array数组
        $inBodyResourceArray = (array)json_decode($inBodyResource, true);
    
        try {
            // 获取订单信息
            $order = Db::table('order')->where('out_trade_no', $inBodyResourceArray['out_trade_no'])->first();
    
            Db::startTrans();
            if ($order) {
                // 修改order订单的状态
                Db::table('order')->where('id', $order['id'])->update([
                    'openid' => $inBodyResourceArray['payer']['openid'],
                    'trade_state' => $inBodyResourceArray['trade_state']
                ]);
    
                Db::table('payment')->insert([
                     'out_trade_no' => $inBodyResourceArray['out_trade_no'],
                    'transaction_id' => $inBodyResourceArray['transaction_id'],
                    'trade_type' => $inBodyResourceArray['trade_type'],
                    'trade_state' => $inBodyResourceArray['trade_state'],
                    'trade_state_desc' => $inBodyResourceArray['trade_state_desc'],
                    'total_amount' => $inBodyResourceArray['amount']['total'],
                    'bank_type' => $inBodyResourceArray['bank_type'],
                    'success_time' => strtotime($inBodyResourceArray['success_time'])
                ]);
    
                Db::commit();
            } else {
                Db::rollback();
            }
        } catch (\Exception $e) {
            Db::rollback();
        }
    }
    

    🎈 开通 JSAPI 支付#

    • 点击 产品中心我的产品JSAPI支付点击开通
    • 开通后,选择 开发配置JSAPI支付域名 申请添加 JSAPI支付域名
    • 关于申请支付域名的流程基本都差不多要求也差不多,看上面的 H5支付域名 申请就行,这里就不过多赘述了
    开通 JSAPI 支付

    🎈 JSAPI 支付流程#

    • JSAPI支付是在微信内的浏览器使用的,如果用户是在微信外打开的话,需要提醒去微信内打开页面
    • JSAPI支付需要使用微信内置的 WeixinJSBridge.invoke 方法
    • 由于 JSAPI 调用支付需要用到用户的 openid,所以需要想方设法在用户调用 JSAPI 之前获取到 openid点击查看获取 openid 的官方文档
    • 获取用户 openid,需要先获取 code,这个经常做微信业务的人都知道,那么如何在用户无感知的情况下就获取到 openid
    • 思路就是,一般支付最少会有3个页面,这里标注为abc 三个页面,通常是在 a 页面挑选商品,在 b页面确认商品,也就是付款页面,c 页面查询支付状态
    • 由于 code 的存在时间只有5分钟,所以注定 code 获得后不能长时间不使用,也就是说用户一旦在某个页面超过5分钟,这个 code 就失效了,因此最好的方法就是获取 code 后,立马获取 openid
    • 那么就应该设计成从a 页面先跳转到获取 code 页面再跳转到 b 页面,而在 b 页面的一开始就去请求接口,获取用户的 openid 即可
    • 跳转到 b 页面后,链接后自动带上 code参数,链接应该是 https://xxxx/b.html?code=xxxxxxxx
    // a页面,仅做逻辑演示,更加具体的逻辑需要自己完善
    // 判断是否微信浏览器
    function isWeChat() {
        var ua = navigator.userAgent.toLowerCase();
        if (ua.match(/MicroMessenger/i) == 'micromessenger') {
            return true;
        } else {
            return false;
        }
    }
    
    if(!isWeChat()) {
        // 非微信内打开的产品页面
        alert('微信外不支持JSAPI支付,请在微信中打开页面');
        return false;
    }
    
    // 用户挑选完商品后跳转,这里appid需要上面跟商户绑定的公众号appid
    // 微信授权分为静默授权和非静默授权,其中非静默授权,需要用户点击确认授权后,才可以获取code,
    // 因为这里主打一个用户无感知,而且我们只需要openid即可,所以我们只需要使用静默授权即可
    // 静默授权可以获取用户更多的信息,比如头像、昵称等,而静默授权只能获取openid,这点需要注意,具体情况选择不同
    // 非静默授权
    // $(location).attr('href', `https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxx&redirect_uri=${encodeURIComponent('https://xxxx/b.html')}&response_type=code&scope=snsapi_userinfo#wechat_redirect`)
    // 静默授权
    $(location).attr('href', `https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxx&redirect_uri=${encodeURIComponent('https://xxxx/b.html')}&response_type=code&scope=snsapi_base#wechat_redirect`)
    
    // b页面,仅做逻辑演示,更加具体的逻辑需要自己完善
    let openid = '';
    
    // 获取code, 请求接口获取openid
    function getParamUrl(name, url) {
      if (!url) url = location.href;
      if (url.indexOf('?') == -1) return '';
    
      try {
        var re = new RegExp("" + name + "=([^&?]*)", "ig");
        return ((url.match(re)) ? (decodeURIComponent(url.match(re)[0].substr(name.length + 1))) : '');
      } catch (_e) {
        return '';
      }
    }
    
    let code = getParamUrl('code');
    
    $.getJSON('后端接口地址/openid?callback=?', function(res) {
        if(res.code == 200) {
            openid = res.data;
        } else {
            console.error(res.msg);
        }
    })
    
    // 用户确定订单后,拉起支付
    let params = {
        total: 2, // 单位:元
        description: 'Image形象店-深圳腾大-QQ公仔', // 产品的介绍
        openid: openid //用户的openid
        // ....更多入库参数
    };
    
    $.getJSON('后端接口地址/jssapi?' + $.param(params) + '&callback=?', function(res) {
        WeixinJSBridge.invoke('getBrandWCPayRequest', {
          'appId': res.data.sign.appId,
          'timeStamp': res.data.sign.timeStamp,
          'nonceStr': res.data.sign.nonceStr,
          'package': res.data.sign.package,
          'signType': res.data.sign.signType,
          'paySign': res.data.sign.paySign
        }, function (response) {
          if (response.err_msg == "get_brand_wcpay_request:ok") {
            $(location).attr("href", `https://xxxxxx/finish?out_trade_no=${res.data.out_trade_no}`)
          } else {
            // 有些用户调起了支付,但是未付款取消的处理方式,你可以给他简单简单提示
            toast('支付异常取消')
    
            // 当然有些用户是误操作,你可以提醒二次支付
            if(confirm('检测到你操作有误,是否重新支付?')) {
                WeixinJSBridge.invoke('getBrandWCPayRequest', {
                      'appId': res.data.sign.appId,
                      'timeStamp': res.data.sign.timeStamp,
                      'nonceStr': res.data.sign.nonceStr,
                      'package': res.data.sign.package,
                      'signType': res.data.sign.signType,
                      'paySign': res.data.sign.paySign
                    }, function (response) {
                    if (response.err_msg == "get_brand_wcpay_request:ok") {
                        $(location).attr("href", `https://xxxxxx/finish?out_trade_no=${res.data.out_trade_no}`)
                    }
                })
            }
          }
        });
    });
    
    
    // 获取用户的openid
    
    $input = $request->only(['code']);
    
    $response = getCurl("https://api.weixin.qq.com/sns/oauth2/access_token?appid={$this->appid}&secret={$this->secret}&code={$input['code']}&grant_type=authorization_code");
    
    $openid = json_decode($response, true)['openid'];
    
    // 返回openid
    return jsonp([
        'code' => 200,
        'msg' => '获取成功',
        'data' => $openid
    ]);
    
    
    // 封装的GET请求
    function getCurl($url, $timeout = 5)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        $result = curl_exec($ch);
        curl_close($ch);
    
        return $result;
    }
    
    
    // 仅仅用作展示,不可直接复制使用
    require_once('../vendor/autoload.php');
    
    use WeChatPay\Builder;
    use WeChatPay\Formatter;
    use WeChatPay\Crypto\Rsa;
    use WeChatPay\Util\PemUtil;
    
    // 接受参数,相当于原生的$_GET,这里会比h5支付多一个openid
    $input = $request->only(['openid', 'name', 'total', 'description', 'phone']);
    
    // 生成商户订单号
    $out_trade_no = getOutTradeNo();
    
    // 处理金额
    // 由于微信使用的是分作为单位,所以前端传的是元的话,需要转换一下
    $total = $input['total'] * 100;
    
    // 商户号
    $merchantId = '1xxxxxx1';
    
    // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
    $merchantPrivateKeyFilePath = 'file://../cert/merchant/apiclient_key.pem';
    $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
    
    // 「商户API证书」的「证书序列号」
    $merchantCertificateSerial = '1xxxxxxxxxxxxxxxxxxxxx91';
    
    // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
    $platformCertificateFilePath = 'file://../cert/wechatpay/wechatpay_4xxxxxxxxxxxxxxxxxxx9.pem';
    $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
    
    // 从「微信支付平台证书」中获取「证书序列号」
    $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
    
    // 构造一个 APIv3 客户端实例
    $instance = Builder::factory([
        'mchid'      => $merchantId,
        'serial'     => $merchantCertificateSerial,
        'privateKey' => $merchantPrivateKeyInstance,
        'certs'      => [
            $platformCertificateSerial => $platformPublicKeyInstance,
        ],
    ]);
    
    
    try {
        // 调用 transactions/jsapi 接口后会生成prepay_id
        $resp = $this->instance()
            ->chain('v3/pay/transactions/jsapi')
            ->post(['json' => [
                'mchid'        => $merchantId, // 商户号
                'out_trade_no' => $out_trade_no, // 商户订单号
                'appid'        => '********换成跟商户号绑定的公众号APPID**********',
                'description'  => $input['description'], //商品描述
                'notify_url'   => 'https://xxxxx/notify', // 用户支付后的回调地址,在这里修改订单的状态
                'amount' => [
                    'total' => $total,
                    'currency' => 'CNY'
                ],
                'payer' => [
                    'openid' => $input['openid']
                ]
            ]]);
            
        // 需要根据prepay_id去生成加密的信息
        $prepay_id = json_decode($resp->getBody(), true)['prepay_id'];
        $sign = getSign($prepay_id);
            
       // 如果请求成功,需要将一些参数进行入库,这里仅作演示,非正式数据入库
       $response = Db::table('order')->insert([
           'openid' => $input['openid'],
           'name' => $input['name'],
           'description' => $input['description'],
           'total' => $input['total'],
           'phone' => $input['phone'],
           'trade_state' => 'START',
       ]);
       
       // 入库成功后,将跳转链接和订单号传给前端,前端拿到跳转地址跳转即可
       if($response) {
           return jsonp([
            'code' => 200,
            'msg' => '操作成功',
            'data' => [
                  'out_trade_no' => $out_trade_no,
                  'sign' => $sign
               ]
            ]);
       } else {
           return jsonp([
            'code' => 100,
            'msg' => '操作失败'
           ]);
       }
    } catch (\Exception $e) {
        // 进行错误处理
        if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
            $r = $e->getResponse();
            echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        }
    }
    
    // 获取加密参数
    function getSign($prepay_id)
    {
        $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyFilePath);
    
        $params = [
            'appId' => $this->appid,
            'timeStamp' => (string)Formatter::timestamp(),
            'nonceStr' => Formatter::nonce(),
            'package' => 'prepay_id=' . $prepay_id,
        ];
    
        $params += ['paySign' => Rsa::sign(
            Formatter::joinedByLineFeed(...array_values($params)),
            $merchantPrivateKeyInstance
        ), 'signType' => 'RSA'];
    
        return $params;
    }
    

    🎈 通用微信支付库封装#

    • 由于直接使用微信的支付库,代码非常的匀余,所以封装了一个微信支付库
    • 由于只针对一些业务的 api封装,所以肯定不全,需要的可以自己添加需要的api
    • 微信支付API接口列表: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml
    
    /**
     * User: tinygeeker
     * Desc: 微信支付库封装
     * Date: 2023/08/10
     */
    
    namespace App;
    
    use App\Helper;
    use WeChatPay\Builder;
    use WeChatPay\Formatter;
    use WeChatPay\Crypto\Rsa;
    use WeChatPay\Util\PemUtil;
    
    class WxPay
    {
        // appid
        private $appid;
    
        // 商户号
        private $merchantId;
    
        // 商户API私钥
        private $merchantPrivateKeyFilePath;
    
        // 证书序列号
        private $merchantCertificateSerial;
    
        // 微信支付平台证书
        private $platformCertificateFilePath;
    
        /**
         * @param $appid
         * @param $merchantId
         * @param $merchantCertificateSerial
         */
        public function __construct($appid = '', $merchantId = '', $merchantCertificateSerial = '')
        {
            $this->appid = $appid ?: '换成自己的APPID';
            $this->merchantId = $merchantId ?: '换成自己的商户号';
            $this->merchantCertificateSerial = $merchantCertificateSerial ?: '换成自己的证书序列号';
    
            $this->merchantPrivateKeyFilePath = 'file:///common/cert/merchant/apiclient_key.pem'; // 换成自己的
            $this->platformCertificateFilePath = 'file:///common/cert/wechatpay/wechatpay_xxx.pem'; // 换成自己的
        }
    
        /**
         * @return \WeChatPay\BuilderChainable
         */
        protected function instance()
        {
            $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
            $platformPublicKeyInstance = Rsa::from($this->platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
    
            $platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateFilePath);
    
            $instance = Builder::factory([
                'mchid' => $this->merchantId,
                'serial' => $this->merchantCertificateSerial,
                'privateKey' => $merchantPrivateKeyInstance,
                'certs' => [
                    $platformCertificateSerial => $platformPublicKeyInstance,
                ],
            ]);
    
            return $instance;
        }
    
        public function getSign($prepay_id)
        {
            $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyFilePath);
    
            $params = [
                'appId' => $this->appid,
                'timeStamp' => (string)Formatter::timestamp(),
                'nonceStr' => Formatter::nonce(),
                'package' => 'prepay_id=' . $prepay_id,
            ];
    
            $params += ['paySign' => Rsa::sign(
                Formatter::joinedByLineFeed(...array_values($params)),
                $merchantPrivateKeyInstance
            ), 'signType' => 'RSA'];
    
            return $params;
        }
    
        public function checkOutTradeNo($out_trade_no)
        {
            try {
                $resp = $this->instance()
                    ->v3->pay->transactions->outTradeNo->_out_trade_no_
                    ->get([
                        // Query 参数
                        'query' => ['mchid' => $this->merchantId],
                        // 变量名 => 变量值
                        'out_trade_no' => $out_trade_no,
                    ]);
    
                return $resp->getBody();
            } catch (\Exception $e) {
                // 进行错误处理
                echo $e->getMessage(), PHP_EOL;
                if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                    $r = $e->getResponse();
                    echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
                    echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
                }
                echo $e->getTraceAsString(), PHP_EOL;
            }
        }
        
        // h5下单
        public function h5($total, $out_trade_no, $description, $notify_url)
        {
            try {
                $resp = $this->instance()
                    ->chain('v3/pay/transactions/h5')
                    ->post(['json' => [
                        'mchid' => $this->merchantId,
                        'out_trade_no' => $out_trade_no,
                        'appid' => $this->appid,
                        'description' => $description,
                        'notify_url' => $notify_url,
                        'amount' => [
                            'total' => $total,
                            'currency' => 'CNY'
                        ],
                        'scene_info' => [
                            'payer_client_ip' => Helper::getClientIp(),
                            'h5_info' => [
                                'type' => 'Wap'
                            ]
                        ]
                    ]]);
    
                return $resp->getBody();
            } catch (\Exception $e) {
                // 进行错误处理
                echo $e->getMessage(), PHP_EOL;
                if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                    $r = $e->getResponse();
                    echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
                    echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
                }
                echo $e->getTraceAsString(), PHP_EOL;
            }
        }
        
        // jsapi下单
        public function jsapi($openid, $total, $out_trade_no, $description, $notify_url)
        {
            try {
                $resp = $this->instance()
                    ->chain('v3/pay/transactions/jsapi')
                    ->post(['json' => [
                        'mchid' => $this->merchantId,
                        'out_trade_no' => $out_trade_no,
                        'appid' => $this->appid,
                        'description' => $description,
                        'notify_url' => $notify_url,
                        'amount' => [
                            'total' => $total,
                            'currency' => 'CNY'
                        ],
                        'payer' => [
                            'openid' => $openid
                        ]
                    ]]);
    
                return $resp->getBody();
            } catch (\Exception $e) {
                // 进行错误处理
                echo $e->getMessage(), PHP_EOL;
                if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                    $r = $e->getResponse();
                    echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
                    echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
                }
                echo $e->getTraceAsString(), PHP_EOL;
            }
        }
        
        
       // todo... 更多接口可根据官方文档列表自行添加
    }
    
    
    /**
     * User: tinygeeker
     * Desc: 工具库
     * Date: 2023/08/10
     */
     
    namespace App;
    
    class Helper
    {
        /**
         * @return array|mixed|string|string[]
         */
        static public function getClientIP()
        {
            if (@$_SERVER["HTTP_ALI_CDN_REAL_IP"]) {
                $ip = $_SERVER["HTTP_ALI_CDN_REAL_IP"];
            } elseif (@$_SERVER["HTTP_X_FORWARDED_FOR"] ?: false) {
                $ips = explode(',', $_SERVER["HTTP_X_FORWARDED_FOR"]);
                $ip = $ips[0];
            } elseif (@$_SERVER["HTTP_CDN_SRC_IP"] ?: false) {
                $ip = $_SERVER["HTTP_CDN_SRC_IP"];
            } elseif (getenv('HTTP_CLIENT_IP')) {
                $ip = getenv('HTTP_CLIENT_IP');
            } elseif (getenv('HTTP_X_FORWARDED')) {
                $ip = getenv('HTTP_X_FORWARDED');
            } elseif (getenv('HTTP_FORWARDED_FOR')) {
                $ip = getenv('HTTP_FORWARDED_FOR');
            } elseif (getenv('HTTP_FORWARDED')) {
                $ip = getenv('HTTP_FORWARDED');
            } else {
                $ip = $_SERVER['REMOTE_ADDR'];
            }
    
            $ip = str_replace(['::ffff:', '[', ']'], ['', '', ''], $ip);
            return $ip;
        }
    
        /**
         * @param $length
         * @param $type
         * @return false|string
         */
        static public function createRandomStr($length = 32, $type = 0)
        {
            switch ($type) {
                case 1:
                    $chars = '0123456789';
                    break;
                case 2:
                    $chars = 'abcdefghijklmnopqrstuvwxyz';
                    break;
                case 3:
                    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
                    break;
                case 4:
                    $chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
                    break;
                case 5:
                    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
                    break;
                default:
                    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
                    break;
            }
    
            return substr(str_shuffle($chars), 0, $length);
        }
    
        /**
         * @param $url
         * @param $timeout
         * @return bool|string
         */
        static public function getCurl($url, $timeout = 5)
        {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_HEADER, 0);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
            $result = curl_exec($ch);
            curl_close($ch);
    
            return $result;
        }
    
        /**
         * @param $url
         * @param $data
         * @param $header
         * @param $timeout
         * @return bool|string
         */
        static public function postCurl($url, $data, $header = [], $timeout = 5)
        {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            if ($header) {
                curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
            }
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
            $result = curl_exec($ch);
            curl_close($ch);
    
            return $result;
        }
    }
    
  • 相关阅读:
    顺序表详解(SeqList)
    java计算机毕业设计食品销售网站源码+数据库+系统+lw文档+mybatis+运行部署
    微信小程序实现课程表
    AUTOSAR从入门到精通-基于 CAN 总线的汽车发电机智能调节器(中)
    学习记录十六 ---- spring boot
    阿里技术官首发的Java核心框架指导手册,为了大厂得码住学起来~
    按照C++语言程序结构组成数字电路进行计算的计算机
    SpringMVC初级的概念和常用注解(入门案例)
    如何处理消费过程中的重复消息
    一道[CSCCTF 2019 Qual]FlaskLight的详解再遇SSTI
  • 原文地址:https://www.cnblogs.com/tinygeeker/p/17620498.html