PHP实现微信小程序支付的帮助类

背景

事先准备工作

  1. 申请一个小程序,并开通微信支付,详细见:微信小程序支付业务说明
  2. 仔细查阅微信支付官方文档,详细见: 微信支付开发者文档
  3. 仔细阅读 微信支付统一下单接口
  4. 仔细阅读 支付结果通知接口
  5. 整理并在商户平台设置好相应的回掉地址,比如http://test.dev.com/wechat/pa...
  6. 服务端编写两个接口
    1) 微信预支付接口,http://test.dev.com/wechat/pr... , 以商品订单为例,此接口接受两个参数,goods_id 和 uid ,goods_id表示商品编号,uid表示用户编号,返回参数如下
 {
        errcode: 200,
        msg: "SUCCESS",
        data: {
            status: 1,  //状态,为1表示成功,其他的表示失败
            result: "success",
            data: {
                appId: "xxx",  //小程序的appid
                timeStamp: 1545909092, //时间戳
                nonceStr: "vuryhptlafvpee92pxhji6zs5jl2n0gu", //随机串
                package: "prepay_id=wx27191130962951f060bfa1323531879649", //支付的包参数
                signType: "MD5", //签名方式
                paySign: "B04272BB9BBDB1F52863D3B0EF580BE8" //支付签名
            }
        }
    }

2) 微信支付回调接口,http://test.dev.com/wechat/pa... ,此接口最好是get和post都设置,因为 微信在进行回调的时候会以post的形式进行请求

5.建表
1) 商品订单表(shop_goods_order),其中重要的字段有out_trade_no,out_trade_no传递给微信支付的支付订单号,也是我们自己的系统与微信对接的订单唯一标识;bill_no表示微信支付的交易订单号,这个字段只有在订单支付成功之后进行更新,该字段也是查询位置支付订单的唯一标识,详细的表结构如下

CREATE TABLE `shop_goods_order` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `uid` int(10) DEFAULT '0' COMMENT '用户编号',
  `goods_id` int(10) DEFAULT '0' COMMENT '商品编号',
  `out_trade_no` varchar(30) DEFAULT '' COMMENT '订单序列号',
  `bill_no` varchar(30) DEFAULT '' COMMENT '支付方返回的交易订单号',
  `paid_money` int(10) DEFAULT '0' COMMENT '支付的金额',
  `paid_integral` int(10) DEFAULT '0' COMMENT '支付的健康币',
  `paid_type` varchar(15) DEFAULT 'WXPAY' COMMENT '支付类型,有WXPAY和INTEGRAL等值',
  `paid_status` varchar(10) DEFAULT 'CHECKED' COMMENT '支付状态,CHECKED表示初始状态,SUCC表示支付成功,FAILED表示支付失败,REFUND表示已退款',
  `add_time` int(10) DEFAULT '0' COMMENT '添加时间',
  `paid_time` int(10) DEFAULT '0' COMMENT '支付时间',
  `update_time` int(10) DEFAULT '0' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;

2) 商品信息表(shop_goods_info),字段如下

CREATE TABLE `shop_goods_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(100) DEFAULT '' COMMENT '商品名称',
  `note` varchar(300) DEFAULT '' COMMENT '商品描述',
  `market_price` int(10) DEFAULT '0' COMMENT '原价',
  `sale_price` int(10) DEFAULT '0' COMMENT '售价',
  `integral` int(8) DEFAULT '0' COMMENT '健康币',
  `main_thumbnail` varchar(40) DEFAULT '' COMMENT '主图',
  `thumbnail1` varchar(40) DEFAULT '' COMMENT '缩略图1',
  `thumbnail2` varchar(40) DEFAULT '' COMMENT '缩略图2',
  `thumbnail3` varchar(40) DEFAULT '' COMMENT '缩略图3',
  `thumbnail4` varchar(40) DEFAULT '' COMMENT '缩略图4',
  `thumbnail5` varchar(40) DEFAULT '' COMMENT '缩略图5',
  `content` text COMMENT '详细介绍',
  `add_time` int(10) DEFAULT '0' COMMENT '添加时间',
  `update_time` int(10) DEFAULT '0' COMMENT '更新时间',
  `is_online` tinyint(1) DEFAULT '1' COMMENT '商品是否上线',
  `sort` int(4) DEFAULT '0' COMMENT '排序值,越大越靠前',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

实现步骤

业务实现时序图

实现步骤说明

客户端

  1. 客户端调用 微信预支付接口 获取对应的微信支付参数
  2. 获取基础的支付参数后,调用wx.requetPayment接口调起微信支付
  3. 用户输入密码完成支付

服务端

  1. 客户端在发起支付前先往商品订单表里面创建一条订单,并生成对应的out_trade_no参数
  2. 调用微信支付的统一下单接口https://api.mch.weixin.qq.com...,向微信发起支付订单请求,统一下单接口文档地址,见微信支付统一下单接口
  3. 支付请求结束后微信将支付结果返回给 微信支付回调接口
  4. 若支付成功,服务端将订单的paid_status字段设置succ ,并将bill_no、paid_time、update_time更新,bill_no的值为微信支付的transaction_id;若支付失败,将paid_status字段更新为failed,并更新update_time字段

关键代码

客户端

发起微信支付

 wxPay:function () {
    var that = this
    var params = {
      goods_id: that.data.goods_id,
      uid: that.data.uid,
      paid_type: 'WXPAY'
    }

    var param = JSON.stringify(params)
    console.log(param)
    param = app.Encrypt(param)
    var url = app.data.API_DOMAIN + "/wechat/prepay?param=" + param
    wx.showModal({
      title: '提示',
      content: '确定要微信支付购买此系列课吗?',
      success(res) {
        if (res.confirm) {
          if (that.data.iswxpay == 0) {
            that.setData({
              iswxpay: 1
            })
            app.httpRequest(that.data.uid, url, function (response) {
              var payinfo = response.data.data.data
              wx.requestPayment({
                timeStamp: payinfo.timeStamp.toString(),
                nonceStr: payinfo.nonceStr,
                package: payinfo.package,
                signType: 'MD5',
                paySign: payinfo.paySign,
                success(res) {
                  wx.showToast({
                    title: '购买成功',
                    icon: 'success'
                  })
                  that.setData({
                    is_paid: 1
                  })
                  that.getSeminarInfo(that.data.sid, that.data.uid)
                },
                fail(res) {
                  that.setData({
                    iswxpay: 0
                  })
                  wx.showToast({
                    title: '购买失败',
                    icon: 'none'
                  })
                }
              })
              console.log(response.data.data.data)
            }, function (f_res) { }, function (f_res) { })
          }
        } else {
          that.setData({
            iswxpay: 0
          })
          console.log('取消微信支付')
        }
      }
    })
  },

服务端

预支付接口关键代码

1、入口方法:orderPay

/**
     * 微信支付的获取支付参数的接口
     * 1.先要用户编号和支付方式获取对应的订单,如果存在则取存在的,若不存在则创建,一种支付类型的订单值存在一条记录
     * 2.创建订单后根据out_trade_no来调用微信支付的统一下单接口得到微信支付的支付参数
     * 3.将参数返回给前端进行支付
     * 4.支付成功之后进行回掉
     */
    public function orderPay($uid, $goodsId, $paidType){
        $result = [];
        $lockKey = BusinessHelper::getPayOrderLockRedisKey();
        //枷锁是为了防止并发
        $this->doWithLock(function()use(&$result,$uid,$goodsId,$paidType){
            error_log('$paidType ================>'.$paidType);
            switch ($paidType){
                case Constant::PAID_TYPE_MIXED :
                    error_log('doIntegralPay ================>');
                    $result = $this->doMixedPay($uid,$goodsId,$paidType);
                    error_log('integral pay result ================>'.json_encode($result));
                    break;
                case Constant::PAID_TYPE_WXPAY :
                    $result = $this->doWxaPay($uid,$goodsId,$paidType);
                    error_log('wx pay result ================>'.json_encode($result));
                    break;
            }
        },$lockKey,5);
        error_log('result ================>'.json_encode($result));
        return $result;
    }

2、微信核心支付方法:doWxaPay

/**
     * 通过小程序支付的逻辑
     * @param $uid 用户编号
     * @param $goodsId 系列课编号
     * @param $paidType 支付类型,有INTEGRAL和WXPAY两种
     * @return array
     */
    public function doWxaPay($uid, $goodsId, $paidType){
        $goodsInfo = ShopGoodsInfoService::getById($goodsId);
        if(!$goodsInfo){
            return [
                'status' => -1,
                'result' => '商品已经下架或者不存在'
            ];
        }
        $config = BusinessHelper::getWechatPayConfig();
        $payHelper = new WechatPayHelper($config);
        $payContent = $this->getWxaPrepayContent($uid,$paidType,$goodsId);
        $params = $payHelper->prepay($payContent);
        error_log('param ==============>'.json_encode($params));
        return $params;
    }

3、创建订单方法:createOrder
这个方法是为了建立订单,为了保证表示每一次支付都建立一个订单,我这边做两重的订单复用,先根据订单状态去查询是否有待支付的订单,如果有在判断这个订单的差功能键时间是否已经超过7天,如果超过七天则另外创建新的订单,尽最大的进行数据复用

/**
     * 创建和验证订单,接口方法
     * @param $uid 用户编号
     * @param $paidType 支付类型
     * @param $goodsId 系列课编号
     * @return array
     */
    protected function createOrder($uid, $paidType, $goodsId){
        $existOrder = $this->getUserGoodsOrderWithPaidType($uid,$paidType,$goodsId);
        if(!$existOrder){
            return $this->generateOrder($uid,$paidType,$goodsId);
        }
        //验证7天之类订单有效
        $createTime = date('Y-m-d',$existOrder['add_time']);
        $today = date('Y-m-d');
        $diff = TimeHelper::getDiffBetweenTwoDays($today,$createTime);
        if($diff > 7){
            return $this->generateOrder($uid,$paidType,$goodsId);
        }
        return $existOrder;
    }

4、订单查重方法:getUserGoodsOrderWithPaidType

 /**
     * 根据支付类型获取用户对应的商品的订单
     */
    public function getUserGoodsOrderWithPaidType($uid, $paidType, $goodsId){
        $order = BeanHelper::convertStdClsToArr(
            ShopGoodsOrder::where('uid', $uid)
                ->where('goods_id',$goodsId)
                ->where('paid_type',$paidType)
                ->whereIn('paid_status',[Constant::PAID_STATUS_CHECKED])
                ->orderBy('add_time','desc')
                ->first()
        );
        return $order;
    }

5、生成订单方法:

/**
     * 生成订单,辅助方法
     * @param $uid 用户编号
     * @param $paidType 支付类型
     * @param $goodsId 系列课编号
     * @return array
     */
    public function generateOrder($uid, $paidType, $goodsId){
        $goodsInfo = ShopGoodsInfoService::getById($goodsId);
        $priceKey = $paidType == Constant::PAID_TYPE_WXPAY ? 'market_price' : 'sale_price';
        $price = formatArrValue($goodsInfo,$priceKey,0);
        $integral = $paidType == Constant::PAID_TYPE_WXPAY ? 0 : formatArrValue($goodsInfo,'integral',0);
        $baseMeasureUnit = 100;
        $insertOrderData = [
            'uid' => $uid,
            'goods_id' => $goodsId,
            'out_trade_no' => BusinessHelper::generateOutTradeNo(Constant::PAID_SCENE_SHOP_GOODS_ORDER),
            'paid_money' => $price * $baseMeasureUnit,
            'paid_integral' => $integral,
            'paid_type' => $paidType,
            'paid_status' => Constant::PAID_STATUS_CHECKED,
            'add_time' => time(),
            'update_time' => time(),
        ];
        $existOrder = BeanHelper::convertStdClsToArr($this->store($insertOrderData));
        return $existOrder;
    }

6、生成outTradeNo方法
这个方法中的getPaidSceneMapping方法返回的是一个数组,out_trade_no方法有3个部分组成,分别是当前时间,场景值(这个是为了保证不同的支付场景对应的不同的业务代码)以及10位随机数字组成

   /**
     * 生成第三方支付的外部订单号
     */
    public static function generateOutTradeNo($paidScene = Constant::PAID_SCENE_SEMINAR_ORDER){
        $prefix = date('YmdHis');
        $paidSceneMap = self::getPaidSceneMapping();
        $scene = formatArrValue($paidSceneMap,$paidScene,'0001');
        $suffix = generateRandomNum(10);
        return $prefix.$scene.$suffix;
    }
    
    /**
     * 获取支付场景的map,这个是为了区分不同的支付场景时候更新不同的业务字段,为了拓展进行的预留
     */
    public static function getPaidSceneMapping(){
        return [
            Constant::PAID_SCENE_SEMINAR_ORDER => '0001',
            Constant::PAID_SCENE_SHOP_GOODS_ORDER => '0002'
        ];
    }

支付回调接口关键代码

入口方法:payNotify

/**
 * 支付的回掉
 */
public function payNotify(Request $request){
    error_log('notify request param ========>');
    $config = BusinessHelper::getWechatPayConfig();
    $helper = new WechatPayHelper($config);
    $result = $helper->notify($request);
    return $result;
}

微信支付帮助类

config = $config;
    }

    /**
     * 预支付请求接口(POST)
     * 返回json的数据
     */
    public function prepay($payContent)
    {
        $config = $this->config;
        $unifiedorder = [
            'appid'            =>$config['appid'],
            'mch_id'        =>$config['mchid'],
            'nonce_str'         =>self::getNonceStr(),
            'body'         =>$payContent['body'],
            'out_trade_no'   =>$payContent['out_trade_no'],
            'total_fee'         =>$payContent['fee'],
            'spbill_create_ip'=>$_SERVER['REMOTE_ADDR'],
            'notify_url'     =>$config['notify_url'],
            'trade_type'     =>'JSAPI',
            'openid'        =>$payContent['openid']
        ];
        error_log('config ===============>'.json_encode($config));
        $unifiedorder['sign'] =  $this->makeSign($unifiedorder);
        error_log('unifine order param ===============>'.json_encode($unifiedorder));
        //请求数据
        $xmldata =  $this->array2xml($unifiedorder);
        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        $res = $this->request($url, $xmldata);
        if(!$res){
             return $this->errorResult("Can't connect the server");
        }
        $content =  $this->xml2array($res);
        error_log('unifine order result ===============>'.json_encode($content));
        if(strval($content['result_code']) == 'FAIL'){
            return $this->errorResult(strval($content['return_msg']));
        }
        if(strval($content['return_code']) == 'FAIL'){
            return $this->errorResult(strval($content['return_msg']));
        }
        //拼接小程序的接口数据
        $resData = [
            'appId'       => strval($content['appid']),
            'timeStamp'    => time(),
            'nonceStr' =>  $this->getNonceStr(),
            'package'  => 'prepay_id='.strval($content['prepay_id']),
            'signType' => 'MD5'
        ];
        //加密签名
        $resData['paySign'] =  $this->makeSign($resData);
        return $this->successResult($resData);
    }


    /**
     * @return array|bool
     * 微信支付回调验证
     * 返回数据
     */
    public function notify(){
        //$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
        error_log("wechat pay notify message ============>");
        $xml = file_get_contents('php://input');
        //将服务器返回的XML数据转化为数组
        $data =  $this->xml2array($xml);
        // 保存微信服务器返回的签名sign
        $dataSign = $data['sign'];
        // sign不参与签名算法
        unset($data['sign']);
        $sign =  $this->makeSign($data);
        // 判断签名是否正确  判断支付状态
        $result = false;
        error_log("return data ============>".json_encode($data));
        //验证订单是否已经支付,调用订单查询接口
        $isPayment = $this->verifyPament($data);
        error_log("isPayment ============>".$isPayment);
        if($isPayment && ($data['return_code']=='SUCCESS') && ($data['result_code']=='SUCCESS')) {
            error_log("isPayment success============>");
            $outTradeNo = $data['out_trade_no'];
            $concurrentTime = 30;
            $lockKey = getCacheKey('redis_key.cache_key.zset_list.lock') . $outTradeNo;
            //采用并发锁控制并发
            SeminarOrderService::doWithLock(function()use(&$result , $data){
                $result = $data;
                $this->setPaidSuccess($data);
            },$lockKey,$concurrentTime);
        }else{
            error_log("isPayment failed============>");
            $this->setPaidFail($data);
        }
        // 返回状态给微信服务器
        if($result){
            $str='';
        }else {
            $str='';
        }
        return $str;
    }


    /**
     * 支付成功
     */
    public function setPaidSuccess($data){
        error_log('current paid  data =============>'.json_encode($data));
        $paidType = substr($data['out_trade_no'], 14, 4);
        error_log('current paid type is =============>'.$paidType);
        switch ($paidType){
            case '0001' :
                SeminarOrderService::setOrderPaid($data);
                break;
            case '0002':
                ShopGoodsOrderService::setOrderPaid($data);
                break;
        }
    }

    /**
     * 支付失败
     */
    public function setPaidFail($data){
        $paidType = intval(substr($data['out_trade_no'], 14, 4));
        LogHelper::info('current paid type is =============>'.$paidType);
        switch ($paidType){
            case '0001' :
                SeminarOrderService::setOrderPaidFailed($data);
                break;
            case '0002':
                ShopGoodsOrderService::setOrderPaidFailed($data);
                break;
        }
    }


    /**
     * 验证支付的问题
     */
    public function verifyPament($wxPayResp){
        error_log("verify paymnent method=======>".json_encode($wxPayResp));
        $url = "https://api.mch.weixin.qq.com/pay/orderquery";
        //检测必填参数
        if(!$wxPayResp['transaction_id'] && !$wxPayResp['out_trade_no']) {
            error_log("订单查询接口中,out_trade_no、transaction_id至少填一个!");
            return false;
        }
        error_log("开始查询==============》接口");
        $config = BusinessHelper::getWechatPayConfig();
        error_log("post config ==============》".json_encode($config));
        error_log("transaction is===============>".$wxPayResp['transaction_id']);
        error_log("appid is===============>".$config['appid']);
        error_log("transaction is===============>".$config['mchid']);
        error_log("nonce_string is===============>".$this->getNonceStr());
        $params = [
            'appid' => $config['appid'],
            'mch_id' => $config['mchid'],
            'nonce_str' => $this->getNonceStr(),
            'transaction_id' => $wxPayResp['transaction_id']
        ];
        error_log("post PARAM without sign==============》");
        $params['sign'] = $this->makeSign($params);
        error_log("post PARAM0 with sign ==============》");
        $xmlData = $this->array2xml($params);
        $response = $this->request($url,$xmlData);
        if(!$response){
            error_log("接口请求错误:");
            return false;
        }
        $result =  $this->xml2array($response);
        error_log("查询订单接口返回结果:".json_encode($result));
        if(array_key_exists("return_code", $result)
            && array_key_exists("trade_state", $result)
            && $result["return_code"] == "SUCCESS"
            && $result["trade_state"] == "SUCCESS"){
            return true;
        }
        return false;
    }



//---------------------------------------------------------------用到的函数------------------------------------------------------------
    /**
     * 错误返回提示
     * @param string $errMsg 错误信息
     * @param string $status 错误码
     * @return  array json的数据
     */
    protected function errorResult($errMsg = 'error', $status = Constant::PAID_RESULT_FAILED)
    {
        return [
            'status'=>$status,
            'result'=>'fail',
            'data'=>$errMsg
        ];
    }


    /**
     * 正确返回
     * @param  array $data 要返回的数组
     * @return  array json的数据
     */
    protected function successResult($data=[]){
        return [
            'status'=> Constant::PAID_RESULT_SUCCESS,
            'result'=>'success',
            'data'=>$data
        ];
    }

    /**
     * 将一个数组转换为 XML 结构的字符串
     * @param array $arr 要转换的数组
     * @param int $level 节点层级, 1 为 Root.
     * @return string XML 结构的字符串
     */
    protected function array2xml($arr, $level = 1){
        $s = $level == 1 ? "" : '';
        foreach($arr as $tagname => $value) {
            if (is_numeric($tagname)) {
                $tagname = $value['TagName'];
                unset($value['TagName']);
            }
            if(!is_array($value)) {
                $s .= "<{$tagname}>".(!is_numeric($value) ? '' : '')."";
            }else {
                $s .= "<{$tagname}>" . $this->array2xml($value, $level + 1)."";
            }
        }
        $s = preg_replace("/([\x01-\x08\x0b-\x0c\x0e-\x1f])+/", ' ', $s);
        return $level == 1 ? $s."" : $s;
    }

    /**
     * 将xml转为array
     * @param  string  $xml xml字符串
     * @return array    转换得到的数组
     */
    protected function xml2array($xml)
    {
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $result= json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $result;
    }

    /**
     *
     * 产生随机字符串,不长于32位
     * @param int $length
     * @return 产生的随机字符串
     */
    protected function getNonceStr($length = 32){
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str ="";
        for ( $i = 0; $i < $length; $i++ ) {
            $str .= substr($chars, mt_rand(0, strlen($chars)-1), 1);
        }
        return $str;
    }

    /**
     * 生成签名
     * @return 签名
     */
    protected function makeSign($data){
        //获取微信支付秘钥
        $key = $this->config['mch_secret'];
        //去空
        $data = array_filter($data);
        //签名步骤一:按字典序排序参数
        ksort($data);
        $signParam = http_build_query($data);
        $signParam = urldecode($signParam);
        //签名步骤二:在string后加入KEY
        $signContent = $signParam."&key=".$key;
        //签名步骤三:MD5加密
        $sign = md5($signContent);
        // 签名步骤四:所有字符转为大写
        $result=strtoupper($sign);
        return $result;
    }

    /**
     * 微信支付发起请求
     */
    protected function request($url, $xmldata, $second=30, $aHeader=array()){
        $ch = curl_init();
        //超时时间
        curl_setopt($ch,CURLOPT_TIMEOUT,$second);
        curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1);
        //这里设置代理,如果有的话
        //curl_setopt($ch,CURLOPT_PROXY, '10.206.30.98');
        //curl_setopt($ch,CURLOPT_PROXYPORT, 8080);
        curl_setopt($ch,CURLOPT_URL,$url);
        curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
        curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,false);
        if( count($aHeader) >= 1 ){
            curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader);
        }
        curl_setopt($ch,CURLOPT_POST, 1);
        curl_setopt($ch,CURLOPT_POSTFIELDS,$xmldata);
        $data = curl_exec($ch);
        if($data){
            curl_close($ch);
            return $data;
        }
        else {
            $error = curl_errno($ch);
            echo "call faild, errorCode:$error\n";
            curl_close($ch);
            return false;
        }
    }


}

踩坑点

1、支付回调接口http://test.dev.com/wechat/pa... 一定要设置成get、post都能访问,我当初只设置了get请求可以访问,浪费了好多时间进行排查,而微信回调的数据基本都是以post形式进行调用的

你可能感兴趣的:(php)