详解微信小程序支付流程

时间:2024-02-26 10:13:59

转发博主 https://blog.csdn.net/qq_38378384/article/details/80882980

  花了几天把小程序的支付模块接口写了一下,可能有着公众号开发的一点经验,没有入太多的坑,在此我想记录一下整个流程。

首先先把小程序微信支付的图搬过来:

相信会来查百度的同学们基本都是对文档的说明不是很理解。我下面大概总结一下整个业务逻辑的过程。

微信小程序的商户系统一般是以接口的形式开发的,小程序通过调用与后端约定好的接口进行参数的传递以及数据的接收。在小程序支付这块,还需要跟微信服务器进行交互。过程大致是这样的:

一.小程序调用登录接口获取code,传递给商户服务器用来获取用户的openID

 我们知道在微信平台中,同一个公众号的openID都是不同的,它是用户身份识别的id,也就是说,我们通过openID来区分不同的用户,这个有微信开发基础的应该都很熟悉。为了知道谁在支付,我们需要先获取当前用户的openid,那么openID应该怎么获取呢?看下图:

 

  1. 小程序调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

  2. 开发者服务器以code换取 用户唯一标识openid 和 会话密钥session_key。

看不懂吗?不急,听我慢慢解释,这个业务流程大致就是首先你得先在小程序的代码中调用wx.login()来向微信获取到code,拿到了之后把code通过request传给商户服务器,再由商户服务器通过骚操作来跟微信服务器要session_key和openID。

伪代码如下(小程序端):

  1.  
    getToken: function () {
  2.  
    //调用登录接口
  3.  
    wx.login({
  4.  
    success: function (res) {
  5.  
    var code = res.code;
  6.  
    wx.request({
  7.  
    url: 商户服务器接口地址,
  8.  
    data: {
  9.  
    code: code
  10.  
    },
  11.  
    method: \'POST\',
  12.  
    success: function (res) {
  13.  
    wx.setStorageSync(\'token\', res.data.token); //存在小程序缓存中
  14.  
    },
  15.  
    fail: function (res) {
  16.  
    console.log(res.data);
  17.  
    }
  18.  
    })
  19.  
    }
  20.  
    })
  21.  
    }

调用这几行代码就可以向跟微信服务器要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,我们可以这么封装

  1.  
    //要缓存的数据数组
  2.  
    $cacheValue = $result; //包含openID和session_key
  3.  
    $cacheValue[\'uid\'] =$uid; //用户id
  4.  
     
  5.  
    $cacheValue[\'scope\'] =ScopeEnum::User; //用户权限级别

缓存的方式我们可以选择redis,memcache, 文件缓存等等,采用键值对(key-value)的方式进行存储,记得设置好过期时间。这里的key我们用token来赋值,token可以通过这样的方式进行生成:

  1.  
    //获取32位随机字符串
  2.  
    $str = getRandChar(32); //自定义方法生成32位随机串
  3.  
    //三组字符串进行md5加密
  4.  
    $timeStamp =$_SERVER[\'REQUEST_TIME_FLOAT\'];
  5.  
    //salt
  6.  
    $salt = config(\'secure.token_salt\'); //随机字符串
  7.  
    //返回token
  8.  
     
  9.  
    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,把需要的参数通过调用对应的方法传入。

伪代码如下:

  1.  
    //调用微信支付统一下单接口
  2.  
    $wxOrderData = new \WxPayUnifiedOrder();
  3.  
    //设置相关参数
  4.  
    $wxOrderData->SetOut_trade_no($this->orderNO);
  5.  
    $wxOrderData->SetTrade_type(\'JSAPI\');
  6.  
    $wxOrderData->SetTotal_fee($totalPrice * 100); //这里的价格单位是分
  7.  
    $wxOrderData->SetBody(\'Mc\');
  8.  
    $wxOrderData->SetOpenid($openid);
  9.  
    $wxOrderData->SetNotify_url(config(\'secure.pay_back_url\'));//支付回调

其中第一个是你的订单号,订单号的生成方法可以自定义,第二个是死参数,第三个是总订单价格,第四个是名称如果是中文的话要转码,第四个是openID,这个这时候就可以从缓存中取了。最后一个是支付回调,就是支付成功后微信要访问的地址。必须是公网能访问的,或者你使用ngrok来进行反向代理转发本地的服务器。

参数设置好了之后,就直接调用SDK的方法了

 $wxOrder = \WxPayApi::unifiedOrder($wxOrderData);
如果参数没有错误的话,返回的数据中会含有prepay_id,这个是我们需要的参数。

 

3.再次签名
  1.  
    // 提交JSAPI输入对象
  2.  
    $jsApiPayData = new \WxPayJsApiPay();
  3.  
    //设置appid
  4.  
    $jsApiPayData->SetAppid(config(\'wx.app_id\'));
  5.  
    //timeStamp
  6.  
    $jsApiPayData->SetTimeStamp((string)time());
  7.  
    //随机串
  8.  
    $randStr = md5(time().mt_rand(0,1000));
  9.  
    $jsApiPayData->SetNonceStr($randStr);
  10.  
    //数据报
  11.  
    $jsApiPayData->SetPackage(\'prepay_id=\'.$wxOrder[\'prepay_id\']);
  12.  
    //类型
  13.  
    $jsApiPayData->SetSignType(\'MD5\');
  14.  
    //生成签名
  15.  
    $sign = $jsApiPayData->MakeSign();
  16.  
    //获得签名数组
  17.  
    $signData = $jsApiPayData->GetValues();
  18.  
    //增加字段paySign
  19.  
    $signData[\'paySign\']=$sign;
  20.  
    //删除signData中的app_Id字段
  21.  
    unset($signData[\'appId\']);
  22.  
    return $signData;

再次签名完成后,就把五个参数返回给小程序。

四,小程序获取五个参数后,鉴权调起支付

伪代码(小程序端)

  1.  
    pay: function () {
  2.  
    var token = wx.getStorageSync(\'token\');
  3.  
    var that = this;
  4.  
     
  5.  
    wx.request({
  6.  
    url: baseUrl + \'/order\',
  7.  
    header: {
  8.  
    token: token
  9.  
    },
  10.  
    data: { //产品的数据
  11.  
    products:
  12.  
    [
  13.  
    {
  14.  
    product_id: 1, count: 1
  15.  
    },
  16.  
     
  17.  
    {
  18.  
    product_id: 2, count: 1
  19.  
    }
  20.  
    ]
  21.  
    },
  22.  
    method: \'POST\',
  23.  
    success: function (res) {
  24.  
    console.log(res.data);
  25.  
    if (res.data.pass) {
  26.  
    wx.setStorageSync(\'order_id\', res.data.order_id);
  27.  
    that.getPreOrder(token, res.data.order_id); //调用getPreOrder
  28.  
    }
  29.  
    else {
  30.  
    console.log(\'订单未创建成功\');
  31.  
    }
  32.  
    }
  33.  
    })
  34.  
    },
  35.  
     
  36.  
    getPreOrder: function (token, orderID) {
  37.  
    if (token) {
  38.  
    wx.request({
  39.  
    url: baseUrl + \'/pay/pre_order\',
  40.  
    method: \'POST\',
  41.  
    header: {
  42.  
    token: token
  43.  
    },
  44.  
    data: {
  45.  
    id: orderID
  46.  
    },
  47.  
    success: function (res) {
  48.  
    var preData = res.data;
  49.  
    console.log(preData);
  50.  
     
  51.  
    wx.requestPayment({ //请求支付
  52.  
    timeStamp: preData.timeStamp.toString(),
  53.  
    nonceStr: preData.nonceStr,
  54.  
    package: preData.package,
  55.  
    signType: preData.signType,
  56.  
    paySign: preData.paySign,
  57.  
    success: function (res) {
  58.  
    console.log(res.data);
  59.  
    },
  60.  
    fail: function (error) {
  61.  
    console.log(error);
  62.  
    }
  63.  
    })
  64.  
    }
  65.  
    })
  66.  
    }
  67.  
    },

如果一切正常的话,在微信开发者工具就会显示这个二维码,

如果在真机上测试的话,就会直接弹出支付页面。小程序会直接显示支付成功或者失败的页面,然后微信服务器就会开始访问我们之前设置的支付回调地址来推送支付结果,根据结果可以来更新订单的状态。这里我就不写业务逻辑了,大概讲一下就好。

五,支付回调

实际上我们需要重写WxPayNotify类的NotifyProcess方法,这里记得Loader::impor()引入那个入口类。

  1.  
    /**
  2.  
    *
  3.  
    * 回调方法入口,子类可重写该方法
  4.  
    * 注意:
  5.  
    * 1、微信回调超时时间为2s,建议用户使用异步处理流程,确认成功之后立刻回复微信服务器
  6.  
    * 2、微信服务器在调用失败或者接到回包为非确认包的时候,会发起重试,需确保你的回调是可以重入
  7.  
    * @param array $data 回调解释出的参数
  8.  
    * @param string $msg 如果回调处理失败,可以将错误信息输出到该方法
  9.  
    * @return true 回调出来完成不需要继续回调,false回调处理未完成需要继续回调
  10.  
    */
  11.  
    public function NotifyProcess($data, &$msg)
  12.  
    {
  13.  
    //TODO 用户基础该类之后需要重写该方法,成功的时候返回true,失败返回false
  14.  
    return true;
  15.  
    }

  也就是说你需要写个新类继承WxPayNotify,再重写NotifyProcess方法,根据检查$data[\'result_code\']是否为SUCCESS可以判断成功与否,成功的话你可以根据业务需求写业务逻辑,最后return true 即可。这时候会想,我重写了这个方法后微信怎么调用呢,其实这里微信不是要直接调用这个方法,你应该在微信支付回调的方法中实例化这个新类,然后根据获得的对象去调用Handle()方法。$obj = new 新类(),$obj->Handle()。