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 為後臺返回的詳細失敗原因