Model

<?php

/**
 * User: Eden
 * Date: 2019/3/21
 * 共有内容
 */

class WxPayModel extends Model {
    protected static $SSL_CERT_PATH = './apiclient_cert.pem';//证书路径
    protected static $SSL_KEY_PATH =  './apiclient_key.pem';//证书路径

    public static function unifiedOrder($openid,$order_num,$total_fee,$products_name,$notify_url = ''){
        $trade_no = $order_num;
        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        $param = [
            'appid'             => C('APPID'),
            'mch_id'            => C('MCHID'),
            'nonce_str'         => self::createNonceStr(),
            'sign_type'         => 'MD5',
            'body'              => $products_name,  //商品名称组合
            'attach'            => C('APP_NAME').'-附加信息',
            'out_trade_no'      => $trade_no,       //订单号
            'fee_type'          => 'CNY',
            'total_fee'         => $total_fee,
            'spbill_create_ip'  => $_SERVER['REMOTE_ADDR'],
            'goods_tag'         => C('APP_NAME').'-商品标记',
            'notify_url'        => $notify_url ?:C('NOTIFY_URL'),
            'trade_type'        => 'JSAPI',
            'openid'            => $openid
        ];

        $sign = self::MakeSign($param);
        $param['sign'] = $sign;
        $xml = self::ToXml($param);
        $result = self::FromXml(Http::postXmlCurl($url,$xml));
        setlog($param,$result,__METHOD__);

        // 加工数据
        $data = [
            'appId' => $result['appid'] ?: C('APPID'),
            'timeStamp' => time(),
            'nonceStr' => self::createNonceStr(),
            'package' => 'prepay_id=' . $result['prepay_id'],
            'signType' => 'MD5'
        ];
        $sign = self::MakeSign($data);
        $data['sign'] = $sign;
        return $data;
    }

    /**
     * 处理退款
     * @param $out_trade_no
     * @param $total_fee
     * @param $refund_fee
     * @return array
     * @throws Exception
     */
    public static function refundOrder($out_trade_no,$total_fee,$refund_fee) {
        $refund_no = $out_trade_no.rand('1111,9999');
        $param = array(
            'appid'         => C('APPID'),
            'mch_id'        => C('MCHID'),
            'nonce_str'     => self::createNonceStr(),
            'out_refund_no' => $refund_no, //由后端生成的退款单号,需要保证唯一,因为多个同样的退款单号只会退款一次。
            'out_trade_no'  => $out_trade_no,                   //退款订单在支付时生成的订单号
            'total_fee'     => $total_fee,
            'refund_fee'    => $refund_fee,
            'op_user_id'    => C('MCHID'),          //操作员 op_user_id .与商户号相同即可
        );

        $param['sign'] = self::MakeSign($param);
        $xml_data = self::ToXml($param);
        $xml_result = self::postXmlSSLCurl($xml_data,'https://api.mch.weixin.qq.com/secapi/pay/refund');
        $result = self::FromXml($xml_result);
        setlog($param,$result,__METHOD__);

        if (!$result){
            $result_arr = [
                'num'     =>      '0',
                'desc'   =>      '接口错误',
            ];
            return $result_arr;
        }

        if ($result['result_code'] != 'SUCCESS'){
            $result_arr = [
                'num'     =>      '-1',
                'desc'    =>      $result['err_code_des']
            ];
        } else {
            $result_arr = [
                'num'     =>      '1',
                'desc'    =>      '退款成功',
                'refund_id'    =>      $result['refund_id'],
                'refund_no'    =>      $refund_no,
            ];
        }

        return $result_arr;
    }

    public static function FromXml($xml)
    {
        if(!$xml){
            throw new Exception("xml数据异常!");
        }
        //将XML转为array
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $values;
    }

    public static function ToXml($array){
        if(!is_array($array)|| count($array) <= 0){
            return ;
        }
        $xml = '<xml version="1.0">';
        foreach ($array as $key=>$val){
            if (is_numeric($val)){
                $xml.="<".$key.">".$val."</".$key.">";
            }else{
                $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml.="</xml>";
        return $xml;
    }

    public static function createNonceStr($length = 16) {
        $chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
        $str = '';
        for ( $i = 0; $i < $length; $i++ )  {
            $str .= substr($chars, mt_rand(0, strlen($chars)-1), 1);
        }
        return $str;
    }

    public static function MakeSign($data)
    {
        //签名步骤一:按字典序排序参数
        ksort($data);
        $string = self::ToUrlParams($data);
        //签名步骤二:在string后加入KEY
        $string = $string . "&key=".C('WEIXIN_PAY_KEY');
        //签名步骤三:MD5加密
        $string = md5($string);
        //签名步骤四:所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    public static function ToUrlParams($array)
    {
        $buff = "";
        foreach ($array as $k => $v)
        {
            if($k != "sign" && $v != "" && !is_array($v)){
                $buff .= $k . "=" . $v . "&";
            }
        }
        $buff = trim($buff, "&");
        return $buff;
    }


    /**
     * 需要使用证书的请求
     */
    public static function postXmlSSLCurl($xml,$url,$second=30)
    {
        $ch = curl_init();
        //超时时间
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        //这里设置代理,如果有的话
        //curl_setopt($ch,CURLOPT_PROXY, '8.8.8.8');
        //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);
        //设置header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        //设置证书
        //使用证书:cert 与 key 分别属于两个.pem文件
        //默认格式为PEM,可以注释
        curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLCERT, self::$SSL_CERT_PATH);
        //默认格式为PEM,可以注释
        curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLKEY, self::$SSL_KEY_PATH);
        //post提交方式
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        $data = curl_exec($ch);
        //返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            echo "curl出错,错误码:$error" . "<br>";
            curl_close($ch);
            return false;
        }
    }
}

Vendor类

<?php

/**
 * User: Eden
 * Date: 2019/3/21
 * 共有内容
 */

class WxPay {
    protected static $SSL_CERT_PATH = './apiclient_cert.pem';//证书路径
    protected static $SSL_KEY_PATH =  './apiclient_key.pem';//证书路径

    public static function unifiedOrder($openid,$order_num,$total_fee,$products_name,$notify_url = ''){
        $trade_no = $order_num;
        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        $param = [
            'appid'             => C('APPID'),
            'mch_id'            => C('MCHID'),
            'nonce_str'         => self::createNonceStr(),
            'sign_type'         => 'MD5',
            'body'              => $products_name,  //商品名称组合
            'attach'            => C('APP_NAME').'-附加信息',
            'out_trade_no'      => $trade_no,       //订单号
            'fee_type'          => 'CNY',
            'total_fee'         => $total_fee,
            'spbill_create_ip'  => $_SERVER['REMOTE_ADDR'],
            'goods_tag'         => C('APP_NAME').'-商品标记',
            'notify_url'        => $notify_url ?:C('NOTIFY_URL'),
            'trade_type'        => 'JSAPI',
            'openid'            => $openid
        ];

        $sign = self::MakeSign($param);
        $param['sign'] = $sign;
        $xml = self::ToXml($param);
        $result = self::FromXml(Http::postXmlCurl($url,$xml));
        setlog($param,$result,__METHOD__);

        // 加工数据
        $data = [
            'appId' => $result['appid'] ?: C('APPID'),
            'timeStamp' => time(),
            'nonceStr' => self::createNonceStr(),
            'package' => 'prepay_id=' . $result['prepay_id'],
            'signType' => 'MD5'
        ];
        $sign = self::MakeSign($data);
        $data['sign'] = $sign;
        return $data;
    }

    /**
     * 处理退款
     * @param $out_trade_no
     * @param $total_fee
     * @param $refund_fee
     * @return array
     * @throws Exception
     */
    public static function refundOrder($out_trade_no,$total_fee,$refund_fee) {
        $refund_no = $out_trade_no.rand('1111,9999');
        $param = array(
            'appid'         => C('APPID'),
            'mch_id'        => C('MCHID'),
            'nonce_str'     => self::createNonceStr(),
            'out_refund_no' => $refund_no, //由后端生成的退款单号,需要保证唯一,因为多个同样的退款单号只会退款一次。
            'out_trade_no'  => $out_trade_no,                   //退款订单在支付时生成的订单号
            'total_fee'     => $total_fee,
            'refund_fee'    => $refund_fee,
            'op_user_id'    => C('MCHID'),          //操作员 op_user_id .与商户号相同即可
        );

        $param['sign'] = self::MakeSign($param);
        $xml_data = self::ToXml($param);
        $xml_result = self::postXmlSSLCurl($xml_data,'https://api.mch.weixin.qq.com/secapi/pay/refund');
        $result = self::FromXml($xml_result);
        setlog($param,$result,__METHOD__);

        if (!$result){
            $result_arr = [
                'num'     =>      '0',
                'desc'   =>      '接口错误',
            ];
            return $result_arr;
        }

        if ($result['result_code'] != 'SUCCESS'){
            $result_arr = [
                'num'     =>      '-1',
                'desc'    =>      $result['err_code_des']
            ];
        } else {
            $result_arr = [
                'num'     =>      '1',
                'desc'    =>      '退款成功',
                'refund_id'    =>      $result['refund_id'],
                'refund_no'    =>      $refund_no,
            ];
        }

        return $result_arr;
    }

    public static function FromXml($xml)
    {
        if(!$xml){
            throw new Exception("xml数据异常!");
        }
        //将XML转为array
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $values;
    }

    public static function ToXml($array){
        if(!is_array($array)|| count($array) <= 0){
            return ;
        }
        $xml = '<xml version="1.0">';
        foreach ($array as $key=>$val){
            if (is_numeric($val)){
                $xml.="<".$key.">".$val."</".$key.">";
            }else{
                $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml.="</xml>";
        return $xml;
    }

    public static function createNonceStr($length = 16) {
        $chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
        $str = '';
        for ( $i = 0; $i < $length; $i++ )  {
            $str .= substr($chars, mt_rand(0, strlen($chars)-1), 1);
        }
        return $str;
    }

    public static function MakeSign($data)
    {
        //签名步骤一:按字典序排序参数
        ksort($data);
        $string = self::ToUrlParams($data);
        //签名步骤二:在string后加入KEY
        $string = $string . "&key=".C('WEIXIN_PAY_KEY');
        //签名步骤三:MD5加密
        $string = md5($string);
        //签名步骤四:所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    public static function ToUrlParams($array)
    {
        $buff = "";
        foreach ($array as $k => $v)
        {
            if($k != "sign" && $v != "" && !is_array($v)){
                $buff .= $k . "=" . $v . "&";
            }
        }
        $buff = trim($buff, "&");
        return $buff;
    }


    /**
     * 需要使用证书的请求
     */
    public static function postXmlSSLCurl($xml,$url,$second=30)
    {
        $ch = curl_init();
        //超时时间
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        //这里设置代理,如果有的话
        //curl_setopt($ch,CURLOPT_PROXY, '8.8.8.8');
        //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);
        //设置header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        //设置证书
        //使用证书:cert 与 key 分别属于两个.pem文件
        //默认格式为PEM,可以注释
        curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLCERT, self::$SSL_CERT_PATH);
        //默认格式为PEM,可以注释
        curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLKEY, self::$SSL_KEY_PATH);
        //post提交方式
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        $data = curl_exec($ch);
        //返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            echo "curl出错,错误码:$error" . "<br>";
            curl_close($ch);
            return false;
        }
    }
}

使用效果差不多。