詳解微信小程式支付流程
花了幾天把小程式的支付模組介面寫了一下,可能有著公眾號開發的一點經驗,沒有入太多的坑,在此我想記錄一下整個流程。
首先先把小程式微信支付的圖搬過來:
相信會來查百度的同學們基本都是對文件的說明不是很理解。我下面大概總結一下整個業務邏輯的過程。
微信小程式的商戶系統一般是以介面的形式開發的,小程式通過呼叫與後端約定好的介面進行引數的傳遞以及資料的接收。在小程式支付這塊,還需要跟微信伺服器進行互動。過程大致是這樣的:
一.小程式呼叫登入介面獲取code,傳遞給商戶伺服器用來獲取使用者的openID
我們知道在微信平臺中,同一個公眾號的openID都是不同的,它是使用者身份識別的id,也就是說,我們通過openID來區分不同的使用者,這個有微信開發基礎的應該都很熟悉。為了知道誰在支付,我們需要先獲取當前使用者的openid,那麼openID應該怎麼獲取呢?看下圖:
小程式呼叫wx.login() 獲取 臨時登入憑證code ,並回傳到開發者伺服器。
開發者伺服器以code換取 使用者唯一標識openid 和 會話金鑰session_key。
看不懂嗎?不急,聽我慢慢解釋,這個業務流程大致就是首先你得先在小程式的程式碼中呼叫wx.login()來向微信獲取到code,拿到了之後把code通過request傳給商戶伺服器,再由商戶伺服器通過騷操作來跟微信伺服器要session_key和openID。
虛擬碼如下(小程式端):
getToken: function () { //呼叫登入介面 wx.login({ success: function (res) { var code = res.code; wx.request({ url: 商戶伺服器介面地址, data: { code: code }, method: 'POST', success: function (res) { wx.setStorageSync('token', res.data.token); //存在小程式快取中 }, fail: function (res) { console.log(res.data); } }) } }) }
呼叫這幾行程式碼就可以向跟微信伺服器要code,並且將code傳到商戶伺服器中,記住這裡最好使用post傳送請求,安全性的東西我應該不用講了,因為避免其他人濫用介面,於是我們使用token來進行驗證。並將商戶伺服器返回的token存在小程式快取中。
那麼伺服器端應該怎麼做呢?
我門通過小程式提交的code,和小程式的APPID以及APPSECRET和拼接下列的url,並用curl進行get請求。
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
返回的資料是一個json物件,我門通過使用json_decode(JSON,true)解析為陣列,資料包括使用者的openID以及session_key,獲取到了後我們應該將openID存入資料庫中,它代表著使用者的身份,那麼令牌應該怎麼生成呢。
二.token的生成以及快取
我們根據一個使用者表將id和openid聯絡起來,對應openID的id則是使用者的uid,我們可以這麼封裝
//要快取的資料陣列
$cacheValue = $result; //包含openID和session_key
$cacheValue['uid'] =$uid; //使用者id
$cacheValue['scope'] =ScopeEnum::User; //使用者許可權級別
快取的方式我們可以選擇redis,memcache, 檔案快取等等,採用鍵值對(key-value)的方式進行儲存,記得設定好過期時間。這裡的key我們用token來賦值,token可以通過這樣的方式進行生成:
//獲取32位隨機字串
$str = getRandChar(32); //自定義方法生成32位隨機串
//三組字串進行md5加密
$timeStamp =$_SERVER['REQUEST_TIME_FLOAT'];
//salt
$salt = config('secure.token_salt'); //隨機字串
//返回token
return md5($str.$timeStamp.$salt);
這種演算法基本保障了token的唯一性。因為值是我們獲取到的openID和session_key所在的陣列,所以需要將陣列轉成json才能存進去。以後的程式碼當我們需要openID或者uid等時可以直接通過取快取的方式來取。
三,呼叫統一下單介面,獲取prepay_id,再次簽名
在你寫完了訂單操作後,如何讓使用者支付訂單費用呢?這裡就是重點了,我一步一步來說:
1.下載微信JS-SDK:
(https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1)
解壓開啟進入lib資料夾中:
我們需要將lib中的檔案放到我們的框架中,例如我使用的是tp5,就放到extend下,最好是在extend下建個子資料夾。其中WxPay.Api.php是入口,WxPay.Config.php是配置檔案。下好後需要改動一些地方。在WxPay.Config.php中修改下列的東西改成你的。
然後在WxPay.Api.php中require一下WxPay.Notify.php,如圖:
在某個控制器或者服務層的程式碼先是用Loader::import()引入WxPay.Api.php,相當於五個都引入了。
2.呼叫統一下單api
這裡要囉嗦的是,如何你寫的是有關商品買賣的小程式,那麼需要在支付前再次檢測一下庫存量,因為使用者下完訂單後不一定馬上就會付款,如果在付款的期間庫存量沒了便會出現問題。業務邏輯我就不說太多了,這取決於你寫程式碼的嚴謹性。
在我們引入了上面那個檔案後,先例項化這個類WxPayUnifiedOrder,把需要的引數通過呼叫對應的方法傳入。
虛擬碼如下:
//呼叫微信支付統一下單介面
$wxOrderData = new \WxPayUnifiedOrder();
//設定相關引數
$wxOrderData->SetOut_trade_no($this->orderNO);
$wxOrderData->SetTrade_type('JSAPI');
$wxOrderData->SetTotal_fee($totalPrice * 100); //這裡的價格單位是分
$wxOrderData->SetBody('Mc');
$wxOrderData->SetOpenid($openid);
$wxOrderData->SetNotify_url(config('secure.pay_back_url'));//支付回撥
其中第一個是你的訂單號,訂單號的生成方法可以自定義,第二個是死引數,第三個是總訂單價格,第四個是名稱如果是中文的話要轉碼,第四個是openID,這個這時候就可以從快取中取了。最後一個是支付回撥,就是支付成功後微信要訪問的地址。必須是公網能訪問的,或者你使用ngrok來進行反向代理轉發本地的伺服器。
引數設定好了之後,就直接呼叫SDK的方法了
$wxOrder = \WxPayApi::unifiedOrder($wxOrderData);
如果引數沒有錯誤的話,返回的資料中會含有prepay_id,這個是我們需要的引數。3.再次簽名
// 提交JSAPI輸入物件
$jsApiPayData = new \WxPayJsApiPay();
//設定appid
$jsApiPayData->SetAppid(config('wx.app_id'));
//timeStamp
$jsApiPayData->SetTimeStamp((string)time());
//隨機串
$randStr = md5(time().mt_rand(0,1000));
$jsApiPayData->SetNonceStr($randStr);
//資料報
$jsApiPayData->SetPackage('prepay_id='.$wxOrder['prepay_id']);
//型別
$jsApiPayData->SetSignType('MD5');
//生成簽名
$sign = $jsApiPayData->MakeSign();
//獲得簽名陣列
$signData = $jsApiPayData->GetValues();
//增加欄位paySign
$signData['paySign']=$sign;
//刪除signData中的app_Id欄位
unset($signData['appId']);
return $signData;
再次簽名完成後,就把五個引數返回給小程式。
四,小程式獲取五個引數後,鑑權調起支付
虛擬碼(小程式端)
pay: function () {
var token = wx.getStorageSync('token');
var that = this;
wx.request({
url: baseUrl + '/order',
header: {
token: token
},
data: { //產品的資料
products:
[
{
product_id: 1, count: 1
},
{
product_id: 2, count: 1
}
]
},
method: 'POST',
success: function (res) {
console.log(res.data);
if (res.data.pass) {
wx.setStorageSync('order_id', res.data.order_id);
that.getPreOrder(token, res.data.order_id); //呼叫getPreOrder
}
else {
console.log('訂單未建立成功');
}
}
})
},
getPreOrder: function (token, orderID) {
if (token) {
wx.request({
url: baseUrl + '/pay/pre_order',
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.data);
},
fail: function (error) {
console.log(error);
}
})
}
})
}
},
如果一切正常的話,在微信開發者工具就會顯示這個二維碼,
如果在真機上測試的話,就會直接彈出支付頁面。小程式會直接顯示支付成功或者失敗的頁面,然後微信伺服器就會開始訪問我們之前設定的支付回撥地址來推送支付結果,根據結果可以來更新訂單的狀態。這裡我就不寫業務邏輯了,大概講一下就好。
五,支付回撥
實際上我們需要重寫WxPayNotify類的NotifyProcess方法,這裡記得Loader::impor()引入那個入口類。
/**
*
* 回撥方法入口,子類可重寫該方法
* 注意:
* 1、微信回撥超時時間為2s,建議使用者使用非同步處理流程,確認成功之後立刻回覆微信伺服器
* 2、微信伺服器在呼叫失敗或者接到回包為非確認包的時候,會發起重試,需確保你的回撥是可以重入
* @param array $data 回撥解釋出的引數
* @param string $msg 如果回撥處理失敗,可以將錯誤資訊輸出到該方法
* @return true 回調出來完成不需要繼續回撥,false回撥處理未完成需要繼續回撥
*/
public function NotifyProcess($data, &$msg)
{
//TODO 使用者基礎該類之後需要重寫該方法,成功的時候返回true,失敗返回false
return true;
}
也就是說你需要寫個新類繼承WxPayNotify,再重寫NotifyProcess方法,根據檢查$data['result_code']是否為SUCCESS可以判斷成功與否,成功的話你可以根據業務需求寫業務邏輯,最後return true 即可。這時候會想,我重寫了這個方法後微信怎麼呼叫呢,其實這裡微信不是要直接呼叫這個方法,你應該在微信支付回撥的方法中例項化這個新類,然後根據獲得的物件去呼叫Handle()方法。$obj = new 新類(),$obj->Handle()。