PHP对接微信支付

此文章发布于26个月前,部分信息可能已经过时,请自行斟酌确认

近期正好对接微信支付功能,写了一个微信支付类,方便以后使用,这里分享给大家,配置一下直接调用就能用!

食用说明:

  • 设置为自己的$appid,$mchid,$mch_key。详情看代码。
  • 根据自己情况设置文件的命名空间,也就是namespace
  • 整个类为整体,不要只复制其中一部分。
  • 调用里面支付类方法时需要设置回调地址,根据自己实际情况修改。



<?php
/**
 * Created by PhpStorm.
 * User: Qingzhi
 * Blog: Https://www.xinyouqu.com
 * Date: 2020/12/22
 * Time: 15:06
 */

namespace app\paycurrency\controller;



class WechatPayCurrency
{

    protected static $appid = '';//直连商户申请的公众号或移动应用appid。
    protected static $mchid = '';//直连商户的商户号,由微信支付生成并下发。
    protected static $mch_key = '';//支付密钥


    protected static $RefundUrl = '';//退款回调地址
    protected static $NotifyUrl = '';//支付回调地址

    protected static $sslCertPath = '';//微信退款时才需要的_cert.pem证书文件路径
    protected static $sslKeyPath = '';//微信退款时才需要的_key.pem证书文件路径

    /**
     * 微信app支付接口
     * @param $order 商户订单编号
     * @param $price 金额(分)
     * @param string $body 商品描述
     * @return array
     */
    public static function wechatPaymentApp($order, $price, $body = '')
    {
        $gatewayUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        /** 商品描述 $body */
        $body = $body ?: '微信支付费用';
        /** 回调路径 $NotifyUrl */
//        $NotifyUrl = request()->domain() . self::$NotifyUrl;
        $NotifyUrl = self::$NotifyUrl;
        // 获取统一下单参数 xml
        $post = [
            'appid'            => self::$appid, // 必填--服务商商户的APPID
            'body'             => $body, // 必填--商品描述
            'mch_id'           => self::$mchid, // 必填--微信支付分配的商户号
            'nonce_str'        => self::randNbumer(32, 3), // 必填--随机字符串,不长于32位。推荐随机数生成算法
            'notify_url'       => $NotifyUrl, //支付完成回调地址url,不能带参数
            'out_trade_no'     => $order, // 商户订单号
            'spbill_create_ip' => self::contrast_ip(), //必填--终端IP。调用微信支付API的机器IP  contrast_ip   $_SERVER['SERVER_ADDR']
            'total_fee'        => (int)$price, // 订单价格
            'trade_type'       => 'APP'//交易类型 默认JSAPI   APP
        ];
        // 生成签名
        $KEY          = self::$mch_key;
        $sign         = self::MakeSign($post, $KEY);
        $post['sign'] = $sign;
        // 数组转xml
        $post_xml = self::arrayToXml($post);
        //POST方式请求http
        $xdXml = self::http_request($gatewayUrl, $post_xml);
        //微信返回 xml  转为数组
        $xdArray = self::xmlToArray($xdXml);
        if ($xdArray['return_code'] == 'SUCCESS' && $xdArray['result_code'] == 'SUCCESS') {
            //二次验签
            $data = self::getSignApp($xdArray);
            return self::return_result($data, 20000, '订单创建成功');
        } else {
            $msg = $xdArray['err_code_des'] ?? ($xdArray['return_msg'] ?? '订单生成失败,请重试');
            return self::return_result([], 50000, $msg);
        }
    }

    /**
     * 随机生成字符串
     * @param int $len 字符串长度
     * @param int $type 字符串类型
     * @return bool|string
     */
    private static function randNbumer($len = 32, $type = 3)
    {
        switch ($type) {
            case 1:
                $codeSet = '123456789';
                break;
            case 2:
                $codeSet = 'abcdefhijkmnpqrstuvwxyz';
                break;
            case 3:
                $codeSet = '2345678abcdefhijkmnpqrstuvwxyz';
                break;
            case 4:
                $codeSet = 'abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY';
                break;
            default:
                $codeSet = '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY';
                break;
        }
        return substr(str_shuffle(str_repeat($codeSet, $len)), 0, $len);
    }

    /**
     * 传入ip则验证是否一致,不传入则返回当前ip
     * @param null $contrast 想要对比的IP地址
     * @return array|bool|false|string
     */
    private static function contrast_ip($contrast = null)
    {
        if (isset($_SERVER)) {
            if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
                $realip = $_SERVER['HTTP_X_FORWARDED_FOR'];
            } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
                $realip = $_SERVER['HTTP_CLIENT_IP'];
            } else {
                $realip = $_SERVER['REMOTE_ADDR'];
            }
        } else {
            //不允许就使用getenv获取
            if (getenv("HTTP_X_FORWARDED_FOR")) {
                $realip = getenv("HTTP_X_FORWARDED_FOR");
            } elseif (getenv("HTTP_CLIENT_IP")) {
                $realip = getenv("HTTP_CLIENT_IP");
            } else {
                $realip = getenv("REMOTE_ADDR");
            }
        }
        if (!$contrast) return $realip;
        if ($contrast == $realip) return true;
        return false;
    }

    /**
     * @param $params
     * @param $KEY 支付key
     * @param string $type
     * @return string 签名
     */
    private static function MakeSign($params, $KEY, $type = "MD5")
    {
        //签名步骤一:按字典序排序数组参数
        ksort($params);
        $string = self::ToUrlParams($params);  //参数进行拼接 1
        $string = $string . "&key=" . $KEY;
        //签名步骤二:加密
        if ($type == 'MD5') {
            $string = md5($string);
        } else {
            $string = hash_hmac('sha256', $string, $KEY);
        }
        //签名步骤三:所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    /**
     * 将参数拼接为url(key=value&key=value)
     * @param $params
     * @return string
     */
    private static function ToUrlParams($params)
    {
        $string = '';
        if (!empty($params)) {
            $array = array();
            foreach ($params as $key => $value) {
                $array[] = $key . '=' . $value;
            }
            $string = implode("&", $array);
        }
        return $string;
    }

    /**
     * array转xml
     * @param $arr
     * @return string
     */
    private static function arrayToXml($arr)
    {
        $xml = "<xml>";
        foreach ($arr as $key => $val) {
            // 将字符转为大写
            //$key = strtoupper($key);
            // 进遍历
            if (is_array($val)) {
                $xml .= "<" . $key . ">" . self::arrayToXml($val) . "</" . $key . ">";
            } else {
                $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
            }
        }
        $xml .= "</xml>";
        return $xml;
    }

    /**
     * 微信传输调用接口
     * @param $url 接口链接
     * @param null $data 数组
     * @param array $cert 证书路径信息
     * @param array $headers 头信息
     * @return bool|string
     */
    private static function http_request($url, $data = null, $cert = array(), $headers = array())
    {
        $curl = curl_init();
        if (count($headers) >= 1) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        }
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        // 判断是使用证书
        if ($cert) {
            curl_setopt($curl, CURLOPT_SSLCERTTYPE, 'PEM'); // 证书的类型。支持的格式有"PEM" (默认值), "DER"和"ENG"。
            curl_setopt($curl, CURLOPT_SSLCERT, $cert['cert_url']); // 证书,用于双向认证
            curl_setopt($curl, CURLOPT_SSLKEYTYPE, 'PEM'); // 客户端私钥类型,支持的私钥类型为"PEM"(默认值)、"DER"和"ENG"
            curl_setopt($curl, CURLOPT_SSLKEY, $cert['cert_key_url']); // 私钥的文件路径  getcwd()
            //curl_setopt($curl,CURLOPT_CAINFO,getcwd()."/customerkey/rootca.pem");// 公共pem
            //curl_setopt($curl, CURLOPT_KEYPASSWD,'');// 私钥密码,私钥在创建时可以选择加密。
        }
        curl_setopt($curl, CURLOPT_URL, $url);
        if (!empty($data)) {
            curl_setopt($curl, CURLOPT_POST, 1);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($curl);
        //$meta = curl_getinfo($curl);// 发送信息
        //dump($meta);
        curl_close($curl);
        return $output;
    }

    /**
     * xml转array
     * @param unknown $xml
     * @return Ambigous <string, unknown>
     */
    private static function xmlToArray($xml)
    {
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $xmlstring = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
        $val       = json_decode(json_encode($xmlstring), true);
        return $val;
        //         方法一
        //        $result = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        //        return $result;

        //         方法二 建名全大写不适应
        //        $p = xml_parser_create();
        //        xml_parse_into_struct($p, $xml, $vals, $index);
        //        xml_parser_free($p);
        //        $data = "";
        //        foreach ($index as $key => $value) {
        //            if ($key == 'xml' || $key == 'XML') continue;
        //            $tag        = $vals[$value[0]]['tag'];
        //            $value      = $vals[$value[0]]['value'];
        //            $data[$tag] = $value;
        //        }
        //        return $data;

        //         方法三  键值对不对
        //        $data = simplexml_load_string($xml); //xml转object
        //        $data = json_encode($data);  //objecct转json
        //        $data = json_decode($data, true); //json转array
        //        return $data;
    }

    /**
     * app二次验签
     * @param $xdArray
     * @return mixed
     */
    private static function getSignApp($xdArray)
    {
        $getPost['appid']     = $xdArray['appid'];
        $getPost['partnerid'] = $xdArray['mch_id'];
        $getPost['prepayid']  = $xdArray['prepay_id'];
        $getPost['package']   = 'Sign=WXPay';
        $getPost['noncestr']  = self::randNbumer(32, 3);
        $getPost['timestamp'] = (string)time();
        $KEY                  = self::$mch_key;
        $sign                 = self::MakeSign($getPost, $KEY);
        $getPost['sign']      = $sign;
        return $getPost;
    }

    /**
     * 自己封装的返回方法
     * @param array $data
     * @param int $code
     * @param string $msg
     * @return array
     */
    private static function return_result($data = [], $code = 20000, $msg = '')
    {
        if (is_string($data)) {
            $msg  = $data;
            $data = [];
        }
        return ['data' => $data, 'code' => $code, 'msg' => $msg, 'time' => time()];
    }

    /**
     * 小程序支付接口
     * @param $open_id 用户的openid
     * @param $order 订单编号
     * @param $price 金额(分)
     * @param string $body 商品描述
     * @return array
     */
    public static function wechatPaymentApple($open_id, $order, $price, $body = '')
    {
        $gatewayUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';//微信统一下单接口
        //        $notify_url = request()->domain() . self::$NotifyUrl;//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
        $notify_url = self::$NotifyUrl;//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
        // 获取统一下单参数 xml
        $post = [
            'appid'            => self::$appid, // 必填--服务商商户的APPID
            'body'             => $body ?? '商品名称', // 必填--商品描述
            'mch_id'           => self::$mchid, // 必填--微信支付分配的商户号
            'nonce_str'        => self::randNbumer(32, 3), // 必填--随机字符串,不长于32位。推荐随机数生成算法
            'notify_url'       => $notify_url, //支付完成回调地址url,不能带参数
            'openid'           => $open_id,
            'out_trade_no'     => $order, // 商户订单号
            'spbill_create_ip' => self::contrast_ip(), //必填--终端IP。调用微信支付API的机器IP
            'total_fee'        => (int)$price, // 订单价格
            'trade_type'       => 'JSAPI'//交易类型 默认JSAPI
        ];
        // 生成签名
        $KEY          = self::$mch_key;
        $sign         = self::MakeSign($post, $KEY);
        $post['sign'] = $sign;

        // 数组转xml
        $post_xml = self::arrayToXml($post);
        //        dump($post_xml);exit;
        //POST方式请求http
        $xdXml = self::http_request($gatewayUrl, $post_xml);
        //微信返回 xml  转为数组
        $xdArray = self::xmlToArray($xdXml);
        // 当生成订单成功后进行二次签名
        if ($xdArray['return_code'] == 'SUCCESS' && $xdArray['result_code'] == 'SUCCESS') {
            //二次签名  小程序调起支付数据签名字段列表:
            $res  = self::getSign($xdArray);
            $data = [
                'res'    => $xdArray,
                'result' => $res,
            ];
            return self::return_result($data, 20000, $xdArray['return_msg'] ?? '');
        } else {
            $data = [
                'res'    => $xdArray,
                'result' => $xdArray,
            ];
            return self::return_result($data, 50000, $xdArray['return_msg'] ?? '');
        }
    }

    /**
     * 小程序二次验签
     * @param $xdArray
     * @return mixed
     */
    private static function getSign($xdArray)
    {
        $getPost['appId']     = self::$appid;
        $getPost['timeStamp'] = (string)time();
        $getPost['nonceStr']  = self::randNbumer(32, 3);
        $getPost['package']   = 'prepay_id=' . $xdArray['prepay_id'];
        $getPost['signType']  = "MD5";
        $KEY                  = self::$mch_key;
        $sign                 = self::MakeSign($getPost, $KEY);
        $getPost['paySign']   = $sign;
        return $getPost;
    }

    /**
     * 微信web支付
     * @param $order 订单编号
     * @param $price 订单金额
     * @param string $body 商品描述
     * @return array
     */
    public static function wechatPaymentWeb($order, $price, $body = '')
    {
        $gatewayUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';//微信统一下单接口
        //        $notify_url = request()->domain() . self::$NotifyUrl;//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
        $notify_url = self::$NotifyUrl;//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
        //统一下单参数 xml
        $post = [
            'appid'            => self::$appid, // 必填--服务商商户的APPID
            'body'             => $body ?? '商品名称', // 必填--商品描述
            'mch_id'           => self::$mchid, // 必填--微信支付分配的商户号
            'nonce_str'        => self::randNbumer(32, 3), // 必填--随机字符串,不长于32位。推荐随机数生成算法
            'notify_url'       => $notify_url, //支付完成回调地址url,不能带参数
            'out_trade_no'     => $order, // 商户订单号
            'spbill_create_ip' => self::contrast_ip(), //必填--终端IP。调用微信支付API的机器IP  contrast_ip   $_SERVER['SERVER_ADDR']
            'total_fee'        => (int)$price, // 订单价格
            'trade_type'       => 'NATIVE'//交易类型 默认JSAPI
        ];
        // 生成签名
        $KEY          = self::$mch_key;
        $sign         = self::MakeSign($post, $KEY);
        $post['sign'] = $sign;
        // 数组转xml
        $post_xml = self::arrayToXml($post);
        //POST方式请求http
        $xdXml = self::http_request($gatewayUrl, $post_xml);
        //微信返回 xml  转为数组
        $xdArray = self::xmlToArray($xdXml);
        if ($xdArray['return_code'] == 'SUCCESS' && $xdArray['result_code'] == 'SUCCESS') return self::return_result($xdArray, 20000, '订单创建成功');
        $msg = $xdArray['err_code_des'] ?? ($xdArray['return_msg'] ?? '订单生成失败,请重试');
        return self::return_result([], 50000, $msg);
    }

    /**
     * 微信退款查询
     * @param $out_trade_no
     * @param string $type //微信订单号:transaction_id   商户订单号:out_trade_no    商户退款单号:out_refund_no     微信退款单号:refund_id
     * @return array
     */
    public static function wechatRefundInquiry($out_trade_no, $type = 'out_trade_no')
    {
        $queryRefundUrl = "https://api.mch.weixin.qq.com/pay/refundquery";//微信退款查询接口
        if (empty($out_trade_no)) return self::return_result([], 50000, '订单号不能为空');
        $query = self::wechatpayQuery($out_trade_no, $type);
        //POST方式请求http
        $xdXml = self::http_request($queryRefundUrl, $query);
        //微信返回 xml  转为数组
        $xdArray = self::xmlToArray($xdXml);
        if (!empty($xdArray['return_code']) && $xdArray['return_code'] == 'SUCCESS' && isset($xdArray['result_code']) && $xdArray['result_code'] == 'SUCCESS') {
            return self::return_result($xdArray, 20000, '订单查询成功');
        }
        $msg = $xdArray['err_code_des'] ?? ($xdArray['return_msg'] ?? '订单查询失败,请重试');
        return self::return_result([], 50000, $msg);
    }

    /**
     * 获取商户平台配置
     * @param $out_trade_no 订单号
     * @return string
     */
    private static function wechatPayQuery($out_trade_no, $type = 'out_trade_no')
    {
        $post        = [
            'appid'     => self::$appid, // 必填--服务商商户的APPID
            'mch_id'    => self::$mchid, // 必填--微信支付分配的商户号
            'nonce_str' => self::randNbumer(32, 3), // 必填--随机字符串,不长于32位。推荐随机数生成算法
        ];
        $post[$type] = $out_trade_no;
        // 生成签名
        $KEY          = self::$mch_key;
        $sign         = self::MakeSign($post, $KEY);
        $post['sign'] = $sign;

        // 数组转xml
        $post_xml = self::arrayToXml($post);
        return $post_xml;
    }

    /**
     * 微信退款申请
     * @param $order 原订单号
     * @param int $price 金额 分
     * @param null $out_request_no 退款单号
     * @param string $refund_reason 退款信息
     * @return array
     */
    public static function wechatPaymentRefund($order, int $price, $out_request_no = null, $refund_reason = '退款')
    {
        $refundUrl = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
        //        $refund_url     = request()->domain() . self::$RefundUrl;//退款回调地址
        $refund_url     = self::$RefundUrl;//退款回调地址
        $out_request_no = $out_request_no ?? date('YmdHis') . self::randNbumer(6, 1);//生成退款订单号
        $post_arr       = [
            'appid'         => self::$appid,
            'mch_id'        => self::$mchid,
            'nonce_str'     => self::randNbumer(32, 3),//随机字符串
            'sign_type'     => 'HMAC-SHA256',
            //                'transaction_id' => '',
            'out_trade_no'  => $order,
            'out_refund_no' => $out_request_no,
            'total_fee'     => $price,
            'refund_fee'    => $price,
            'refund_desc'   => $refund_reason,//退款原因
            'notify_url'    => $refund_url
        ];
        // 2、制作签名
        $KEY              = self::$mch_key;
        $sign             = self::MakeSign($post_arr, $KEY, $post_arr['sign_type']);
        $post_arr['sign'] = $sign;
        // 3、 转xml
        $post_xml = self::arrayToXml($post_arr);
        //4、发送请求
        //证书
        //        $sslCertPath = env('extend_path') . '/cert/apiclient_cert.pem';
        //        $sslKeyPath  = env('extend_path') . '/cert/apiclient_key.pem';
        $sslCertPath = self::$sslCertPath;
        $sslKeyPath  = self::$sslKeyPath;
        $cert        = [
            'cert_url'     => $sslCertPath,
            'cert_key_url' => $sslKeyPath,
        ];
        //POST方式请求http
        $xdXml = self::http_request($refundUrl, $post_xml, $cert);
        //微信返回 xml  转为数组
        $xdArray = self::xmlToArray($xdXml);
        if ($xdArray['return_code'] == 'SUCCESS' && $xdArray['result_code'] == 'SUCCESS') {
            return self::return_result($xdArray, 20000, '退款申请成功');
        } else {
            $msg = $xdArray['err_code_des'] ?? ($xdArray['return_msg'] ?? '退款申请失败,请重试');
            return self::return_result([], 50000, $msg);
        }
    }

    /**
     * 微信支付订单查询
     * @param bool $out_trade_no
     * @return Ambigous|bool
     */
    public static function wechatPaymentQuery($out_trade_no)
    {
        $querywayUrl = "https://api.mch.weixin.qq.com/pay/orderquery";//微信订单查询接口
        if (empty($out_trade_no)) return self::return_result([], 50000, '订单号不能为空');
        $query = self::wechatPayQuery($out_trade_no);
        //POST方式请求http
        $xdXml = self::http_request($querywayUrl, $query);
        //微信返回 xml  转为数组
        $xdArray = self::xmlToArray($xdXml);
        if (empty($xdArray['return_code'])) return self::return_result($xdArray, 50000, '查询失败');
        if ($xdArray['return_code'] == 'SUCCESS' && $xdArray['result_code'] == 'SUCCESS') {
            return self::return_result($xdArray, 20000, '查询成功');
        }
        $msg = $xdArray['err_code_des'] ?? ($xdArray['return_msg'] ?? '订单查询失败');
        return self::return_result($xdArray, 50000, $msg);
    }


}
最后修改:2021 年 03 月 25 日 12 : 07 AM
如果觉得我的文章对你有用,请随意赞赏

2 条评论

  1. 燕小六

    超赞!直接就可以用了!

    1. 晴栀
      @燕小六

      注意要修改好自己的支付回调地址哦

发表评论