微信扫码支付Wxpay.class.php

在经过微信扫码支付DEMO的蹂躏后,决定将繁复杂乱的DEMO文件重新整合封装,最后就有了这个清晰简洁的Wxpay.class.php文件了。

终极版 Wxpay.class.php
<?php
/**
 * Wxpay 自定义微信扫码类
 * 主要是将支微信支付DEMO中的方法整合成了一个文件
 * @version 1.0 2016-07-06
 */
class Wxpay {

    // 交易类型 公众号支付
    const TRADE_TYPE_JSAPI = 'JSAPI';
    // 交易类型 原生扫码支付
    const TRADE_TYPE_NATIVE = 'NATIVE';
    // 交易类型 app支付
    const TRADE_TYPE_APP = 'APP';

    // 统一下单接口
    const URL_UNIFIED_ORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";

    // 订单查询接口
    const URL_ORDER_QUERY = "https://api.mch.weixin.qq.com/pay/orderquery";

    // 关单接口
    const URL_CLOSE_ORDER = 'https://api.mch.weixin.qq.com/pay/closeorder';

    // 申请退款接口
    const URL_REFUND = 'https://api.mch.weixin.qq.com/secapi/pay/refund';

    // 查询退款接口
    const URL_REFUND_QUERY = 'https://api.mch.weixin.qq.com/pay/refundquery';

    // 下载对账单接口
    const URL_DOWNLOAD_BILL = 'https://api.mch.weixin.qq.com/pay/downloadbill';

    // 交易保障接口
    const URL_REPORT = 'https://api.mch.weixin.qq.com/payitil/report';

    // 转换短链接接口
    const URL_SHORT_URL = 'https://api.mch.weixin.qq.com/tools/shorturl';

    const URL_MICRO_PAY = 'https://api.mch.weixin.qq.com/pay/micropay';



    // 微信支付配置数组
    // appid        公众账号appid
    // mch_id       商户号
    // apikey       加密key
    // appsecret    公众号appsecret
    // sslcertPath  证书路径(apiclient_cert.pem)
    // sslkeyPath   密钥路径(apiclient_key.pem)
    private $_config;


    /**
     * 构造方法 初始化微信支付配置
     * @access public
     * @param $config 微信支付配置数组
     */
    public function __construct($config){

        $this->_config = $config;

    }

    /**
     * unifiedOrder 统一下单
     * @access public
     * @param array 统一下单参数数组
     * @return array
     */
    public function unifiedOrder($para = array()) {
        // out_trade_no 商户系统订单 必填
        if ( ! isset($para['out_trade_no'])) return false;
        // body 商品描述/订单描述 必填
        if ( ! isset($para['body'])) return false;
        // total_fee 总金额 必填
        if ( ! isset($para['total_fee'])) return false;
        // spbill_create_ip 终端IP 必填
        if ( ! isset($para['spbill_create_ip'])) $para['spbill_create_ip'] = $_SERVER['REMOTE_ADDR'];
        // APPID 公众账号ID 必填
        $para['appid'] = $this->_config['APPID'];
        // MCHID 商户号 必填
        $para['mch_id'] = $this->_config['MCHID'];
        // device_info 设备号
        if ( ! isset($para['device_info'])) $para['device_info'] = 'WEB';
        // nonce_str 32位随机字符串
        $para['nonce_str'] = $this->getNonceStr();
        // detail 商品详情 可选
        if ( ! isset($para['detail'])) $para['detail'] = '';
        // attach 附加数据 可选
        if ( ! isset($para['attach'])) $para['attach'] = '';
        // fee_type 货币类型 可选
        if ( ! isset($para['fee_type'])) $para['fee_type'] = 'CNY';
        // time_start 交易起始时间 可选
        if ( ! isset($para['time_start'])) $para['time_start'] = '';
        // time_expire 交易结束时间 可选
        if ( ! isset($para['time_expire'])) $para['time_expire'] = '';
        // goods_tag 商品标记 可选
        if ( ! isset($para['goods_tag'])) $para['goods_tag'] = '';
        // notify_url 通知地址 必填
        if ( ! isset($para['notify_url'])) $para['notify_url'] = $this->_config['NOTIFY_URL'];
        // trade_type 交易类型
        if ( ! isset($para['trade_type'])) $para['trade_type'] = 'NATIVE';
        if ($para['trade_type'] == 'NATIVE') {
            // product_id 商品ID 自定义 trade_type=NATIVE时 必填
            if ( ! isset($para['product_id'])) $para['product_id'] = $this->_config['PRODUCT_ID'];
            if ( ! isset($para['openid'])) $para['openid'] = '';
        }
        if ($para['trade_type'] == 'JSAPI') {
            if ( ! isset($para['product_id'])) $para['product_id'] = '';
            // openid 用户标识 trade_type=JSAPI时 必填
            if ( ! isset($para['openid'])) return false;
        }
        
        return $this->getHttpResponsePOST(self::URL_UNIFIED_ORDER, $para);
    }

    /**
     * getCodeUrl 扫码支付 模式二 获取支付二维码
     * @access public
     * @param array $para 支付参数数组
     * @return string
     */
    public function getCodeUrl($para = array()){
        $r = $this->unifiedOrder($para);
        if ($r['return_code'] == 'SUCCESS') {
            if ($r['result_code'] == 'SUCCESS') {
                return $r['code_url'];
            } else {
                $this->logDebug('#获取支付CODE_URL错误# '.$r['err_code'].' : '.$r['err_code_des']);
                return false;
            }
        } else {
            $this->logDebug('#获取支付CODE_URL错误# '.$r['return_msg']);
            return false;
        }
    }

    /**
     * orderQuery 查询订单
     * @access public
     * @param string $order_code
     * @param bool $mode
     * @return array
     */
    public function orderQuery($order_code, $mode = false){
        $para = array();
        $para['appid'] = $this->_config['APPID'];
        $para['mch_id'] = $this->_config['MCHID'];
        $para['nonce_str'] = $this->getNonceStr();
        if ($mode) {    // 使用微信订单号
            $para['transaction_id'] = $order_code;
        } else {    // 使用商户订单号
            $para['out_trade_no'] = $order_code;
        }

        return $this->getHttpResponsePOST(self::URL_ORDER_QUERY, $para);
    }

    /**
     * closeOrder 关闭订单
     * @access public
     * @param string $out_trade_no
     * @return array
     */
    public function closeOrder($out_trade_no){
        $para = array(
            'appid'        => $this->_config['APPID'],
            'mch_id'       => $this->_config['MCHID'],
            'nonce_str'    => $this->getNonceStr(),
            'out_trade_no' => $out_trade_no
        );

        return $this->getHttpResponsePOST(self::URL_CLOSE_ORDER, $para);
    }

    /**
     * refund 申请退款
     * @access public
     * @param array $para 参数数组
     * @param bool $mode
     * @return array
     */
    public function refund($para = array(), $mode = false){
        $para['appid'] = $this->_config['APPID'];
        $para['mch_id'] = $this->_config['MCHID'];
        $para['nonce_str'] = $this->getNonceStr();
        if ($mode) {    // 使用微信订单号退款
            if ( ! isset($para['transaction_id'])) return false;
        } else {    // 使用商户订单号退款
            if ( ! isset($para['out_trade_no'])) return false;
        }
        if ( ! isset($para['out_refund_no'])) return false;
        if ( ! isset($para['total_fee'])) return false;
        if ( ! isset($para['refund_fee'])) return false;
        if ( ! isset($para['op_user_id'])) $para['op_user_id'] = $this->_config['MCHID'];

        return $this->getHttpResponsePOST(self::URL_REFUND, $para, true);
    }

    /**
     * downloadBill 下载对账单
     * @access public
     * @param string $bill_date 下载对账单的日期,格式:20140603
     * @param string $bill_type 类型
     * @return array
     */
    public function downloadBill($bill_date, $bill_type = 'ALL'){
        $para = array();
        $para['appid'] = $this->_config['APPID'];
        $para['mch_id'] = $this->_config['MCHID'];
        $para['nonce_str'] = $this->getNonceStr();
        $para['bill_date'] = $bill_date;
        $para['bill_type'] = $bill_type;

        return $this->getHttpResponsePOST(self::URL_DOWNLOAD_BILL, $para);
    }

    /**
     * 获取js支付使用的第二个参数
     */
    public function get_package($prepay_id) {
        $data = array();
        $data['appId'] = $this->_config['APPID'];
        $data['nonceStr']  = $this->getNonceStr();
        $data['timeStamp'] = time();
        $data['package']   = 'prepay_id='.$prepay_id;
        $data['signType']  = 'MD5';
        $data['paySign']   = $this->sign($data);
        
        return $data;
    }


    /**
     * getWxpayBackData 获取微信返回数据
     * @access public
     * @param void
     * @return array | bool
     */
    public function getWxpayBackData() {
        $xml = file_get_contents('php://input');
        $data = $this->xml2array($xml);

        return $this->checkSign($data) ? $data : NULL;
    }

    /**
     * replyNotify 回复通知
     * @access public
     * @param string $return_code 返回状态码
     * @param string $return_msg 返回信息
     * @return void
     */
    final public function replyNotify($return_code = 'SUCCESS', $return_msg = ''){
        $data = array();
        $data['return_code'] = $return_code;
        if ($return_msg) $data['return_msg'] = $return_msg;

        echo $this->array2xml($data);
    }

    /**
     * sign 生成签名
     * @access private
     * @param array $data
     * @return string
     */
    private function sign($data = array()){
        // 签名步骤一 按字典序排序参数
        ksort($data);

        $buff = '';

        // 去除空值
        // 把数组所有元素 按照“参数=参数值”的模式用“&”字符拼接成字符串
        // 签名步骤二 格式化参数格式化成url参数
        foreach ($data as $k => $v) {
            if($k != 'sign' && $v != '' && ! is_array($v)){
                $buff .= $k . '=' . $v . '&';
            }
        }
        $buff = trim($buff, '&');

        // 签名步骤三 在buff后加入KEY
        $string_sign_temp = $buff . '&key=' . $this->_config['KEY'];

        return strtoupper(md5($string_sign_temp));
    }

    /**
     * checkSign 验证数据签名
     * @access public
     * @param array $data 数据数组
     * @return bool 数据校验结果
     */
    public function checkSign($data) {
        if ( ! isset($data['sign']))  return false;
        $sign = $data['sign'];
        unset($data["sign"]);

        return $this->sign($data) == $sign;
    }

    /**
     * array2xml 将数组转换为XML
     * @access private
     * @param array $arr
     * @return xml
     */
    private function array2xml($arr = array()){
        $xml = '<xml>' . PHP_EOL;
        foreach ($arr as $k => $v) {
            if ($v > 0 && trim($v) != '' && ! is_array($v)) {
                if (is_numeric($v)) {
                    $xml.='<'.$k.'>'.$v.'</'.$k.'>' . PHP_EOL;
                }else{
                    $xml.='<'.$k.'><![CDATA['.$v.']]></'.$k.'>' . PHP_EOL;
                }
            }
        }
        $xml .= '</xml>';

        return $xml;
    }

    /**
     * xml2array 将XML转换为数组
     * @access private
     * @param string $xml
     * @return array $arr
     */
    private function xml2array($xml) {
        try {
            return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        } catch(Exception $e) {

        }
    }

    /**
     * getNonceStr 产生随机字符串,不长于32位
     * @access public
     * @param void
     * @return 产生的随机字符串
     */
    public function getNonceStr($len = 32){

        return substr(str_shuffle("abcdefghijklmnopqrstuvwxyz0123456789"), 0, $len);

    }


    /**
     * getHttpResponsePOST 远程获取数据 POST模式
     * @access private
     * @param string $url
     * @param array $data
     * @param bool $is_need_cert
     * @return array
     */
    private function getHttpResponsePOST($url, $data = array(), $is_need_cert = false){
        $data['sign'] = $this->sign($data);
        $xml = $this->array2xml($data);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_URL, $url);
        if ($is_need_cert == true) {
            //使用证书 cert 与 key 分别属于两个.pem文件
            curl_setopt($ch,CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch,CURLOPT_SSLCERT, $this->_config['SSLCERT_PATH']);
            curl_setopt($ch,CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch,CURLOPT_SSLKEY, $this->_config['SSLKEY_PATH']);
        }
        $content = curl_exec($ch);
        $array = $this->xml2array($content);
        return $array;
    }

    /**
     * logDebug
     * 写日志,方便测试(看网站需求,也可以改成把记录存入数据库)
     * 注意:服务器需要开通fopen配置
     * @access public
     * @param string $content 要写入日志里的文本内容 默认值:空值
     * @return void
     */
    public function logDebug($content = '') {
        $fp = fopen($this->_config['LOG_PATH'], 'a+');
        flock($fp, LOCK_EX);    // 文件锁定 避免其他人同时操作
        fwrite($fp, '# Wxpay Debug : ' . date('Y-m-d H:i:s') . ' # '. $content . "\n\n");
        flock($fp, LOCK_UN);
        fclose($fp);
    }


}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,333评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,994评论 4 61
  • 从今天开始我要记录我每天的收获,养成写日记的习惯,
    巧蕾陪你一起去看海阅读 190评论 0 1
  • 也许平时里表现最乐观的人,受到的伤害也是最深的。 我自己是一个很乐观的人,甚至在许多同学和朋友的心里我可能...
    WIFIgawaine阅读 159评论 0 0
  • 我都不知道该说什么了,不是我不努力去改善,而是实在是……或许我们都太自我为中心了,我得不到尊重,我选择躲避,可以躲...
    正一爸阅读 250评论 0 0