微信的 h5 支付和 jsapi 支付

🎈 申请商户号

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

🎈 申请商户证书

🎈 设置APIv3密钥

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

echo substr(str_shuffle($chars), 0, $length);

🎈 下载 SDK 开发包

bash 复制代码
# 初始化文件夹
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: 生成后的证书保存地址
bash 复制代码
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 备案查询地址: beian.miit.gov.cn/
  • 关于域名的填写,如果只填写域名不填写具体域名路径,微信在支付的时候就只会校验域名,这也是最方便的,因为域名下有多个项目有支付功能的话,就不需要重复添加了

🎈 H5支付流程

  • H5支付是在微信以外的浏览器使用的,如果是微信内的话,使用的是 jsapi 支付
  • 所以一般用户进入页面的第一件事,就是检测用户使用的环境是微信浏览器还是其他浏览器
  • 前端传一些用户挑选商品后的参数,并请求后端处理接口,后端应该将一些参数进行入库,顺便请求 H5 支付接口
  • 接口应该返回跳转链接 h5_url,如果你想用户付款之后到结果页面,需要添加 redirect_url 参数,这个参数一定要用 encodeURIComponent 进行处理
  • 由于官方在 jssapi 支付中说明,不要相信前端的 success 结果,所以需要在结果页中,让用户自动触发查询结果,因此需要返回后端生成的订单号,用作在结果页的用户手动点击查询
js 复制代码
// 判断是否微信浏览器
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}`))
    });
}
php 复制代码
<?php
// 仅仅用作展示,不可直接复制使用
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;
}
php 复制代码
<?php
// 回调处理,当用户支付订单后,微信会请求该接口,也就是上面在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支付需要使用微信内置的 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
js 复制代码
// 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`)
js 复制代码
// 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}`)
                }
            })
        }
      }
    });
});
php 复制代码
<?php
// 获取用户的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;
}
php 复制代码
<?php
// 仅仅用作展示,不可直接复制使用
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接口列表: pay.weixin.qq.com/wiki/doc/ap...
php 复制代码
<?php
/**
 * 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... 更多接口可根据官方文档列表自行添加
}
php 复制代码
<?php
/**
 * 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;
    }
}
相关推荐
架构师沉默3 分钟前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦20 分钟前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl1 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6861 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情1 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player1 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明2 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展
武超杰2 小时前
Spring Boot入门教程
java·spring boot·后端
IT 行者2 小时前
Spring Boot 集成 JavaMail 163邮箱配置详解
java·spring boot·后端
gelald3 小时前
JVM - 运行时内存模型
java·jvm·后端