之前的网站涉及到微信支付都是用的v2版本,好久也没更新了,没出问题也没去管,最近新做了一个项目,发现V3都出了好久了,然后就去研究了一下,踩了一点坑,记录一下。(注意:百度出来的大部分都是扯淡的)
老规矩,先看文档( 这里强调一下前提:你要有一个已认证的公众号或小程序,同时要开通了微信支付的功能 )
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_0.shtml
我是先做的PC端的支付,所以先对接的Native支付:
1.先去微信支付的控制台设置密钥;然后申请证书,申请好后将压缩包解压到网站根目录里(或者服务器上,保证路径是对的即可);
然后设置支付回调域名;要记住mchid(商户号),后续会用到。
2.去微信公众平台设置app_id,app_secret(jsapi支付需要用到),设置授权域名、js安全域名,IP白名单;
3.根据文档进行对接:
主要参考这个:https://github.com/wechatpay-apiv3/wechatpay-php
安装比较简单,composer require wechatpay/wechatpay
4.安装完成后,在需要用到支付的控制器里引入:
我是放在了一个单独的控制器里:
<?php
namespace app\index\controller;
use think\Controller;
use think\facade\Config;
use think\facade\Session;
use think\facade\Env;
use think\Db;
class Common extends Controller
{
//微信支付
public function wxpay($id){
$row = Db::name('order')->where(['id'=>$id])->find();
$data = [];
$data['out_trade_no'] = $row['order_number'];
$data['id'] = $row['id'];
$data['description'] = $row['description];
$data['total'] = $row['price]*100;//单位:分
//调用微信nativePay支付
$res = nativePay($data);
//生成付款二维码
if($res['code'] == 200){
$code = getQRCode($id, $res['data']['code_url']);
}else{
$error = 1;
}
//这里判断二维码是否生成
if(!file_exists(Env::get('root_path') .'public'.$code)){
$error = 1;
}
return ['code'=>$code,'error'=>$error];
}
}
然后支付回调和相关的方法写到Payments.php里
<?php
namespace app\index\controller;
use app\ygbxapi\controller\Api;
use think\Controller;
use think\Db;
use think\facade\Env;
use think\facade\Session;
use think\facade\Request;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Formatter;
class Payments extends Common
{
/**
* @function wxpayNotifyCallback
* @intro 微信支付回调
* @return string
*/
public function wxpayNotifyCallback()
{
//这里是用的TP方法
$inWechatpaySignature = Request::header('Wechatpay-Signature');// 请根据实际情况获取
$inWechatpayTimestamp = Request::header('Wechatpay-Timestamp');// 请根据实际情况获取
$inWechatpaySerial = Request::header('Wechatpay-Serial');// 请根据实际情况获取
$inWechatpayNonce = Request::header('Wechatpay-Nonce');// 请根据实际情况获取
$inBody = file_get_contents('php://input');// 请根据实际情况获取,例如: file_get_contents('php://input');
$apiv3Key = config('app.wxpay.key');// 在商户平台上设置的APIv3密钥
// 根据通知的平台证书序列号,查询本地平台证书文件,
// 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
$platformPublicKeyInstance = Rsa::from('file://'.config('app.wxpay.platform_key_path'), Rsa::KEY_TYPE_PUBLIC);
// 检查通知时间偏移量,允许5分钟之内的偏移
$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
$verifiedStatus = Rsa::verify(
// 构造验签名串
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature,
$platformPublicKeyInstance
);
if ($timeOffsetStatus && $verifiedStatus) {
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($inBody, true);
// 使用PHP7的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
// 加密文本消息解密
$inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
$inBodyResourceArray = (array)json_decode($inBodyResource, true);
$notfiyOutput = [];
$notfiyOutput['appid'] = $inBodyResourceArray['appid'];
$notfiyOutput['attach'] = $inBodyResourceArray['attach'];
$notfiyOutput['bank_type'] = $inBodyResourceArray['bank_type'];
$notfiyOutput['payer_total'] = $inBodyResourceArray['amount']['payer_total'];
$notfiyOutput['currency'] = $inBodyResourceArray['amount']['currency'];
$notfiyOutput['payer_currency'] = $inBodyResourceArray['amount']['payer_currency'];
$notfiyOutput['mch_id'] = $inBodyResourceArray['mchid'];
$notfiyOutput['openid'] = $inBodyResourceArray['payer']['openid'];
$notfiyOutput['out_trade_no'] = $inBodyResourceArray['out_trade_no'];
$notfiyOutput['trade_state'] = $inBodyResourceArray['trade_state'];
$notfiyOutput['success_time'] = strtotime($inBodyResourceArray['success_time']);
$notfiyOutput['trade_state_desc'] = $inBodyResourceArray['trade_state_desc'];
$notfiyOutput['total_fee'] = $inBodyResourceArray['amount']['total'];
$notfiyOutput['trade_type'] = $inBodyResourceArray['trade_type'];
$notfiyOutput['transaction_id'] = $inBodyResourceArray['transaction_id'];
//回调信息存入数据库
$row = Db::name('wxpay_record')->where(['transaction_id'=>$inBodyResourceArray['transaction_id']])->find();
if($row){
if($row['trade_state'] != 'SUCCESS'){
$id = Db::name('wxpay_record')->where(['transaction_id'=>$inBodyResourceArray['transaction_id']])->update($notfiyOutput);
}
}else{
$id = Db::name('wxpay_record')->insertGetId($notfiyOutput);
}
if($id){
//这里是更新订单状态
。。。。。。。
//删除对应的购物车信息
。。。。。。。
//加上邮件通知或短信通知
。。。。。。。
}
}
echo json_encode(['code'=>'SUCCESS','message'=>'成功']);
}
/**
* @function wxpayCheck
* @intro 微信支付结果查询(通过订单ID查询)
* @return string
*/
public function wxpayCheck()
{
$id = input('id');
$row = Db::name('order')->where(['id'=>$id])->find();
if(!$row){
ajaxReturn(0,'订单不存在');
}
$wxpayRow = Db::name('wxpay_record')->where(['out_trade_no'=>$row['order_number']])->find();
if(!$wxpayRow){
ajaxReturn(0,'订单尚未付款');
}
//去微信后台查询
$res = checkOrder($wxpayRow['transaction_id']);//这个地方是个坑,要注意
if($res !== false){
if($res['trade_state'] == 'SUCCESS'){
if($row['status'] == 0){
//如果回调里面没更新数据库,这里可以更新数据库订单支付状态
}
ajaxReturn(1,'付款成功', url('wxpayCheckOrder',['order_number'=>$row['order_number']]));
}else{
ajaxReturn(0,$res['trade_state_desc']);
}
}else{
ajaxReturn(1,'订单查询失败');
}
}
//微信支付(jsapi支付)
public function wxjsapipay(){
$id = input('id');
if(!Session::has('openid')){
$options = array(
'appid' => config('app.wechat.appid'),
'appsecret' => config('app.wechat.appsecret')
);
//没登录的话,先去静默授权,因为支付的时候需要用到openid,这里授权逻辑不敷述了
$weApi = new \wechat\WechatApi($options);
$url = $weApi->getOauthRedirect(getHostDomain().url('login',['id'=>$id]), 'state', 'snsapi_base');
$this->redirect($url);
}
$row = Db::name('order')->where(['id'=>$id])->find();
if(!$row){
ajaxReturn(0,'订单不存在');
}
$error = 0;
$tempDescription = [];//描述最多127个字节
$data = [];
$data['out_trade_no'] = $row['order_number'];
$data['openid'] = Session::get('openid');
$data['description'] = $row['description'];
$data['total'] = $row[' price']*100;//单位:分
$res = jsapiPay($data);
if($res['code'] == 200){
$returnData = getJsParameters($res['data']['prepay_id']);//这里尤其需要注意
$returnData = json_decode($returnData, true);
}else{
exit('支付错误');
}
$this->assign('data', $returnData);
$this->assign('row', $row);
return $this->fetch();
}
我的调用微信支付的方法是放在common.php里的
<?php
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\Hash;
use WeChatPay\Util\PemUtil;
use WeChatPay\Formatter;
/**
* 获取支付配置信息
*/
function getWxpayInstance(){
// 商户号
$merchantId = config('app.wxpay.mch_id');
// 商户私钥
$merchantPrivateKeyFilePath = 'file://'.config('app.wxpay.ssl_key_path');// 注意 `file://` 开头协议不能少
// 加载商户私钥
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
$merchantCertificateFilePath = 'file://'.config('app.wxpay.ssl_cert_path');// 注意 `file://` 开头协议不能少
// 解析「商户证书」序列号
$merchantCertificateSerial = PemUtil::parseCertificateSerialNo($merchantCertificateFilePath);
// 「平台证书」,可由下载器 `./bin/CertificateDownloader.php` 生成并假定保存为 `/path/to/wechatpay/cert.pem`
//这个平台证书需要用命令行去生成一下,注意参数
$platformCertificateFilePath = 'file://'.config('app.wxpay.platform_key_path');// 注意 `file://` 开头协议不能少
// 加载「平台证书」公钥
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 解析「平台证书」序列号,「平台证书」当前五年一换,缓存后就是个常量
$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
// 工厂方法构造一个实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
]);
return $instance;
}
/**
* Native下单
*/
function nativePay($payData){
$instance = getWxpayInstance();
//Native下单
try {
$resp = $instance
->v3->pay->transactions->native
->post(['json' => [
'mchid' => config('app.wxpay.mch_id'),
'out_trade_no' => $payData['out_trade_no'],
'appid' => config('app.wxpay.app_id'),
'description' => $payData['description'],
'notify_url' => config('app.wxpay.notify_url'),
'amount' => [
'total' => $payData['total'],
'currency' => 'CNY'
],
]]);
$code = $resp->getStatusCode();
$body = $resp->getBody();
$res = [
'code' => $code,
'data' => json_decode($body, true)
];
} catch (\Exception $e) {
// 异常错误处理
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
}
$res = [
'code' => 500,
'data' => $e->getMessage()
];
}
return $res;
}
/**
* Native查询订单
*/
function checkOrder($transaction_id){
$instance = getWxpayInstance();
$res = $instance
->v3->pay->transactions->id->{'{transaction_id}'}
->getAsync([
// 查询参数结构
'query' => ['mchid' => config('app.wxpay.mch_id')],
// uri_template 字面量参数
'transaction_id' => $transaction_id,
])
->then(static function($response) {
// 正常逻辑回调处理
return $response;//这里不是该方法的返回值,注意避坑
})
->otherwise(static function($e) {
// 异常错误处理
$e->getMessage();
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$r->getStatusCode() . ' ' . $r->getReasonPhrase();
$r->getBody();
}
$e->getTraceAsString();
return false;
})
->wait();
return $res == false ? false : json_decode($res->getBody(), true);//这里避坑,return一定要在最外边
}
/**
* 生成支付二维码
*/
function getQRCode($id, $code_url){
$file_path = '/uploads/qrcode';
$save_path = Env::get('root_path') .'public'.$file_path;
if (!file_exists($save_path) && !mkdir($save_path, 0777, true)) {
return false;
}
$file = '/'.$id.time().'.png';
$code = $save_path.$file;
$url = $code_url;
require Env::get('root_path') .'extend/phpqrcode/phpqrcode.php';
QRcode::png($url,$code,'L',7);
return $file_path.$file;
}
/**
* wxpay jsapi下单
*/
function jsapiPay($payData){
$instance = getWxpayInstance();
//jsapi下单
try {
$resp = $instance
->v3->pay->transactions->jsapi
->post(['json' => [
'mchid' => config('app.wxpay.mch_id'),
'appid' => config('app.wxpay.app_id'),
'out_trade_no' => $payData['out_trade_no'],
'description' => $payData['description'],
'notify_url' => config('app.wxpay.notify_url'),
'amount' => [
'total' => $payData['total'],
'currency' => 'CNY'
],
'payer' => [
'openid' => $payData['openid'],
],
]]);
$code = $resp->getStatusCode();
$body = $resp->getBody();
$res = [
'code' => $code,
'data' => json_decode($body, true)
];
} catch (\Exception $e) {
// 异常错误处理
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
}
$res = [
'code' => 500,
'data' => $e->getMessage()
];
}
return $res;
}
/**
* 获取jaapi支付字段
*/
function getJsParameters($prepay_id){
$merchantPrivateKeyFilePath = 'file://'.config('app.wxpay.ssl_key_path');;
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);
$params = [
'appId' => config('app.wxpay.app_id'),
'timeStamp' => (string)Formatter::timestamp(),
'nonceStr' => Formatter::nonce(),
'package' => 'prepay_id='.$prepay_id,
];
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];//这里避坑,前段页面里的加密方式要和这里的一致,V3版本貌似只支持RSA加密
return json_encode($params);
}
jsapi付款的前台页面里的js如下:
<script type="text/javascript">
var appId = "{$data.appId}";
var timeStamp = "{$data.timeStamp}";
var nonceStr = "{$data.nonceStr}";
var package = "{$data.package}";
var paySign = "{$data.paySign}";
var order_number = "{$row.order_number}";
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":appId,
"timeStamp":timeStamp,
"nonceStr":nonceStr,
"package":package,
"signType":"RSA", //避坑
"paySign":paySign
},
function(result){
if(result.err_msg == "get_brand_wcpay_request:ok" ){
window.location.href = "/index/payments/wxpayCheckOrder/order_number/"+order_number;
}else{
var r=confirm("支付失败");
if (r==true){
window.location.href = "/";
}else{
window.location.href = "/";
}
}
});
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
</script>
这样基本的支付流程就走完了,可以根据自己程序的逻辑,在中间穿插自己的代码。