1. 程式人生 > >thinkphp開發小程式之小程式發起微信支付

thinkphp開發小程式之小程式發起微信支付

最近在學一套小程式商城,最近做到了小程式支付環節,分享一下我的心得。

首先,你需要有認證的小程式,並且已開通微信支付,我的是服務號,並且早已申請號了微信支付,現在開通小程式,直接申請繫結即可。

首先我們去下載微信支付SDK,微信只有一套支付用的SDK,集成了掃碼,公眾號等。

下載後我們在thinkphp5的根目錄下面的extend下面建立wxpay資料夾,並且將下載好的sdk解壓,將裡面的核心檔案lib下的所有檔案複製到WxPay裡。

然後在WxPay.Config.php檔案裡填入支付商戶等資訊,在這裡要注意的是,裡面的APPID是小程式的APPID,由於我是直接從我先前公眾號支付的程式碼複製過來的,所以在測試出現appid and openid not match 報錯返回資訊。

接下來就是匯入到thinkphp5裡,因為官方SDK沒有使用名稱空間,所以我們使用tp5自導的Loader類來引入。

在使用的業務控制器中使用以下程式碼引入WxPay.Api.php:

我們看一下他require_once哪些檔案:

require_once "WxPay.Exception.php";
require_once "WxPay.Config.php";
require_once "WxPay.Data.php";

可見,主要檔案都已經被引入。所以我們就不需要引入其他檔案了

Loader::import,使用前記得use think\Loader;

三個引數,第一個WxPay.WxPay為extend下面的WxPay資料夾+類名的第一個'.'之前的檔名

第二個引數,我們填寫常量EXTEND_PATH,表明extend資料夾

第三個引數,就是類名,第一個'.'之後的字尾名稱。

Loader::import('WxPay.WxPay',EXTEND_PATH,'.Api.php');
這樣已經引入了。
接下來我們第一步,我的一個方法裡面的程式碼:
    private function makeWxPreOrder($totalPrice)
    {
        //傳遞過來的引數為訂單商品總價格
        /**
         *
         * 統一下單,WxPayUnifiedOrder中out_trade_no、body、total_fee、trade_type必填
         * appid、mchid、spbill_create_ip、nonce_str不需要填入
         * @param WxPayUnifiedOrder $inputObj
         * @param int $timeOut
         * @throws WxPayException
         * @return 成功時返回,其他拋異常
         */
        $wxOrderData = new \WxPayUnifiedOrder();
        //唯一訂單號
        $wxOrderData->SetOut_trade_no($orderNo);
        //代表JSAPI模式,不要修改,公眾號支付,H5,小程式都是這個
        $wxOrderData->SetTrade_type('JSAPI');
        //價格,單位為分
        $wxOrderData->SetTotal_fee($totalPrice * 100);
        //商品簡介
        $wxOrderData->SetBody('零食商販');
        //使用小程式使用者的openid
        $wxOrderData->SetOpenid($openid);
        //非同步回撥驗證路徑,開發者自定義
        $wxOrderData->SetNotify_url('http://www.xxx.com/pay/notify');
        //這個是我又封裝的一個生成簽名的方法
        return $this->getPaySignature($wxOrderData);
    }

下面看生成簽名的方法,就是上面說的封裝生成預支付資訊
private function getPaySignature($wxOrderData)
    {
        $wxOrder = \WxPayApi::unifiedOrder($wxOrderData);
        // 失敗時不會返回result_code
        if($wxOrder['return_code'] != 'SUCCESS' || $wxOrder['result_code'] !='SUCCESS'){
            Log::record($wxOrder,'error');
            Log::record('獲取預支付訂單失敗','error');//這裡為記錄異常,根據自己業務選擇處理
        }
        return $wxOrder;
    }

一切順利,微信伺服器返回程式碼如下:前兩個資訊我隱藏了,這是微信返回的預支付資訊
appid: "XXXXXXXXXX"
mch_id: "XXXXXXX"
nonce_str: "6D1bLcdvtnCLaCCh"
prepay_id: "wx20171221124029ff0a820eff0678005613"
result_code: "SUCCESS"
return_code: "SUCCESS"
return_msg: "OK"
sign: "917038D942BEDEF0B8754D0828E52C0F"
trade_type: "JSAPI"

但是getPaySignature這個方法是不完整的,因為並未得到小程式所要拉起支付所需結果,我們來看一下小程式拉起支付所需,圖片展示


所以我們需要對以上資料進行簽名等處理,修改後的兩個方法如下:

    private function getPaySignature($wxOrderData)
    {
        $wxOrder = \WxPayApi::unifiedOrder($wxOrderData);
        // 失敗時不會返回result_code
        if($wxOrder['return_code'] != 'SUCCESS' || $wxOrder['result_code'] !='SUCCESS'){
            Log::record($wxOrder,'error');  //修改成自己的異常處理
            Log::record('獲取預支付訂單失敗','error');
        }
        $signature $this->sign($wxOrder);
        return $signature;
    }

    //按照文件要求生成簽名,傳遞給小程式,讓小程式拉起支付
    private function sign($wxOrder)
    {
        $jsApiPayData = new \WxPayJsApiPay();
        //傳入小程式appid
        $jsApiPayData->SetAppid(\WxPayConfig::APPID);
        //按照文件,要求是字串型別
        $jsApiPayData->SetTimeStamp((string)time());
        //生成隨機字串
        $rand = md5(time() . mt_rand(0, 1000));
        $jsApiPayData->SetNonceStr($rand);
        //拼接prepay_id,要注意拼接
        $jsApiPayData->SetPackage('prepay_id=' . $wxOrder['prepay_id']);
        //簽名方式md5
        $jsApiPayData->SetSignType('md5');
        //然後呼叫sdk自帶的MakeSign方法生成簽名
        $sign = $jsApiPayData->MakeSign();
        //然後在使用sdk自帶方法獲取到上面的我們賦值到成員屬性生成的陣列
        $rawValues = $jsApiPayData->GetValues();
        //然後我們在陣列上加上生成的簽名
        $rawValues['paySign'] = $sign;
        //刪除appid,因為返回給客戶端沒有用,所以消除
        unset($rawValues['appId']);
        //返回
        return $rawValues;
    }


至此,返回結果如下,正好小程式wx.requestPayment(OBJECT)拉起支付所需引數:

nonceStr: "e323026d6254dd19f15561450fdecfb6"
package: "prepay_id=wx20171221143203d3e4d388b40964188996"
paySign: "39D08C08CEFCE485928927A812DD74BB"
signType: "md5"
timeStamp: "1513837924"

小程式中程式碼如下:

  //獲取token值
  getToken:function(){
      wx.login({
      success: function (res) {
        if (res.code) {
          var code = res.code
          wx.request({
            url: 'http://www.xcx.com/api/v1/token/user?XDEBUG_SESSION_START=16697',
            data:{
              code:code
            },
            method:'POST',
            success: function (res) {
              console.log(res.data),
                wx.setStorageSync('super_token', res.data.token)
            }
          })
        } else {
          console.log('獲取使用者登入態失敗!' + res.errMsg)
        }
      }
    });
  },
  //測試訂單資訊
  subOrder: function () {
    var super_token = wx.getStorageSync('super_token');
    var that = this;
    if (!super_token){
      console.log('獲取快取token失敗,請檢查')
    };
    wx.request({
      url: baseUrl + '/order',
      header:{
        token:super_token
      },
      data:{
        products:[
          {
            "product_id": 1,
            "count": 1
          },
          {
            "product_id": 3,
            "count": 2
          }
        ]
      },
      method:'POST',
      success:function(res){
        console.log(res.data);
        if(res.data.pass){
          wx.setStorageSync('order_id', res.data.order_id);
          that.getPreOrder(super_token,res.data.order_id);
        }else{
          console.log('訂單建立失敗');
        }
      }
    })
  },
  //訂單發起支付
  getPreOrder:function(token,orderID){
    if(token){
      wx.request({
        url: baseUrl + '/pay/pre_order?XDEBUG_SESSION_START=11697',
        method:'POST',
        header:{
          token:token
        },
        data:{
          id: orderID
        },
        success:function(res){
          var preData = res.data;
          console.log(preData);
          wx.requestPayment({
            'timeStamp': preData.timeStamp.toString(),
            'nonceStr': preData.nonceStr,
            'package': preData.package,
            'signType': preData.signType,
            'paySign': preData.paySign,
            'success': function (res) {
              console.log(res.errMsg);
            },
            'fail': function (error) {
              console.log(error);
            }
          })
        }
      })
    }
  }

業務控制器完整程式碼我貼出來
<?php
/**
 * Created by YuanPan.
 * User: YuanPan
 * Date: 2017/12/20
 * Time: 16:34
 */

namespace app\api\service;


use app\lib\enum\OrderStatusEnum;
use app\lib\exception\OrderException;
use app\lib\exception\TokenException;
use think\Exception;
use app\api\service\Order as OrderService;
use app\api\model\Order as OrderModel;
use think\Loader;
use think\Log;

Loader::import('WxPay.WxPay',EXTEND_PATH,'.Api.php');

//訂單業務
class Pay
{
    private $orderID;
    private $orderNo;
    //構造方法接受order的id
    public function __construct($orderID)
    {
        //不允許傳遞空
        if(!$orderID){
            throw new Exception('訂單號不允許為空');
        }
        //賦值成員屬性
        $this->orderID = $orderID;
    }

    public function pay()
    {
        //再一次對庫存量檢測
        //訂單號可能不存在
        //訂單號和當前使用者不匹配
        //訂單號已經被支付過了
        $this->checkOrderValid();
        $orderservice = new OrderService();
        $status = $orderservice->checkOrderStock($this->orderID);
        if(!$status['pass']){
            return $status;
        }
        return $this->makeWxPreOrder($status['orderPrice']);
    }

    private function makeWxPreOrder($totalPrice)
    {
        $openid = Token::getCurrentTokenVar('openid');
        if(!$openid){
            throw new TokenException();
        }
        /**
         *
         * 統一下單,WxPayUnifiedOrder中out_trade_no、body、total_fee、trade_type必填
         * appid、mchid、spbill_create_ip、nonce_str不需要填入
         * @param WxPayUnifiedOrder $inputObj
         * @param int $timeOut
         * @throws WxPayException
         * @return 成功時返回,其他拋異常
         */
        $wxOrderData = new \WxPayUnifiedOrder();
        //唯一訂單號
        $wxOrderData->SetOut_trade_no($this->orderNo);
        //代表JSAPI模式,不要修改,公眾號支付,H5,小程式都是這個
        $wxOrderData->SetTrade_type('JSAPI');
        //價格,單位為分
        $wxOrderData->SetTotal_fee($totalPrice * 100);
        //商品簡介
        $wxOrderData->SetBody('零食商販');
        //使用小程式使用者的openid
        $wxOrderData->SetOpenid($openid);
        //非同步回撥驗證路徑
        $wxOrderData->SetNotify_url(config('secure.pay_back_url'));
        //這個是我又封裝的一個生成簽名的方法
        return $this->getPaySignature($wxOrderData);
    }

    private function getPaySignature($wxOrderData)
    {
        $wxOrder = \WxPayApi::unifiedOrder($wxOrderData);
        // 失敗時不會返回result_code
        if($wxOrder['return_code'] != 'SUCCESS' || $wxOrder['result_code'] !='SUCCESS'){
            Log::record($wxOrder,'error');
            Log::record('獲取預支付訂單失敗','error');
//            throw new Exception('獲取預支付訂單失敗');
        }
        //將prepay_id存入對應訂單資料庫
        $this->recordPreOrder($wxOrder);
        $signature =  $this->sign($wxOrder);
        return $signature;
    }

    //按照文件要求生成簽名,傳遞給小程式,讓小程式拉起支付
    private function sign($wxOrder)
    {
        $jsApiPayData = new \WxPayJsApiPay();
        //傳入小程式appid
        $jsApiPayData->SetAppid(\WxPayConfig::APPID);
        //按照文件,要求是字串型別
        $jsApiPayData->SetTimeStamp((string)time());
        //生成隨機字串
        $rand = md5(time() . mt_rand(0, 1000));
        $jsApiPayData->SetNonceStr($rand);
        //拼接prepay_id,要注意拼接
        $jsApiPayData->SetPackage('prepay_id=' . $wxOrder['prepay_id']);
        //簽名方式md5
        $jsApiPayData->SetSignType('md5');
        //然後呼叫sdk自帶的MakeSign方法生成簽名
        $sign = $jsApiPayData->MakeSign();
        //然後在使用sdk自帶方法獲取到上面的我們賦值到成員屬性生成的陣列
        $rawValues = $jsApiPayData->GetValues();
        //然後我們在陣列上加上生成的簽名
        $rawValues['paySign'] = $sign;
        //刪除appid,因為返回給客戶端沒有用,所以消除
        unset($rawValues['appId']);
        //返回
        return $rawValues;
    }

    private function recordPreOrder($wxOrder)
    {
        OrderModel::where(['id'=>$this->orderID])->update(['prepay_id'=>$wxOrder['prepay_id']]);
    }
    private function checkOrderValid()
    {
        $order = OrderModel::get(['id'=>$this->orderID]);
        if(!$order){
            throw new OrderException();
        }
        if(!Token::isValidOperate($order->user_id))
        {
            throw new TokenException(
                [
                    'msg' => '訂單與使用者不匹配',
                    'errorCode' => 10003
                ]);
        }
        if($order->status != OrderStatusEnum::UNPAID){
            throw new OrderException([
                'msg' => '訂單已支付過啦',
                'errorCode' => 80003,
                'code' => 400
            ]);
        }
        $this->orderNo = $order->order_no;
        return true;
    }

}

然後控制器使用:
    public function getPreOrder($id='')
    {
        (new IdMustInt())->goCheck();
        $pay = new Pay($id);
        return $pay->pay();
    }


如果支付成功,小程式端的res.errMsg == 'requestPayment:ok'

success	requestPayment:ok	呼叫支付成功
fail	requestPayment:fail cancel	使用者取消支付
fail	requestPayment:fail (detail message)	呼叫支付失敗,其中 detail message 為後臺返回的詳細失敗原因