最近在学一套小程序商城,最近做到了小程序支付环节,分享一下我的心得。
首先,你需要有认证的小程序,并且已开通微信支付,我的是服务号,并且早已申请号了微信支付,现在开通小程序,直接申请绑定即可。
首先我们去下载微信支付SDK,微信只有一套支付用的SDK,集成了扫码,公众号等。
下载链接:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1
下载后我们在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 为后台返回的详细失败原因