一、小程序支付流程

二、详细步骤

准备工作

发起支付按钮绑定事件。

<!--index.wxml-->

<view class="container">
    <button type="success" bindtap="payLoading">支付</button> </view>

wx.login获取ticket

// index.js

var app = getApp()
Page({

  setLoading: function() {
    var that = this
    wx.login({
      success: function(res) {
        // 成功的话会返回:
        // {errMsg: "login:ok", code: "获取用户OpenID的ticket"}
        that.getOpenId(res.code)
      }
    })
  }

})

获得OpenID

小程序得到ticket后,不能自己获得用户OpenID(微信规定的),因此通过服务器代理

// index.js

var app = getApp()
Page({

  payLoading: function() { ... } getOpenId: function(jsCode) { var that = this wx.request({ url: 'https://myserver.com/login.php', data: { js_code: jsCode // wx.login()时得到的ticket }, success: function (res) { that.getPrePayId(res.data.openid) } }) }, getPrePayId: function() { // 后面讲到 } })

统一下单

得到OpenID后,再次请求服务器,让服务器代理调用统一下单接口:

// index.js

var app = getApp()
Page({

  payLoading: function() { ... },
  getOpenId: function() { ... },

  getPrePayId: function(openId) {
    var that = this
    wx.request({
      url: 'https://myserver.com/pay.php',
      data: {
        openid: openId
      },
      success: function(res) {
        that.pay(res.data)
      }
    })
  },

  pay: function() { // 后面讲到 }

})  

wx.requestPayment() 支付

最后,小程序发起实际的支付,界面上会弹出支付窗口

// index.js

var app = getApp()
Page({

  payLoading: function() { ... },
  getOpenId: function() { ... },
  getPrePayId: function() { ... },

  // data是服务端返回的JSON
  // 加上success、fail回调后,正好符合wx.requestPayment()参数的格式

  pay: function(data) {
    data.success = function(res) {
      // 用户支付成功后的代码
    }
    data.fail = function(res) {
      // 用户支付失败后的代码
    }
    wx.requestPayment(data)
  }

})  

三、后端统一逻辑处理

<?php

namespace app\controller;

use app\controller\Api;
use app\model\Order as OrderModel;
use app\model\Config as ConfigModel;

/**
 * 支付接口 
 */
class Pay extends Api
{

    protected $noNeedLogin = ['*'];

    protected $noNeedRight = ['*'];

    //微信公众号身份的唯一标识
    protected $APPID = "";//填写您的appid。微信公众平台里的

    // APP SECRECT
    protected $APPSECRET = "";

    //受理商ID,身份标识
    protected $MCHID = "";//商户id

    //商户支付密钥Key
    protected $KEY = "";

    // 订单号
    protected $outTradeNo = "";

    protected $totalFee = "";

    protected $openid = "";

    //回调通知接口
    protected $APPURL =   "";

    //请求ip
    protected $CREATE_IP =  "";

    //交易类型
    protected $TRADETYPE = "";

    //商品类型信息
    protected $BODY = "";

    /***
     * @name 初始化相关参数
     * @date 2018/06/19 14:52
     * @throws
     */
    public function _initialize()
    {

        // 商户号 mch_id
        $mch_id_info = ConfigModel::where("name = 'mch_id'")->find();
        $this->MCHID = isset($mch_id_info['value'])?$mch_id_info['value']:'';

        // 微信小程序 appid
        $wx_appid_info = ConfigModel::where("name = 'wx_appid'")->find();
        $this->APPID = isset($wx_appid_info['value'])?$wx_appid_info['value']:'';

        // 微信小程序 wx_secret
        $wx_secret_info = ConfigModel::where("name = 'wx_secret'")->find();
        $this->APPSECRET = isset($wx_secret_info['value'])?$wx_secret_info['value']:'';

        // 微信小程序 notify_url
        $notify_url_info = ConfigModel::where("name = 'notify_url'")->find();
        $this->APPURL = isset($notify_url_info['value'])?$notify_url_info['value']:'';

        // 微信小程序 create_ip
        $create_ip_info = ConfigModel::where("name = 'create_ip'")->find();
        $this->CREATE_IP = isset($create_ip_info['value'])?$create_ip_info['value']:$_SERVER['REMOTE_ADDR'];

        // 微信小程序 wx_key
        $wx_key_info = ConfigModel::where("name = 'wx_key'")->find();
        $this->KEY = isset($wx_key_info['value'])?$wx_key_info['value']:'';

        $this->TRADETYPE = "JSAPI";      //交易类型

        $this->BODY = "幕府问计/商业咨询";           //商品类型信息

    }

    /***
     * @name 小程序支付
     * @date 2018/06/19 14:52
     * @throws
     */
    public function pay(){

        // 订单号
        $order_number = input('post.order_number')?input('post.order_number'):$this->error("非法请求");

        // openid
        $openid = input('post.openid')?input('post.openid'):$this->error("非法请求");

        $pay_info = OrderModel::where("order_number",$order_number)->find();

        $order_price = isset($pay_info['order_price'])?$pay_info['order_price']:'0.00';

        $this->openid = $openid; //用户唯一标识

        $this->outTradeNo = $order_number; //商品编号

        $this->totalFee = $order_price * 100; //总价

        $result = $this->weixinapp();

        $this->success('get param success',$result);

    }


    //对微信统一下单接口返回的支付相关数据进行处理
    private function weixinapp(){

        $unifiedorder=$this->unifiedorder();

        $parameters=array(
            'appId'=>$this->APPID,//小程序ID
            'timeStamp'=>''.time().'',//时间戳
            'nonceStr'=>$this->createNoncestr(),//随机串
            'package'=>'prepay_id='.$unifiedorder['prepay_id'],//数据包
            'signType'=>'MD5'//签名方式
        );

        $parameters['paySign']=$this->getSign($parameters);

        return $parameters;

    }
    /*
     *请求微信统一下单接口
     */
    private function unifiedorder(){
        $parameters = array(
            'appid' => $this->APPID,//小程序id
            'mch_id'=> $this->MCHID,//商户id
            'spbill_create_ip'=>$this->CREATE_IP,//终端ip
            'notify_url'=>$this->APPURL, //通知地址
            'nonce_str'=> $this->createNoncestr(),//随机字符串
            'out_trade_no'=>$this->outTradeNo,//商户订单编号
            'total_fee'=>floatval($this->totalFee), //总金额
            'openid'=>$this->openid,//用户openid
            'trade_type'=>$this->TRADETYPE,//交易类型
            'body' =>$this->BODY, //商品信息
        );

        $parameters['sign'] = $this->getSign($parameters);

        $xmlData = $this->arrayToXml($parameters);

        $xml_result = $this->postXmlCurl($xmlData,'https://api.mch.weixin.qq.com/pay/unifiedorder',60);

        $result = $this->xmlToArray($xml_result);

        return $result;
    }

    //数组转字符串方法
    protected function arrayToXml($arr){

        $xml = "<xml>";

        foreach ($arr as $key=>$val)
        {
            if (is_numeric($val)){
                $xml.="<".$key.">".$val."</".$key.">";
            }else{
                $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }

        $xml.="</xml>";

        return $xml;
    }

    // xml 转 array
    protected function xmlToArray($xml){
        $array_data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $array_data;
    }

    //发送xml请求方法
    private static function postXmlCurl($xml, $url, $second = 30)
    {
        $ch = curl_init();

        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //严格校验

        //设置header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);

        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);

        //post提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
        curl_setopt($ch, CURLOPT_TIMEOUT, 40);
        set_time_limit(0);

        //运行curl
        $data = curl_exec($ch);

        //返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            throw new WxPayException("curl出错,错误码:$error");
        }
    }

    // 对要发送到微信统一下单接口的数据进行签名
    protected function getSign($Obj){
        foreach ($Obj as $k => $v){
            $Parameters[$k] = $v;
        }
        //签名步骤一:按字典序排序参数
        ksort($Parameters);
        $String = $this->formatBizQueryParaMap($Parameters, false);
        //签名步骤二:在string后加入KEY
        $String = $String."&key=".$this->KEY;
        //签名步骤三:MD5加密
        $String = md5($String);
        //签名步骤四:所有字符转为大写
        $result_ = strtoupper($String);
        return $result_;
    }

    // 排序并格式化参数方法,签名时需要使用
    protected function formatBizQueryParaMap($paraMap, $urlencode)
    {
        $buff = "";
        ksort($paraMap);
        foreach ($paraMap as $k => $v)
        {
            if($urlencode)
            {
                $v = urlencode($v);
            }
            //$buff .= strtolower($k) . "=" . $v . "&";
            $buff .= $k . "=" . $v . "&";
        }
        $reqPar = '';
        if (strlen($buff) > 0)
        {
            $reqPar = substr($buff, 0, strlen($buff)-1);
        }
        return $reqPar;
    }

    // 生成随机字符串方法
    protected function createNoncestr($length = 32 ){
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str ="";
        for ( $i = 0; $i < $length; $i++ ) {
            $str.= substr($chars, mt_rand(0, strlen($chars)-1), 1);
        }
        return $str;
    }

    // 支付回调
    public function callback(){

        $post = $this->post_data();    //接受POST数据XML个数

        $post_data = $this->xmlToArray($post);   //微信支付成功,返回回调地址url的数据:XML转数组Array


        /* 微信官方提醒:
         *  商户系统对于支付结果通知的内容一定要做【签名验证】,
         *  并校验返回的【订单金额是否与商户侧的订单金额】一致,
         *  防止数据泄漏导致出现“假通知”,造成资金损失。
         */

        /***
         * ***********************************************************************************
         * @name 微信支付签名校验 start
         * ***********************************************************************************
         */
        $postSign = $post_data['sign'];         //获取到微信返回的 sign

        // 签名验证
        ksort($post_data);// 对数据进行排序

        $str = $this->ToUrlParams($post_data);//重新生成验证字符串

        $user_sign = strtoupper(md5($str));   //再次生成签名,与$postSign比较

        // 写入日志
        $this->save_log('./wxpaylog', "postSign" . $postSign ."\r\n" . "user_sign" . $user_sign . date("Y-m-d H:i:s")."\r\n");

        if($user_sign !== $postSign){

            // 写入日志
            $this->save_log('./wxpaylog', "微信支付回调签名校验失败!" . date("Y-m-d H:i:s")."\r\n");

            $this->error('微信支付回调签名校验失败!');
        }




        /***
         * ***********************************************************************************
         * @name 微信支付订单金额校验 start
         * ***********************************************************************************
         */
        $where['order_number'] = $post_data['out_trade_no'];

        $order_info = OrderModel::where($where)->find();

        if($order_info['order_price'] == ($post_data['total_fee'] * 0.01)){

            // 写入日志
            $this->save_log('./wxpaylog', "微信支付 实际支付金额与订单金额不符!" . date("Y-m-d H:i:s")."\r\n");

            $this->error('微信支付 实际支付金额与订单金额不符!');

        }










        /***
         * ***********************************************************************************
         * @name 微信支付成功 更新订单状态 start
         * ***********************************************************************************
         */
        if($post_data['return_code']=='SUCCESS'){

            if($order_info['pay_status']=='1'){

                $this->return_success();

            }else{

                // 更改订单支付状态

                $update['pay_status'] = '1';

                if(OrderModel::where($where)->update($update)){

                    // 写入日志
                    $this->save_log('./wxpaylog', "订单".$post_data['out_trade_no']."支付成功" ."\r\n" . date("Y-m-d H:i:s")."\r\n");

                    $this->return_success();

                }

            }

        }else{

            // 更改订单支付状态

            $update['pay_status'] = '2';

            OrderModel::where($where)->update($update);

            // 写入日志
            $this->save_log('./wxpaylog', "订单".$post_data['out_trade_no']."支付失败" ."\r\n" . date("Y-m-d H:i:s")."\r\n");

            $this->error('微信支付失败');

        }

    }

    /**
     * 用户取消支付
     * @param
     * @return string
     */
    public function CancelWxPay(){

        $order_number = input('post.order_number')?input('post.order_number'):$this->error("参数错误");

        $where = array();

        $where['order_number'] = $order_number;

        $update['pay_status'] = '3';

        OrderModel::where($where)->update($update);

        // 写入日志
        $this->save_log('./wxpaylog', "订单".$order_number."用户取消支付" ."\r\n" . date("Y-m-d H:i:s")."\r\n");

        $this->success('记录日志成功');

    }

    /**
     * 将参数拼接为url: key=value&key=value
     * @param
     * @return string
     */
    public function ToUrlParams( $data ){
        //效验签名
        return "appid=".$data['appid']."&bank_type=".$data['bank_type']."&cash_fee=".$data['cash_fee']."&fee_type=".$data

            ['fee_type']."&is_subscribe=".$data['is_subscribe']."&mch_id=".$data['mch_id']."&nonce_str=".$data['nonce_str']."&openid=".

            $data['openid']."&out_trade_no=".$data['out_trade_no']."&result_code=".$data['result_code']."&return_code=".$data

            ['return_code']."&time_end=".$data['time_end']."&total_fee=".$data['total_fee']."&trade_type=".$data

            ['trade_type']."&transaction_id=".$data['transaction_id']."&key=".$this->KEY;
    }

    /**
     * 获取 支付回调数据
     * @param
     * @return string
     */
    function post_data(){
        $receipt = $_REQUEST;
        if($receipt==null){
            $receipt = file_get_contents("php://input");
            if($receipt == null){
                $receipt = $GLOBALS['HTTP_RAW_POST_DATA'];
            }
        }
        return $receipt;
    }


    /**
     * 给微信发送确认订单金额和签名正确,SUCCESS信息 -xzz0521
     * @param
     * @return string
     */
    private function return_success(){
        $return['return_code'] = 'SUCCESS';
        $return['return_msg'] = 'OK';
        $xml_post = '<xml>
                    <return_code>'.$return['return_code'].'</return_code>
                    <return_msg>'.$return['return_msg'].'</return_msg>
                    </xml>';
        echo $xml_post;exit;
    }


    /**
     * 日志功能
     * @param
     * @return string
     */
    public function save_log($path, $msg)
    {
        if (! is_dir($path)) {
            mkdir($path);
        }
        $filename = $path . '/' . date('YmdHi') . '.txt';
        $content = date("Y-m-d H:i:s")."\r\n".$msg."\r\n \r\n \r\n ";
        file_put_contents($filename, $content, FILE_APPEND);
    }


}