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

🎈 申请商户号

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

🎈 申请商户证书

  • 首先点击 账户中心API安全申请API证书
  • 申请详细步骤: https://kf.qq.com/faq/161222NneAJf161222U7fARv.html
申请API证书

🎈 设置APIv3密钥

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

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

🎈 下载 SDK 开发包

  • 微信官方提供了 JAVAPHPGO 三种语言版本的开发库,请根据自己开发语言选择
  • JAVA语言: wechatpay-java ‹推荐›、wechatpay-apache-httpclient
  • PHP语言: wechatpay-php ‹推荐›、wechatpay-guzzle-middleware
  • GO语言: wechatpay-go ‹推荐›
  • 由于 php 实现支付相对简单,所以我将以 php 作为支付的讲解
  • 首先使用 composer 安装 sdk
代码语言:shell
复制
# 初始化文件夹
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: 生成后的证书保存地址
代码语言:shell
复制
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 结果,所以需要在结果页中,让用户自动触发查询结果,因此需要返回后端生成的订单号,用作在结果页的用户手动点击查询
代码语言:javascript
复制
// 判断是否微信浏览器
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(&#39;后端接口地址/h5?&#39; + $.param(params) + &#39;&amp;callback=?&#39;, function(res) {
    // 拉起微信支付界面,成功后会跳转到redirect_url链接
    $(location).attr(&#34;href&#34;, res.data.h5_url + &#34;&amp;redirect_url=&#34; + 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 =&gt; 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 &amp;&amp; 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(&#39;,&#39;, _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([&#39;::ffff:&#39;, &#39;[&#39;, &#39;]&#39;], [&#39;&#39;, &#39;&#39;, &#39;&#39;], $ip);
return $ip;

}

代码语言:php
复制
<?php
// 回调处理,当用户支付订单后,微信会请求该接口,也就是上面在notify_url中填写的接口
// 在这里我们可以修改订单的状态啥的
public function notify()
{
// 获取参数
$inBody = file_get_contents('php://input');

// APIv3密钥
$apiv3Key = &#39;xxxxxxxxxxxx&#39;;

// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($inBody, true);

// 加密文本消息解密
$inBodyResource = AesGcm::decrypt(
    $inBodyArray[&#39;resource&#39;][&#39;ciphertext&#39;],
    $apiv3Key,
    $inBodyArray[&#39;resource&#39;][&#39;nonce&#39;],
    $inBodyArray[&#39;resource&#39;][&#39;associated_data&#39;]
);

// 把解密后的文本转换为PHP Array数组
$inBodyResourceArray = (array)json_decode($inBodyResource, true);

try {
    // 获取订单信息
    $order = Db::table(&#39;order&#39;)-&gt;where(&#39;out_trade_no&#39;, $inBodyResourceArray[&#39;out_trade_no&#39;])-&gt;first();

    Db::startTrans();
    if ($order) {
        // 修改order订单的状态
        Db::table(&#39;order&#39;)-&gt;where(&#39;id&#39;, $order[&#39;id&#39;])-&gt;update([
            &#39;openid&#39; =&gt; $inBodyResourceArray[&#39;payer&#39;][&#39;openid&#39;],
            &#39;trade_state&#39; =&gt; $inBodyResourceArray[&#39;trade_state&#39;]
        ]);
        
        
        Db::table(&#39;payment&#39;)-&gt;insert([
             &#39;out_trade_no&#39; =&gt; $inBodyResourceArray[&#39;out_trade_no&#39;],
            &#39;transaction_id&#39; =&gt; $inBodyResourceArray[&#39;transaction_id&#39;],
            &#39;trade_type&#39; =&gt; $inBodyResourceArray[&#39;trade_type&#39;],
            &#39;trade_state&#39; =&gt; $inBodyResourceArray[&#39;trade_state&#39;],
            &#39;trade_state_desc&#39; =&gt; $inBodyResourceArray[&#39;trade_state_desc&#39;],
            &#39;total_amount&#39; =&gt; $inBodyResourceArray[&#39;amount&#39;][&#39;total&#39;],
            &#39;bank_type&#39; =&gt; $inBodyResourceArray[&#39;bank_type&#39;],
            &#39;success_time&#39; =&gt; strtotime($inBodyResourceArray[&#39;success_time&#39;])
        ]);

        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
代码语言:javascript
复制
// 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(&#39;href&#39;, `https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxx&amp;redirect_uri={encodeURIComponent('https://xxxx/b.html')}&response_type=code&scope=snsapi_userinfo#wechat_redirect) // 静默授权 $(location).attr(&#39;href&#39;, 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`)

代码语言:javascript
复制
// 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(&#39;后端接口地址/jssapi?&#39; + .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(&#34;href&#34;, `https://xxxxxx/finish?out_trade_no={res.data.out_trade_no}`)
} else {
// 有些用户调起了支付,但是未付款取消的处理方式,你可以给他简单简单提示
toast('支付异常取消')

    // 当然有些用户是误操作,你可以提醒二次支付
    if(confirm(&#39;检测到你操作有误,是否重新支付?&#39;)) {
        WeixinJSBridge.invoke(&#39;getBrandWCPayRequest&#39;, {
              &#39;appId&#39;: res.data.sign.appId,
              &#39;timeStamp&#39;: res.data.sign.timeStamp,
              &#39;nonceStr&#39;: res.data.sign.nonceStr,
              &#39;package&#39;: res.data.sign.package,
              &#39;signType&#39;: res.data.sign.signType,
              &#39;paySign&#39;: res.data.sign.paySign
            }, function (response) {
            if (response.err_msg == &#34;get_brand_wcpay_request:ok&#34;) {
                $(location).attr(&#34;href&#34;, `https://xxxxxx/finish?out_trade_no=${res.data.out_trade_no}`)
            }
        })
    }
  }
});

});

代码语言:php
复制
<?php
// 获取用户的openid

input = request->only(['code']);

response = getCurl(&#34;https://api.weixin.qq.com/sns/oauth2/access_token?appid={this->appid}&secret={this-&gt;secret}&amp;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 =&gt; 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-&gt;getBody(), true)[&#39;prepay_id&#39;];
$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 &amp;&amp; e->hasResponse()) {
r = e->getResponse();
echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
}

// 获取加密参数
function getSign($prepay_id)
{
merchantPrivateKeyInstance = Rsa::from(this->merchantPrivateKeyFilePath);

$params = [
    &#39;appId&#39; =&gt; $this-&gt;appid,
    &#39;timeStamp&#39; =&gt; (string)Formatter::timestamp(),
    &#39;nonceStr&#39; =&gt; Formatter::nonce(),
    &#39;package&#39; =&gt; &#39;prepay_id=&#39; . $prepay_id,
];

$params += [&#39;paySign&#39; =&gt; Rsa::sign(
    Formatter::joinedByLineFeed(...array_values($params)),
    $merchantPrivateKeyInstance
), &#39;signType&#39; =&gt; &#39;RSA&#39;];

return $params;

}

🎈 通用微信支付库封装

  • 由于直接使用微信的支付库,代码非常的匀余,所以封装了一个微信支付库
  • 由于只针对一些业务的 api封装,所以肯定不全,需要的可以自己添加需要的api
  • 微信支付API接口列表: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml
代码语言: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 = &#39;&#39;, $merchantId = &#39;&#39;, $merchantCertificateSerial = &#39;&#39;)
{
    $this-&gt;appid = $appid ?: &#39;换成自己的APPID&#39;;
    $this-&gt;merchantId = $merchantId ?: &#39;换成自己的商户号&#39;;
    $this-&gt;merchantCertificateSerial = $merchantCertificateSerial ?: &#39;换成自己的证书序列号&#39;;

    $this-&gt;merchantPrivateKeyFilePath = &#39;file:///common/cert/merchant/apiclient_key.pem&#39;; // 换成自己的
    $this-&gt;platformCertificateFilePath = &#39;file:///common/cert/wechatpay/wechatpay_xxx.pem&#39;; // 换成自己的
}

/**
 * @return \WeChatPay\BuilderChainable
 */
protected function instance()
{
    $merchantPrivateKeyInstance = Rsa::from($this-&gt;merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
    $platformPublicKeyInstance = Rsa::from($this-&gt;platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

    $platformCertificateSerial = PemUtil::parseCertificateSerialNo($this-&gt;platformCertificateFilePath);

    $instance = Builder::factory([
        &#39;mchid&#39; =&gt; $this-&gt;merchantId,
        &#39;serial&#39; =&gt; $this-&gt;merchantCertificateSerial,
        &#39;privateKey&#39; =&gt; $merchantPrivateKeyInstance,
        &#39;certs&#39; =&gt; [
            $platformCertificateSerial =&gt; $platformPublicKeyInstance,
        ],
    ]);

    return $instance;
}

public function getSign($prepay_id)
{
    $merchantPrivateKeyInstance = Rsa::from($this-&gt;merchantPrivateKeyFilePath);

    $params = [
        &#39;appId&#39; =&gt; $this-&gt;appid,
        &#39;timeStamp&#39; =&gt; (string)Formatter::timestamp(),
        &#39;nonceStr&#39; =&gt; Formatter::nonce(),
        &#39;package&#39; =&gt; &#39;prepay_id=&#39; . $prepay_id,
    ];

    $params += [&#39;paySign&#39; =&gt; Rsa::sign(
        Formatter::joinedByLineFeed(...array_values($params)),
        $merchantPrivateKeyInstance
    ), &#39;signType&#39; =&gt; &#39;RSA&#39;];

    return $params;
}

public function checkOutTradeNo($out_trade_no)
{
    try {
        $resp = $this-&gt;instance()
            -&gt;v3-&gt;pay-&gt;transactions-&gt;outTradeNo-&gt;_out_trade_no_
            -&gt;get([
                // Query 参数
                &#39;query&#39; =&gt; [&#39;mchid&#39; =&gt; $this-&gt;merchantId],
                // 变量名 =&gt; 变量值
                &#39;out_trade_no&#39; =&gt; $out_trade_no,
            ]);

        return $resp-&gt;getBody();
    } catch (\Exception $e) {
        // 进行错误处理
        echo $e-&gt;getMessage(), PHP_EOL;
        if ($e instanceof \GuzzleHttp\Exception\RequestException &amp;&amp; $e-&gt;hasResponse()) {
            $r = $e-&gt;getResponse();
            echo $r-&gt;getStatusCode() . &#39; &#39; . $r-&gt;getReasonPhrase(), PHP_EOL;
            echo $r-&gt;getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        }
        echo $e-&gt;getTraceAsString(), PHP_EOL;
    }
}

// h5下单
public function h5($total, $out_trade_no, $description, $notify_url)
{
    try {
        $resp = $this-&gt;instance()
            -&gt;chain(&#39;v3/pay/transactions/h5&#39;)
            -&gt;post([&#39;json&#39; =&gt; [
                &#39;mchid&#39; =&gt; $this-&gt;merchantId,
                &#39;out_trade_no&#39; =&gt; $out_trade_no,
                &#39;appid&#39; =&gt; $this-&gt;appid,
                &#39;description&#39; =&gt; $description,
                &#39;notify_url&#39; =&gt; $notify_url,
                &#39;amount&#39; =&gt; [
                    &#39;total&#39; =&gt; $total,
                    &#39;currency&#39; =&gt; &#39;CNY&#39;
                ],
                &#39;scene_info&#39; =&gt; [
                    &#39;payer_client_ip&#39; =&gt; Helper::getClientIp(),
                    &#39;h5_info&#39; =&gt; [
                        &#39;type&#39; =&gt; &#39;Wap&#39;
                    ]
                ]
            ]]);

        return $resp-&gt;getBody();
    } catch (\Exception $e) {
        // 进行错误处理
        echo $e-&gt;getMessage(), PHP_EOL;
        if ($e instanceof \GuzzleHttp\Exception\RequestException &amp;&amp; $e-&gt;hasResponse()) {
            $r = $e-&gt;getResponse();
            echo $r-&gt;getStatusCode() . &#39; &#39; . $r-&gt;getReasonPhrase(), PHP_EOL;
            echo $r-&gt;getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        }
        echo $e-&gt;getTraceAsString(), PHP_EOL;
    }
}

// jsapi下单
public function jsapi($openid, $total, $out_trade_no, $description, $notify_url)
{
    try {
        $resp = $this-&gt;instance()
            -&gt;chain(&#39;v3/pay/transactions/jsapi&#39;)
            -&gt;post([&#39;json&#39; =&gt; [
                &#39;mchid&#39; =&gt; $this-&gt;merchantId,
                &#39;out_trade_no&#39; =&gt; $out_trade_no,
                &#39;appid&#39; =&gt; $this-&gt;appid,
                &#39;description&#39; =&gt; $description,
                &#39;notify_url&#39; =&gt; $notify_url,
                &#39;amount&#39; =&gt; [
                    &#39;total&#39; =&gt; $total,
                    &#39;currency&#39; =&gt; &#39;CNY&#39;
                ],
                &#39;payer&#39; =&gt; [
                    &#39;openid&#39; =&gt; $openid
                ]
            ]]);

        return $resp-&gt;getBody();
    } catch (\Exception $e) {
        // 进行错误处理
        echo $e-&gt;getMessage(), PHP_EOL;
        if ($e instanceof \GuzzleHttp\Exception\RequestException &amp;&amp; $e-&gt;hasResponse()) {
            $r = $e-&gt;getResponse();
            echo $r-&gt;getStatusCode() . &#39; &#39; . $r-&gt;getReasonPhrase(), PHP_EOL;
            echo $r-&gt;getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        }
        echo $e-&gt;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(&#39;,&#39;, _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([&#39;::ffff:&#39;, &#39;[&#39;, &#39;]&#39;], [&#39;&#39;, &#39;&#39;, &#39;&#39;], $ip);
    return $ip;
}

/**
 * @param $length
 * @param $type
 * @return false|string
 */
static public function createRandomStr($length = 32, $type = 0)
{
    switch ($type) {
        case 1:
            $chars = &#39;0123456789&#39;;
            break;
        case 2:
            $chars = &#39;abcdefghijklmnopqrstuvwxyz&#39;;
            break;
        case 3:
            $chars = &#39;ABCDEFGHIJKLMNOPQRSTUVWXYZ&#39;;
            break;
        case 4:
            $chars = &#39;abcdefghijklmnopqrstuvwxyz0123456789&#39;;
            break;
        case 5:
            $chars = &#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&#39;;
            break;
        default:
            $chars = &#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&#39;;
            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;
}

}