第一次做微信支付记录一下: 

  1. 使用企业执照申请, 获得APPID, mch_id, key 等    (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3   微信支付文档里有时序图)
  2. 流程很简单:  用户下单后,  后端生成订单,  然后调用统一下单api,  微信支付系统会生成预付单, 并返回给商户服务器,   支付成功后,微信会通过notify_url返回支付结果给商户后端. 后端可以进一步处理.
  3. 统一下单api:
    接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
    请求参数: APPID, mch_id .......(大约10个左右必填参数)
    转换成XML数据格式, 通过curl 请求.
    如果微信返回成功标识, 还需要对参数进行二次签名

  4. 成功会返回几个参数, 其中包括但不限于: prepay_id, appid, mch_id   (把返回的参数进行二次签名返回给前端, 调起支付)
  5. $xml = file_get_contents('php://input');  //PHP版本大于5.6     $xml = $GLOBALS['HTTP_RAW_POST_DATA'];  //PHP版本小于5.6  //用来接收微信通知的数据,
  6. 遇到的错误: 发送请求, 微信返回 "XML格式错误", 开始不知道怎么排错. 后来在网上看到了一个例子: 使用  $xml = htmlspecialchars($xml); //把XML格式数据显示出来, 看看XML数据有没有少什么参数.  还有一些要注意的地方: 比如说XML编码要求是utf-8的.(我这里默认是utf-8, 不用设置)
  7. 签名失败: 可能的原因很多, 我遇到的有: (1) 因为key 填写的错误(要保证这个是设置秘钥的那个key)  (2) 因为微信文档说了参数值为空的不参与签名,需要过滤掉, 但是total_fee为0的也被过滤掉了, 所以少了这个参数导致签名失败, 解决办法是total_fee为0的就不要下单了. 
  8.  libxml_disable_entity_loader(true); //修复XXE漏洞       参考: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_5
  9. 我使用yii框架做支付遇到的问题: yii配置的路由是默认格式, 因为最开始没有配置美化的URL. 所以导致一个 notify_url 的问题,  事情是这样的: 微信文档里说notify_url  是不能带有参数的, 但是yii默认的URL是 ?r=moduleID/controllerID/actionID 的形式, 使用这个微信是不能通知结果的, 但是如果改成美化的URL, 之前的接口都得变, 后端没什么, 前端就得改改改了. 为了前端不改改改. 只能后端想想办法, 后来的解决办法就是: 在项目的web目录下创建一个PHP文件, 这个是可以直接访问的, 比如web/wxpay.php ,那么就可以通过https://hostName/web/wxpay.php 访问到. (这里注意,域名配置的时候, 配置到项目的根目录 , 如果配置到了index.php,那这个办法就用不了了,只能使用rewrite或者美化URL或者别的方法了)
    $_GET['r'] = 'course/wx-pay/test';    require __DIR__ . '/index.php';
    //在wxpay.php 添加这行代码,微信访问这个文件,在转发到处理结果的控制器方法里

    这里成功了, 紧接着又出现了新的问题, 接受不到微信的通知, 查看nginx/error.log 没发现什么错误, 查看nginx/access.log 发现微信请求报400, 原因:  微信是通过post 请求的, 但是yii 框架接受post请求,会验证csrf, 而微信是么有这个参数的, 所以需要关闭csrf验证机制,只在某个控制器里关闭csrf验证(局部禁止csrf验证), 在/course/wxpay控制器里写下一行代码

    public $enableCsrfValidation = false;   //关闭csrf验证  在控制器中加入这行代码就可以关闭csrf验证, 就能接收微信支付通知结果啦

     

  10. 能够接受到微信的结果通知了, 但是我给微信返回结果,微信没有接收到, 导致微信一直给我发支付通知, 找原因, 发现是因为签名出现了错误, 微信通知结果的参数里有sign, 是为了验证用的. 把这个参数也进行签名了, 所以导致验证失败, 没执行应答微信的逻辑, 只好重新写一个签名的代码
    ksort($arr);
            $buff = '';
            foreach ($arr as $k => $v){
                if($k != 'sign'){
                    $buff .= $k . '=' . $v . '&';
                }
            }
            $stringSignTemp = $buff . 'key=vrjwwwikogt1zs1i0ih3rp2mmayw24';//key为证书密钥
            $sign = strtoupper(md5($stringSignTemp));
    View Code

     

  11. 特别注意: 保存微信参数的时候, 出现了500, 后来发现因为微信返回的time_end是14位的字符串, 而我数据库设置的是int(11), 所以导致服务器错误, 总结: 有时候服务器500, 找问题的时候不能只看代码,忽略了数据库, 极有可能是数据库设置字段约束的时候, 存储字节长度,类型等等有问题.
  12. 附上代码(只供参考, 不能直接使用)
      1 public $enableCsrfValidation = false; //关闭csrf验证(yii接收微信post请求时做的处理)
      2 /**
      3      * 接收用户下单数据
      4      * @param string $token : 用来验证
      5      * @param int $store_id : 区分快应用
      6      * @param int $cid : 课程id
      7      * @param string $phone 用户账号
      8      * @return mixed data
      9      */
     10     public function actionOrder($store_id=null, $cid=null, $phone=null)
     11     {
     12         if($store_id == null){
     13             $data = [
     14                 'code'=>1,
     15                 'msg'=> 'store_id不能为空',
     16                 'data'=>[],
     17             ];
     18             return $data;
     19         }
     20         if($cid == null){
     21             $data = [
     22                 'code'=>1,
     23                 'msg'=> 'cid不能为空',
     24                 'data'=>[],
     25             ];
     26             return $data;
     27         }
     28         if($cid == null){
     29             $data = [
     30                 'code'=>1,
     31                 'msg'=> '请传一个价格',
     32                 'data'=>[],
     33             ];
     34             return $data;
     35         }
     36         $quick_app_id = Store::findOne($store_id)['wechat_app_id']; //根据store_id查找quick_app_id,获取指定的APPID等信息
     37         if(empty($quick_app_id)){
     38             $data = [
     39                 'code'=>1,
     40                 'msg'=> '快应用不存在,请确认store_id参数正确并且后台快应用配置成功',
     41                 'data'=>[],
     42             ];
     43             return $data;
     44         }
     45         //根据id查询课程信息
     46         $course_info = Course::find()->select('fee')->where(['id'=>$cid,])->one(); //防止用户修改付款价格
     47         if(!$course_info){
     48             $data = [
     49                 'code'=>1,
     50                 'msg'=> '没有数据, 请确认cid参数是否正确',
     51                 'data'=>[],
     52             ];
     53             return $data;
     54         }
     55         //生成随机字符串(下面的才是统一下单代码)
     56         $rand = mt_rand(1000,9999).time();
     57         $nonce_str = strval($rand);
     58         $ip = $_SERVER['REMOTE_ADDR'];  //客户端IP
     59         $out_trade_no = date('YmdHis').'-'.date('si-Hm-dY');
     60         $wxpay = WxPay::findOne($quick_app_id);
     61         $wxpay->notify_url = 'https://'.$_SERVER['HTTP_HOST'].'/web/wx-pay.php';
     62         $wxpay->nonce_str = $nonce_str;
     63         //$wxpay->trade_type = 'APP';
     64         $wxpay->total_fee = $course_info['fee'];
     65         $wxpay->spbill_create_ip = $ip;
     66         $wxpay->time_start = date('YmdHis');
     67         $wxpay->time_expire = date('YmdHis', time()+3600);
     68         $wxpay->body = $wxpay->name.'-购买课程';  //需传入应用市场上的APP名字-实际商品名称
     69         $wxpay->out_trade_no = $out_trade_no;
     70         //把订单信息保存到数据库
     71         $order = new Order();
     72         $order->out_trade_no = $out_trade_no;
     73         $order->total_fee = $wxpay->total_fee;
     74         $order->order_time = time();
     75         $order->body = $wxpay->body;
     76         $order->store_id = $store_id;
     77         $order->phone = $phone; //根据phone 和 store_id就可以定位用户了
     78         $order->cid = $cid;
     79         if(!$order->save()){
     80             // 把错误信息添加到log表中保存
     81         }
     82         $dataXML = $wxpay->UniformOrder();
     83         $arr = (array)simplexml_load_string($dataXML, 'SimpleXMLElement', LIBXML_NOCDATA);
     84         //print_r($arr);
     85         if($arr['return_code'] == 'SUCCESS' && $arr['result_code'] == 'SUCCESS'){
     86             //需要进行二次签名
     87             $twoSign['appid'] = $arr['appid'];
     88             $twoSign['partnerid'] = $arr['mch_id'];
     89             $twoSign['prepayid'] = $arr['prepay_id'];
     90             $twoSign['noncestr'] = $arr['nonce_str'];
     91             $twoSign['package'] = 'Sign=WXPay';  //目前固定值
     92             $twoSign['timestamp'] = time();
     93             $twoSign['sign'] = $wxpay->getSign($twoSign, $wxpay->key);
     94             $data = [
     95                 'code' => 0,
     96                 'msg' => 'success',
     97                 'data' => $twoSign,
     98             ];
     99             return $data;
    100         }else{
    101             //fail
    102             $data = [
    103                 'code' => 1,
    104                 'msg' => 'fail',
    105                 'data' => ['errmsg'=>$arr],
    106             ];
    107             return $data;
    108         }
    109     }
    View Code
    /**
         * 微信回调
         */
        public function actionNotify()
        {
            libxml_disable_entity_loader(true); //修复XXE漏洞
    
            $postStr = file_get_contents('php://input'); //php raw data , require php version > 5.6
            $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
            $arr = json_decode(json_encode($postObj), true); #对象转成数组
            //处理一下key匹配对应的快应用问题, 通过订单号查询订单, 在订单表有store_id,根据store_id可以查询到对应的快应用配置的key
            $order = Order::findOne(['out_trade_no'=>$arr['out_trade_no']]);
            if(!$order){
                return false;
            }
            $store = Store::findOne($order->store_id);
            if(!$store){
                return false;
            }
            $wxpay = WxPay::findOne($store->wechat_app_id);
            if(!$wxpay){
                return false;
            }
            if($arr['return_code'] != 'SUCCESS'){
                echo '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
                return ;
            }
            //签名
            ksort($arr);
            $buff = '';
            foreach ($arr as $k => $v){
                if($k != 'sign'){
                    $buff .= $k . '=' . $v . '&';
                }
            }
            $stringSignTemp = $buff . "key={$wxpay->key}";//key为证书密钥
            //$stringSignTemp = $buff . "key=vrjwwwidkogt1zs1i0ih3rp2mmayw24j";//key为证书密钥
            $sign = strtoupper(md5($stringSignTemp));
            if($sign == $arr['sign']){
                //验证签名成功, 处理商户订单逻辑, 注意需要给微信返回接收信息成功的通知  signature successfully
                $session = \Yii::$app->session;
                $session->set('notify', $arr['return_code']);
                //先把结果写在文件里, 看一下结果
                //$paylog = \Yii::$app->basePath.'/web/paylog.txt';
                //$str_arr = var_export($arr, true);
                //$res = file_put_contents($paylog, $str_arr, FILE_APPEND);
                //$order = Order::findOne(['out_trade_no'=>$arr['out_trade_no']]);//上面查询了
                $order->is_pay = 1;
                $order->pay_time = $arr['time_end']; //数据库里这个存储的是int11,而微信返回的是字符串14位
                $order->transaction_id = $arr['transaction_id'];
                $order->openid = $arr['openid'];
                $order->save();
                //用户支付成功了, 把课程添加到用户订阅课程里user_course表
                $r = (new UserCourse())->addSubscribe($order->phone, $order->cid);// 是这一行的原因吗
                if(!$order->save()){
                    return false;
                }
                return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
            }else{
                //fail
                return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
            }
        }
    View Code

    参考: https://www.jianshu.com/p/52bcadca67bc