ThinkPHP5.1接入微信支付V3及避坑指南

微信支付,微信支付V3,PHP微信支付,微信nativePay支付,微信jsapi支付

之前的网站涉及到微信支付都是用的v2版本,好久也没更新了,没出问题也没去管,最近新做了一个项目,发现V3都出了好久了,然后就去研究了一下,踩了一点坑,记录一下。(注意:百度出来的大部分都是扯淡的)

老规矩,先看文档( 这里强调一下前提:你要有一个已认证的公众号或小程序,同时要开通了微信支付的功能 

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_0.shtml

我是先做的PC端的支付,所以先对接的Native支付:

1.先去微信支付的控制台设置密钥;然后申请证书,申请好后将压缩包解压到网站根目录里(或者服务器上,保证路径是对的即可);

16/b553fa56513c45c34c5a6e85eb27c5.png


然后设置支付回调域名;要记住mchid(商户号),后续会用到。

b3/11842c067a6a3338fd6edffc4f2143.png

2.去微信公众平台设置app_id,app_secret(jsapi支付需要用到),设置授权域名、js安全域名,IP白名单;

5d/59e906e59e291dc386053afe290b84.jpg

a0/72830f7efbaa4dc2f3b14b0c638f54.png

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>


这样基本的支付流程就走完了,可以根据自己程序的逻辑,在中间穿插自己的代码。