微信支付PHP类封装

一:类说明

Tool.php: 工具类,封装了xml转数组 、数组转xml、curl post请求 、curl get请求、获取用户openid等方法;

WxPayApi.php: 微信支付API列表类,例如统一下单接口等;

WxPayConfig.php: 微信配置类;

WxPayException: 异常处理类;

WxPayDataBase: 数据基类;

WxPayNotify:支付异步通知应答类;

二:代码如下

1.工具类:Tool.php

  <?php 
  namespace wexin\pay;
  class Tool
  {
        /**
     * 生成随机字符串,不长于32位
     *
     * @param int $length
     * @return string
     */
    public static 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;
    }/*}}}*/

    /**
     * array 转为 xml
     *
     * @param array $data | object
     * @return string
     * @throws WxPayException
     */
    public static function arrayToXml($data = [])
    {/*{{{*/
        if (!is_array($data) || count($data) == 0) {
            throw new WxPayException('数组数据异常!');
        }
        $xml = '<xml>';
        foreach ($data as $key => $val) {
            if (is_numeric($val)) {
                $xml .= '<' . $key . '>' . $val . '</' . $key . '>';
            } else {
                $xml .= '<' . $key . '><![CDATA[' . $val . ']]></' . $key . '>';
            }
        }
        $xml .= '</xml>';
        return $xml;
    }/*}}}*/

    /**
     * xml 转为 array
     *
     * @param string $xml
     * @return mixed
     * @throws WxPayException
     */
    public static function xmlToArray($xml = '')
    {/*{{{*/
        if (empty($xml))
            throw new WxPayException('xml数据异常!');
        // 将XML转为array
        // 禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $data;
    }/*}}}*/

    /**
     * 格式化参数为URL参数
     *
     * @param array $data
     * @return string
     */
    public static function toUrlParams($data = [])
    {/*{{{*/
        $buff = '';
        foreach ($data as $key => $val) {
            if ($key != 'sign' && $val != '' && !is_array($val)) {
                $buff .= $key . '=' . $val . '&';
            }
        }
        $buff = trim($buff, '&');
        return $buff;
    }/*}}}*/

    /**
     * 生成签名
     *
     * @param array $data
     * @return string
     */
    public static function makeSign($data = [])
    {/*{{{*/
        // 签名步骤一:按字典排序参数
        ksort($data);
        $string = self::toUrlParams($data);
        // 签名步骤二:在string后加入key
        $string = $string . '&key=' . WxPayConfig::KEY;
        // 签名步骤三:MD5加密
        $string = md5($string);
        // 签名步骤四:所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }/*}}}*/

    /**
     * 获取客户端IP地址
     *
     * @param int $type 返回类型 0 返回IP地址 1 返回IPV4地址数字
     * @param bool $adv 是否进行高级模式获取(有可能被伪装)
     * @return mixed
     */
    public static function getClientIP($type = 0, $adv = false)
    {/*{{{*/
        $type      = $type ? 1 : 0;
        static $ip = null;
        if (null !== $ip) {
            return $ip[$type];
        }

        if ($adv) {
            if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
                $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
                $pos = array_search('unknown', $arr);
                if (false !== $pos) {
                    unset($arr[$pos]);
                }
                $ip = trim(current($arr));
            } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
                $ip = $_SERVER['HTTP_CLIENT_IP'];
            } elseif (isset($_SERVER['REMOTE_ADDR'])) {
                $ip = $_SERVER['REMOTE_ADDR'];
            }
        } elseif (isset($_SERVER['REMOTE_ADDR'])) {
            $ip = $_SERVER['REMOTE_ADDR'];
        }
        // IP地址合法验证
        $long = sprintf("%u", ip2long($ip));
        $ip   = $long ? [$ip, $long] : ['0.0.0.0', 0];
        return $ip[$type];
    }/*}}}*/

    /**
     * 获取毫秒级时间戳
     *
     * @return array|string
     */
    public static function getMillisecond()
    {/*{{{*/
        $time  = explode (" ", microtime ());
        $time  = $time[1] . ($time[0] * 1000);
        $time2 = explode( ".", $time );
        $time  = $time2[0];
        return $time;
    }/*}}}*/
    
    /**
     * post方式提交数据到对应的接口
     *
     * @param string $data    需要post的xml数据
     * @param $url string    url
     * @param bool $useCert 是否需要证书,默认不需要
     * @param int $timeout   请求执行超时时间,默认30s
     * @return mixed
     * @throws WxPayException
     */
    public static function postCurl($data, $url, $useCert = false, $timeout = 30)
    {/*{{{*/
        $ch = curl_init();
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

        // 如果有配置代理这里就设置代理
        if (WxPayConfig::CURL_PROXY_HOST != "0.0.0.0"
            && WxPayConfig::CURL_PROXY_PORT != 0) {
            curl_setopt($ch, CURLOPT_PROXY, WxPayConfig::CURL_PROXY_HOST);
            curl_setopt($ch, CURLOPT_PROXYPORT, WxPayConfig::CURL_PROXY_PORT);
        }
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // 严格校验
        // 设置header
        curl_setopt($ch, CURLOPT_HEADER, false);
        // 要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        if ($useCert == true) {
            // 设置证书
            // 使用证书:cert 与 key 分别属于两个.pem文件
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, WxPayConfig::SSLCERT_PATH);
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, WxPayConfig::SSLKEY_PATH);
        }
        // post方式提交数据
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        // 运行curl
        $result = curl_exec($ch);
        // 返回结果
        if ($result) {
            curl_close($ch);
            return $result;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            throw new WxPayException('curl出错,错误码: ' . $error);
        }
    }/*}}}*/

    /**
     * curl GET提交
     *
     * @param $url
     * @param int $timeout
     * @return mixed
     */
    public static function getCurl($url, $timeout = 5)
    {/*{{{*/
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // 设置超时
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
 
    }/*}}}*/

    /**
     * 获取用户openid
     */
    public static function getOpenid()
    {
        // 通过code获取openid
        if (!isset($_GET['code'])) {
            // 触发微信返回code码
            $url     = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . $_SERVER['QUERY_STRING'];
            $baseUrl = urlencode($url);
            $redirectUrl = self::createOauthUrlForCode($baseUrl);
            header("Location: $redirectUrl");
            exit();
        } else {
            // 获取code,通过code获取openid
            $code = $_GET['code'];
            // 构造获取openid的链接
            $url  = self::createOauthUrlForOpenid($code);
            // 发送get请求
            $data = self::getCurl($url);
            $data = json_decode($data, true);
            return $data['openid'];

        }
    }

    /**
     * 构造获取code的url连接
     *
     * @param string $redirectUrl 微信服务器回跳的url,需要url编码
     * @return string
     */
    public static function createOauthUrlForCode($redirectUrl)
    {
        $params['appid']         = WxPayConfig::APPID;
        $params['redirect_uri']  = $redirectUrl;
        $params['response_type'] = 'code';
        $params['scope']         = 'snsapi_base';
        $params['state']         = 'STATE#wechat_redirect';
        $queryString = self::toUrlParams($params);
        return 'https://open.weixin.qq.com/connect/oauth2/authorize?' . $queryString;
    }

    /**
     * 构造通过code获取openid的url链接
     *
     * @param $code
     * @return string
     */
    public static function createOauthUrlForOpenid($code)
    {
        $params['appid']      = WxPayConfig::APPID;
        $params['secret']     = WxPayConfig::APPSECRET;
        $params['code']       = $code;
        $params['grant_type'] = 'authorization_code';
        $queryString = self::toUrlParams($params);
        return 'https://api.weixin.qq.com/sns/oauth2/access_token?' . $queryString;
    }
  }

2.微信支付API列表类:WxPayApi.php

<?php
namespace wexin\pay;
class WxPayApi
{
     /**
     * 统一下单请求地址
     */
    const UNIFORM_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder';

    /**
     * 统一下单接口
     * 
     * @param $inputObj
     * @param int $timeOut
     * @return mixed
     * @throws WxPayException
     */
    public static function unifiedOrder($inputObj, $timeOut = 6)
    {
        // 检测必填参数
        if (!isset($inputObj->appid)) {
            throw new WxPayException('缺少统一下单接口必填参数公众号ID:appid!');
        } elseif (!isset($inputObj->mch_id)) {
            throw new WxPayException('缺少统一下单接口必填参数商户号:mch_id!');
        } elseif (!isset($inputObj->sub_mch_id)) {
            throw new WxPayException('缺少统一下单接口必填参数子商户号:sub_mch_id!');
        } elseif (!isset($inputObj->sign)) {
            throw new WxPayException('缺少统一下单接口必填参数签名:sign!');
        } elseif (!isset($inputObj->body)) {
            throw new WxPayException('缺少统一下单接口必填参数商品描述:body!');
        } elseif (!isset($inputObj->out_trade_no)) {
            throw new WxPayException('缺少统一下单接口必填参数商户订单号:out_trade_no!');
        } elseif (!isset($inputObj->total_fee)) {
            throw new WxPayException('缺少统一下单接口必填参数总金额:total_fee!');
        } elseif (!isset($inputObj->notify_url)) {
            throw new WxPayException('缺少统一下单接口必填参数通知地址:notify_url!');
        } elseif (!isset($inputObj->trade_type)) {
            throw new WxPayException('缺少统一下单接口必填参数交易类型:trade_type!');
        } elseif (!isset($inputObj->nonce_str)) {
            throw new WxPayException('缺少统一下单接口必填参数随机字符串:nonce_str!');
        } elseif (!isset($inputObj->spbill_create_ip)) {
            throw new WxPayException('缺少统一下单接口必填参数终端IP:spbill_create_ip!');
        }
        // 关联参数
        if ($inputObj->trade_type == 'JSAPI' &&  !isset($inputObj->openid)) {
            throw new WxPayException("统一下单接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");
        }
        if($inputObj->trade_type == 'NATIVE' && !isset($inputObj->product_id)) {
            throw new WxPayException("统一下单接口中,缺少必填参数product_id!trade_type为NATIVE时,product_id为必填参数!");
        }
        
        // 格式化xml数据
        $xml = Tool::arrayToXml($inputObj->getValues());
        // 请求开始时间
//        $startTimeStamp = Tool::getMillisecond();
        // 发送请求
        $response = Tool::postCurl($xml, self::UNIFORM_ORDER_URL, false, $timeOut);
        $result   = Tool::xmlToArray($response);
        return $result;
    } 
}

3.配置类:WxPayConfig.php

<?php
namespace weixin\pay;
class WxPayConfig
{
      /**
     * 微信公众号信息配置
     *
     * APPID: 绑定支付的APPID,必须配置
     * MCHID: 商户号 必须配置
     * KEY: 商户支付密钥,必须配置
     * APPSECRET: 公众号secert (仅JSAPI支付的时候需要配置, 登录公众平台,进入开发者中心可设置) 
     * @var string 
     */
    const APPID = '';
    const MCHID = '';
    const KEY   = '';
    const APPSECRET = '';
    //=======【证书路径设置】==================================
    /**
     * 证书路径,绝对路径(仅退款、撤销订单时需要)
     */
    const SSLCERT_PATH = '../cert/apiclient_cert.pem';
    const SSLKEY_PATH  = '../cert/apiclient_key.pem';

    //=======【curl代理设置】===================================
    const CURL_PROXY_HOST = "0.0.0.0";
    const CURL_PROXY_PORT = 0;

    //=======【上报信息配置】===================================
    const REPORT_LEVENL = 1;

    //=======【支付异步通知URL】===================================
    const NOTIFY_URL = '';
}

4.异常类:WxPayException.php

<?php
namespace weixin\pay;
class WxPayException extends \Exception
{
     /**
     * @return string
     */
    public function errorMessage()
    {
        return $this->getMessage();
    }
}

5.数据基类:WxPayDataBase.php

<?php
namespace weixin\pay;
class WxPayDataBase
{
     /**
     * @var array
     */
    protected $values = [];

    /**
     * @param $name
     * @param $value
     */
    public function __set($name, $value)
    {
        $this->values[$name] = $value;
    }

    /**
     * __get 
     * 
     * @param mixed $name 
     * @access public
     * @return void
     */
    public function __get($name)
    {
        return $this->values[$name];
    }

    /**
     * __isset 
     * 
     * @param mixed $name 
     * @access public
     * @return void
     */
    public function __isset($name)
    {
        return array_key_exists($name, $this->values); 
    }

    /**
     * setSign 设置签名
     * 
     * @return void
     */
    public function setSign()
    {
        $sign = Tool::makeSign($this->values);
        $this->values['sign'] = $sign; 
    }

    /**
     * getValues 获取设置的值
     * 
     * @return array
     */
    public function getValues()
    {
        return $this->values;
    }
}

6.支付异步通知应答类: WxPayNotify.php

<?php
namespace weixin\pay;
/**
 * 微信支付异步通知应答
 *
 * Class WxPayNotify
 * @package weixin\pay
 */
class WxPayNotify extends WxPayDataBase
{

    /**
     * @param $xml
     */
    public function replayNotify($xml)
    {
        echo $xml;
    }
}

使用如下:

// 统一下单
<?php
 $WxPayData               = new WxPayDataBase();                          
 $WxPayData->appid        = WxPayConfig::APPID;                           
 $WxPayData->mch_id       = WxPayConfig::MCHID;                           
 $WxPayData->sub_mch_id   = '1508144001';                                 
 $WxPayData->body         = '商品';                                   
 $WxPayData->out_trade_no = Tool::getNonceStr();                          
 // 金额:分                                                              
 $WxPayData->total_fee    = 1;                                            
 $WxPayData->notify_url   = 'https://www.qq.com/Wx/WxPay/notify';  
 $WxPayData->trade_type   = 'JSAPI';                                      
 $WxPayData->openid       = '你的openid';               
 // 随机字符串                                                            
 $WxPayData->nonce_str        = Tool::getNonceStr();                      
 // 终端ip                                                                
 $WxPayData->spbill_create_ip = Tool::getClientIP();                      
 $WxPayData->setSign();                                                   
 try {                                                                    
     $result = WxPayApi::unifiedOrder($WxPayData);                                                                       
 } catch (WxPayException $e) {                                            
     die($e->getMessage());                                               
 }        

// 支付异步通知处理函数
function notify()
{
        $xml   = file_get_contents('php://input');   
        $array = Tool::xmlToArray($xml);
        // 记录日志
        file_put_contents('/Data/logs/app/store/notify.log', json_encode($array) . PHP_EOL, FILE_APPEND);
        // 商户应答
        $notify = new WxPayNotify();
        if ($array['result_code'] == 'SUCCESS' && $array['return_code'] == 'SUCCESS') {
            $result_sign = $array['sign'];
            unset($array['sign']);
            $sign = Tool::makeSign($array); 
            // 验证签名 
            if ($sign == $result_sign) {
                $notify->return_code = 'SUCCESS';
                $notify->return_msg  = 'OK';
                $xml = Tool::arrayToXml($notify->getValues());
                $notify->replayNotify();
            } else {
                $notify->return_code = 'FAIL';
                $notify->return_msg  = '签名错误';
                $xml = Tool::arrayToXml($notify->getValues());
                $notify->replayNotify();
            }
            
        } else {
                $notify->return_code = 'FAIL';
                $notify->return_msg  = '支付失败';
                $xml = Tool::arrayToXml($notify->getValues());
                $notify->replayNotify();
        }
}                                                                

jsapi发起支付

微信内H5调起支付

<!DOCTYPE html>
<html lang="en">
<head>
    <title>微信支付样例-支付</title>
    <script>
    function jsApiCall()
    {
        WeixinJSBridge.invoke(
            'getBrandWCPayRequest',
            {
                "appId": "",
                "timeStamp": "",
                "nonceStr": "",
                "package": "",
                "signType": "",
                "paySign": "",
            },
            function(res) {
                WeixinJSBridge.log(res.err_msg);
                alert(res.err_code+res.err_desc+res.err_msg);
            }
        );
    }   
    function callpay()
    {
    
        if (typeof WeixinJSBridge == "undefined"){
            if ( document.addEventListener ) {
                document.addEventListener('WeixinJSBridgeReady', jsApiCall, false);
            } else if (document.attachEvent) {
                document.attachEvent('WeixinJSBridgeReady', jsApiCall); 
                document.attachEvent('onWeixinJSBridgeReady', jsApiCall);
            }
        } else {
            jsApiCall();
        }
    }
    </script>
</head>
<body>
    <br/>
    <font color="#9ACD32"><b>该笔订单支付金额为<span style="color:#f00;font-size:50px">1分</span>钱</b></font><br/><br/>
    <div align="center">
        <button style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" >立即支付</button>
    </div>
</body>
</html>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容