微信H5支付

微信H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在商品展示页确认使用微信支付时,商户发起本服务唤起微信客户端进行支付。主要用于触屏版的手机浏览器请求微信支付的场景,可以方便的从外部浏览器唤醒微信支付。

体验页面:https://wxpay.wxutil.com/mch/pay/h5.v2.php

支付流程

  1. 用户在商品页完成下单并使用微信支付进行支付
  2. 由商户后台向微信支付发起下单请求即调用统一下单接口
  3. 统一下单接口返回支付相关参数给商户后台,商户通过专用链接调起微信支付中间页面。
  4. 在中间页进行H5权限校验以及安全性检测
  5. 若支付成功商户后台会收到微信端的异步通知
  6. 用户在微信支付收银平台完成支付或取消支付并返回商品页面
  7. 商品在展示页引导用户主动发起支付结果的查询
  8. 商户后台判断是否 接收到微信端的支付结果通知,若没有后台调用订单查询接口确定订单状态。
  9. 展示最终的订单支付结果给用户
支付流程

开发流程

  1. 用户下单时选择微信支付
  2. 商户进行业务逻辑处理并调用微信统一下单接口,微信H5交易类型为trade_type=MWEB
  3. 调用下单接口成功时,微信会返回包含支付跳转URL等相关参数,商户通过参数mweb_url调起支付中间页。
  4. 在中间页微信会进行H5权限的校验
  5. 支付成功,微信会向商户发送异步结果通知。

统一下单接口

<?php
class Wechat
{
    /**
      * 组建签名
      * @param array $params 请求参数
      * @param string $key 秘钥
      */
    public static function getSign($params, $key)
    {
        foreach ($params as $k=>$v) {
            if (!$v) {
                unset($params[$k]);
            }
        }
        ksort($params);
        $paramStr = '';
        foreach ($params as $k => $v) {
            $paramStr = $paramStr . $k . '=' . $v . '&';
        }
        $paramStr = $paramStr . 'key='.$key;
        $sign = strtoupper(md5($paramStr));
        return $sign;
    }

    /**
      * 将数组转为XML
      * @param array $params 支付请求参数
      */
    public static function getXml($params)
    {
        if(!is_array($params)|| count($params) <= 0) {
            return false;
        }
        $xml = "<xml>";
        foreach ($params as $key=>$val) {
            if (is_numeric($val)) {
                $xml.="<".$key.">".$val."</".$key.">";
            } else {
                $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml.="</xml>";
        return $xml;
    }
    /*官方统一下单*/
    public static function unifiedOrder($appid, $mch_id, $key, $total_fee, $out_trade_no, $body, $notify_url, $wap_url)
    {
        $spbill_create_ip = $_SERVER["REMOTE_ADDR"]; //获得用户设备IP
        $nonce_str=MD5($out_trade_no);//随机字符串
        $trade_type = 'MWEB';//交易类型 微信H5交易

        $scene_info ='{"h5_info":{"type":"Wap","wap_url":"'.$wap_url.'","wap_name":"支付"}}';//场景信息 必要参数

        $signA ="appid=$appid&body=$body&mch_id=$mch_id&nonce_str=$nonce_str&notify_url=$notify_url&out_trade_no=$out_trade_no&scene_info=$scene_info&spbill_create_ip=$spbill_create_ip&total_fee=$total_fee&trade_type=$trade_type";
        //拼接字符串 注意顺序微信有个测试网址 顺序按照他的来 直接点下面的校正测试 包括下面XML 是否正确
        $strSignTmp = $signA."&key=$key";
        // MD5 后转换成大写
        $sign = strtoupper(MD5($strSignTmp));
        //拼接成XML格式 *XML格式文件要求非常严谨不能有空格这点一定要注意
        $post_data="<xml><appid>$appid</appid><body>$body</body><mch_id>$mch_id</mch_id><nonce_str>$nonce_str</nonce_str><notify_url>$notify_url</notify_url><out_trade_no>$out_trade_no</out_trade_no><scene_info>$scene_info</scene_info><spbill_create_ip>$spbill_create_ip</spbill_create_ip><total_fee>$total_fee</total_fee><trade_type>$trade_type</trade_type><sign>$sign</sign>
</xml>";


        $url = "https://api.mch.weixin.qq.com/pay/unifiedorder";//微信下单接口连接不用更改

        $headers = array();
        $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
        $headers[] = 'Connection: Keep-Alive';
        $headers[] = 'Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3';
        $headers[] = 'Accept-Encoding: gzip, deflate';
        $headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20100101 Firefox/22.0';

        $dataxml = self::httpPost($url,$post_data,$headers);//传参调用curl请求
        $objectxml = (array)simplexml_load_string($dataxml,'SimpleXMLElement',LIBXML_NOCDATA); //将微信返回的XML 转换成数组
        //支付跳转URL
        $mweb_url = "";
        if($objectxml['return_code'] == 'SUCCESS'){
            $mweb_url= $objectxml['mweb_url'];
        }

        return $mweb_url;//跳转后台获取的支付连接
    }
    /**
     * 获取微信支付中间页deepLink参数
     * @param string $url 微信返回的mweb_url
     * @param string $ip 用户端IP
     */
    public static function getDeeplink($url, $ip)
    {
        $headers = array("X-FORWARDED-FOR:$ip", "CLIENT-IP:$ip");
        ob_start();
        $ch = curl_init();
        curl_setopt ($ch, CURLOPT_URL, $url);
        curl_setopt ($ch, CURLOPT_HTTPHEADER , $headers );
        curl_setopt ($ch, CURLOPT_REFERER, "pay.o9di.cn");
        curl_setopt( $ch, CURLOPT_HEADER, 1);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Linux; Android 6.0.1; OPPO R11s Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36');
        curl_exec($ch);
        curl_close ($ch);
        $out = ob_get_contents();
        ob_clean();
        $a = preg_match('/weixin:\/\/wap.*/',$out, $str);
        if ($a) {
            return substr($str[0], 0, strlen($str[0])-1);
        } else {
            return '';
        }
    }
    public static function httpPost($url='',$post_data=array(),$header=array(),$timeout=30) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

        $response = curl_exec($ch);

        curl_close($ch);

        return $response;
    }
    public static function getClientIp($type = 0) {
        $type       =  $type ? 1 : 0;
        $ip         =   'unknown';
        if ($ip !== 'unknown') return $ip[$type];
        if($_SERVER['HTTP_X_REAL_IP']){//nginx 代理模式下,获取客户端真实 IP
            $ip=$_SERVER['HTTP_X_REAL_IP'];
        }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {//客户端的 ip
            $ip     =   $_SERVER['HTTP_CLIENT_IP'];
        }elseif (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($arr[0]);
        }elseif (isset($_SERVER['REMOTE_ADDR'])) {
            $ip     =   $_SERVER['REMOTE_ADDR'];//浏览当前页面的用户计算机的 ip 地址
        }else{
            $ip=$_SERVER['REMOTE_ADDR'];
        }
        // IP 地址合法验证
        $long = sprintf("%u",ip2long($ip));
        $ip   = $long ? array($ip, $long) : array('0.0.0.0', 0);
        return $ip[$type];
    }
}

请求参数

请求微信统一下单接口

接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder

  • appid 微信公众号iD
  • mch_id账户号
  • nonce_str 随机字符串,不长于32位
  • sign 签名
  • body 商品描述
  • out_trade_no 商户订单号,不长于32位
  • total_fee 总金额,以分为单位
  • spbill_create_ip 用户端请求支付时的IP
  • notify_url 异步通知回调地址,必须是可直接访问地址,不能携带参数
  • trade_type 交易类型,如H5则是MWEB
<xml>
<appid>$appid</appid>
<body>$body</body>
<mch_id>$mch_id</mch_id>
<nonce_str>$nonce_str</nonce_str>
<notify_url>$notify_url</notify_url>
<out_trade_no>$out_trade_no</out_trade_no>
<scene_info>$scene_info</scene_info>
<spbill_create_ip>$spbill_create_ip</spbill_create_ip>
<total_fee>$total_fee</total_fee>
<trade_type>$trade_type</trade_type>
<sign>$sign</sign>
</xml>

签名生成

  1. 参与生成签名的参数必须非空
  2. 参数按照ASCII码由小到大排序,参数名区分大小写
  3. 按照上述规则,将参数拼接成如k1=v1&k2=v2....的字符串
  4. 将上一步得到的字符串拼接上key, 如k1=v1&k2=v2&key=192006250b4c09247ec02e
  5. 再将最后得到的字符串进行MD5加密,再转为大写,即为最终的sign值。
$signA ="appid=$appid&body=$body&mch_id=$mch_id&nonce_str=$nonce_str&notify_url=$notify_url&out_trade_no=$out_trade_no&scene_info=$scene_info&spbill_create_ip=$spbill_create_ip&total_fee=$total_fee&trade_type=$trade_type";

//拼接字符串 注意顺序微信有个测试网址 顺序按照他的来 直接点下面的校正测试 包括下面XML 是否正确
$strSignTmp = $signA."&key=$key";
       
// MD5 后转换成大写
$sign = strtoupper(MD5($strSignTmp));

封装函数

/**
 * 生成签名
 *  @return 签名
 */
function makeSign( $params ){
    //签名步骤一:按字典序排序数组参数
    ksort($params);
    $string = makeUrlParams($params);
    //签名步骤二:在string后加入KEY
    $string = $string . "&key=".$this->key;
    //签名步骤三:MD5加密
    $string = md5($string);
    //签名步骤四:所有字符转为大写
    $result = strtoupper($string);
    return $result;
}
/**
 * 将参数拼接为url: key=value&key=value
 * @param   $params
 * @return  string
 */
function makeUrlParams( $params ){
    $string = '';
    if( !empty($params) ){
        $array = array();
        foreach( $params as $key => $value ){
            $array[] = $key.'='.$value;
        }
        $string = implode("&",$array);
    }
    return $string;
}

用户IP

H5支付要求商户在统一下单接口中上传用户真实IP地址spbill_create_ip,为保证微信端获取的用户IP地址与商户端获取的一致,提供了以下获取用户IP的指引。

  • 没有代理的情况

在商户的前端接入层没有做代理的情况下获取IP的方式比较简单,直接获取REMOTE_ADDR即可。

  • 有代理的情况

在有代理的情况下,因为要代替客户端去访问服务器,所以,当请求包经过反向代理后,在代理服务器这里这个IP数据包的IP包头做了修改,最终后端WEB服务器得到的数据包的头部源IP地址是代理服务器的IP地址。这样一来,后端服务器的程序就无法获取用户的真实IP。

在Nginx中配置中加入

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Real-Port $remote_port;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

函数封装

function getClientIp($type = 0) {
    $type       =  $type ? 1 : 0;
    $ip         =   'unknown';
    if ($ip !== 'unknown') return $ip[$type];
    if(isset($_SERVER["HTTP_X_REAL_IP"]) && !empty($_SERVER['HTTP_X_REAL_IP'])){//nginx 代理模式下,获取客户端真实 IP
        $ip=$_SERVER['HTTP_X_REAL_IP'];
    }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {//客户端的 ip
        $ip     =   $_SERVER['HTTP_CLIENT_IP'];
    }elseif (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($arr[0]);
    }elseif (isset($_SERVER['REMOTE_ADDR'])) {
        $ip     =   $_SERVER['REMOTE_ADDR'];//浏览当前页面的用户计算机的 ip 地址
    }else{
        $ip=$_SERVER['REMOTE_ADDR'];
    }
    // IP 地址合法验证
    $long = sprintf("%u",ip2long($ip));
    $ip   = $long ? array($ip, $long) : array('0.0.0.0', 0);
    return $ip[$type];
}

接口返回

<xml><return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wxdded766660f9b840]]></appid>
<mch_id><![CDATA[1516216351]]></mch_id>
<device_info><![CDATA[100]]></device_info>
<nonce_str><![CDATA[2DUN2i2pGnlC6vDi]]></nonce_str>
<sign><![CDATA[95CEA831D598299097A32D8FEEC6BDEF]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<prepay_id><![CDATA[wx22194530678545eb3713f2f10724143329]]></prepay_id>
<trade_type><![CDATA[MWEB]]></trade_type>
<mweb_url><![CDATA[https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx22194530678545eb3713f2f10724143329&package=87106983]]></mweb_url>
  • return_codeSUCCESS代表支付请求成功
/**
 * 错误代码
 * @param  $code       服务器输出的错误代码
 * return string
 */
function getErrorMessage( $code ){
    $errList = array(
        'NOAUTH'                =>  '商户未开通此接口权限',
        'NOTENOUGH'             =>  '用户帐号余额不足',
        'ORDERNOTEXIST'         =>  '订单号不存在',
        'ORDERPAID'             =>  '商户订单已支付,无需重复操作',
        'ORDERCLOSED'           =>  '当前订单已关闭,无法支付',
        'SYSTEMERROR'           =>  '系统错误!系统超时',
        'APPID_NOT_EXIST'       =>  '参数中缺少APPID',
        'MCHID_NOT_EXIST'       =>  '参数中缺少MCHID',
        'APPID_MCHID_NOT_MATCH' =>  'appid和mch_id不匹配',
        'LACK_PARAMS'           =>  '缺少必要的请求参数',
        'OUT_TRADE_NO_USED'     =>  '同一笔交易不能多次提交',
        'SIGNERROR'             =>  '参数签名结果不正确',
        'XML_FORMAT_ERROR'      =>  'XML格式错误',
        'REQUIRE_POST_METHOD'   =>  '未使用post传递参数 ',
        'POST_DATA_EMPTY'       =>  'post数据不能为空',
        'NOT_UTF8'              =>  '未使用指定编码格式',
    );
    if( array_key_exists( $code , $errList ) ){
        return $errList[$code];
    }
}
  • mweb_url为支付跳转页,此时客户端通过mweb_url已经可以调起微信支付

mweb_url是为拉起微信支付收银台的中间页面,可通过访问该URL来拉起微信客户端,完成支付,mweb_url的有效期为 5 分钟。

https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096

正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在mweb_url后拼接上redirect_url参数(需对redirect_url进行urlencode处理),来指定回调页面。

在设置redirect_url时需要注意的是设置的回跳地址的域名与申请H5支付时提交的授权域名是否一致,否则会出现错误,若想通过redirect_url跳转到本域名之外的地址,可以先跳回域名内地址之后再跳出到域名外的地址。

https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn

微信的移动端WAP支付,在非微信浏览器中,需要通过微信的H5支付方式来唤起微信支付客户端,实现是通过拼装一个DeepLink链接,并访问该链接达到唤起微信支付客户端,客户端也可以通过此DeepLink值直接调起支付。通过测试发现,存在浏览器兼容问题。

weixin://wap/pay?prepayid%3Dwx22201221074146ac747121890095299503&package=2656135616&noncestr=1542888966&sign=e31dbc2d1231708ff8a982b15a6c7646

在得到微信返回的mweb_url参数后,可在服务端进一步获得DeepLink

/**
 * 获取微信支付中间页deepLink参数
 * @param string $url 微信返回的mweb_url
 * @param string $ip 用户端IP
 */
function getDeeplink(string $url, string $ip)
{
    $headers = array("X-FORWARDED-FOR:$ip", "CLIENT-IP:$ip");
    ob_start();
    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $url);
    curl_setopt ($ch, CURLOPT_HTTPHEADER , $headers );
    curl_setopt ($ch, CURLOPT_REFERER, "pay.o9di.cn");
    curl_setopt( $ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Linux; Android 6.0.1; OPPO R11s Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36');
    curl_exec($ch);
    curl_close ($ch);
    $out = ob_get_contents();
    ob_clean();
    $a = preg_match('/weixin:\/\/wap.*/',$out, $str);
    if ($a) {
        return substr($str[0], 0, strlen($str[0])-1);
    } else {
        return '';
    }
}

支付中间页

支付中间页
支付中间页

同步回调

正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在 MWEB_URL 后拼接上 redirect_url 参数,来指定回调页面。需对 redirect_url 进行 urlencode 处理。

$redirect_url  = "https://www.wechatpay.com.cn";
$redirect_url = urlencode($redirect_url);
// 返回至指定页面
$mweb_url = $mweb_url."&redirect_url=".$redirect_url;

由于设置redirect_url后,回跳指定页面的操作可能发生在:

  1. 微信支付中间页调起微信收银台后超过5秒
  2. 用户点击“取消支付“或支付完成后点“完成”按钮。

因此无法保证页面回跳时,支付流程已结束,所以商户设置的redirect_url地址不能自动执行查单操作,应让用户去点击按钮触发查单操作。

异步回调

// 接收text/xml数据
$str_Post = file_get_contents("php://input");

// 禁止引用外部XML实体
libxml_disable_entity_loader(true);

$postObj = simplexml_load_string($str_Post, 'SimpleXMLElement', LIBXML_NOCDATA);

$postObj = json_encode($postObj);
$postObj = json_decode($postObj, true);

$result_code = trim($postObj["result_code"]);
$return_code = trim($postObj["return_code"]);
$sign = trim($postObj["sign"]);
$out_trade_no = trim($postObj["out_trade_no"]);

if ($result_code  == 'SUCCESS' && $return_code == 'SUCCESS') {

}

类库

<?php
/**
 * 微信支付服务器端下单
 * 微信APP支付文档地址:  https://pay.weixin.qq.com/wiki/doc/api/app.php?chapter=8_6
 * 使用示例
 *  构造方法参数
 *      'appid'     =>  //填写微信分配的公众账号ID
 *      'mch_id'    =>  //填写微信支付分配的商户号
 *      'notify_url'=>  //填写微信支付结果回调地址
 *      'key'       =>  //填写微信商户支付密钥
 *  );
 *  统一下单方法
 *  $obj = new WxPay($options);
 *  $params['body'] = '商品描述';                   //商品描述
 *  $params['out_trade_no'] = '1217752501201407';   //自定义的订单号,不能重复
 *  $params['total_fee'] = '100';                   //订单金额 只能为整数 单位为分
 *  $params['trade_type'] = 'APP';                  //交易类型 JSAPI | NATIVE |APP | WAP
 *  $obj->unifiedOrder( $params );
 */
class WxPay
{
    //接口API URL前缀
    const API_URL_PREFIX = 'https://api.mch.weixin.qq.com';
    //下单地址URL
    const UNIFIEDORDER_URL = "/pay/unifiedorder";
    //查询订单URL
    const ORDERQUERY_URL = "/pay/orderquery";
    //关闭订单URL
    const CLOSEORDER_URL = "/pay/closeorder";
    //公众账号ID
    private $appid;
    //商户号
    private $mch_id;
    //随机字符串
    private $nonce_str;
    //签名
    private $sign;
    //商品描述
    private $body;
    //商户订单号
    private $out_trade_no;
    //支付总金额
    private $total_fee;
    //终端IP
    private $spbill_create_ip;
    //支付结果回调通知地址
    private $notify_url;
    //交易类型
    private $trade_type;
    //支付密钥
    private $key;
    //证书路径
    private $SSLCERT_PATH;
    private $SSLKEY_PATH;
    //所有参数
    private $params = [];
    
    public function __construct($appid, $mch_id, $notify_url, $key)
    {
        $this->appid = $appid;
        $this->mch_id = $mch_id;
        $this->notify_url = $notify_url;
        $this->key = $key;
    }
    /** 场景信息 必要参数*/
    public function buildSceneInfo($wap_url, $wap_name="支付")
    {
        return '{"h5_info":{"type":"Wap","wap_url":"'.$wap_url.'","wap_name":"'.$wap_name.'"}}';
    }
    /**
     * 下单方法
     * @param   $params 下单参数
     */
    public function unifiedOrder( $params ){
        $this->body = $params['body'];
        $this->out_trade_no = $params['out_trade_no'];
        $this->total_fee = $params['total_fee'];
        $this->trade_type = $params['trade_type'];
        $this->scene_info = $params['scene_info'];
        $this->nonce_str = $this->genRandomString();
        $this->spbill_create_ip = $_SERVER['REMOTE_ADDR'];
        
        $this->params['appid'] = $this->appid;
        $this->params['mch_id'] = $this->mch_id;
        $this->params['nonce_str'] = $this->nonce_str;
        $this->params['body'] = $this->body;
        $this->params['out_trade_no'] = $this->out_trade_no;
        $this->params['total_fee'] = $this->total_fee;
        $this->params['spbill_create_ip'] = $this->spbill_create_ip;
        $this->params['notify_url'] = $this->notify_url;
        $this->params['trade_type'] = $this->trade_type;
        $this->params['scene_info'] = $this->scene_info;
        
        //获取签名数据
        $this->sign = $this->makeSign( $this->params );
        $this->params['sign'] = $this->sign;
        
        $xml = $this->makeArrayToXml($this->params);
        $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::UNIFIEDORDER_URL);
        if( !$response ){
            return false;
        }
        
        $result = $this->parseXmlToArray( $response );
        if( !empty($result['result_code']) && !empty($result['err_code']) ){
            $result['err_msg'] = $this->getErrorMessage( $result['err_code'] );
        }
        return $result;
    }

    /**
     * 获取微信支付中间页deepLink参数
     * 获取deepLink客户端通过次链接可直接调起支付
     * @param string $url 微信返回的mweb_url
     * @param string $ip 用户端IP
     */
    public function getDeepLink($url, $ip)
    {
        $headers = ["X-FORWARDED-FOR:$ip", "CLIENT-IP:$ip"];

        ob_start();
        $ch = curl_init();
        curl_setopt ($ch, CURLOPT_URL, $url);
        curl_setopt ($ch, CURLOPT_HTTPHEADER , $headers );
        curl_setopt ($ch, CURLOPT_REFERER, "pay.o9di.cn");
        curl_setopt( $ch, CURLOPT_HEADER, 1);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Linux; Android 6.0.1; OPPO R11s Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36');
        curl_exec($ch);
        curl_close ($ch);
        $out = ob_get_contents();
        ob_clean();

        $a = preg_match('/weixin:\/\/wap.*/',$out, $str);
        if ($a) {
            return substr($str[0], 0, strlen($str[0])-1);
        } else {
            return '';
        }
    }

    /**
     * 查询订单信息
     * @param $out_trade_no     订单号
     * @return array
     */
    public function orderQuery( $out_trade_no ){
        $this->params['appid'] = $this->appid;
        $this->params['mch_id'] = $this->mch_id;
        $this->params['nonce_str'] = $this->genRandomString();
        $this->params['out_trade_no'] = $out_trade_no;

        //获取签名数据
        $this->sign = $this->makeSign( $this->params );
        $this->params['sign'] = $this->sign;
        $xml = $this->makeArrayToXml($this->params);
        $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::ORDERQUERY_URL);
        if( !$response ){
            return false;
        }
        $result = $this->parseXmlToArray( $response );
        if( !empty($result['result_code']) && !empty($result['err_code']) ){
            $result['err_msg'] = $this->getErrorMessage( $result['err_code'] );
        }
        return $result;
    }
    /**
     * 关闭订单
     * @param $out_trade_no     订单号
     * @return array
     */
    public function closeOrder( $out_trade_no ){
        $this->params['appid'] = $this->appid;
        $this->params['mch_id'] = $this->mch_id;
        $this->params['nonce_str'] = $this->genRandomString();
        $this->params['out_trade_no'] = $out_trade_no;
        //获取签名数据
        $this->sign = $this->makeSign( $this->params );
        $this->params['sign'] = $this->sign;
        $xml = $this->makeArrayToXml($this->params);
        $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::CLOSEORDER_URL);
        if( !$response ){
            return false;
        }
        $result = $this->parseXmlToArray( $response );
        return $result;
    }
    /**
     *
     * 获取支付结果通知数据
     * return array
     */
    public function getNotifyData(){
        //获取通知的数据
        $xml = $GLOBALS['HTTP_RAW_POST_DATA'];
        $data = array();
        if( empty($xml) ){
            return false;
        }
        $data = $this->parseXmlToArray( $xml );
        if( !empty($data['return_code']) ){
            if( $data['return_code'] == 'FAIL' ){
                return false;
            }
        }
        return $data;
    }
    /**
     * 接收通知成功后应答输出XML数据
     * @param string $xml
     */
    public function replyNotify(){
        $data['return_code'] = 'SUCCESS';
        $data['return_msg'] = 'OK';
        $xml = $this->makeArrayToXml( $data );
        echo $xml;
        die();
    }
    /**
     * 生成APP端支付参数
     * @param  $prepayid   预支付id
     */
    public function getAppPayParams( $prepayid ){
        $data = [];
        $data['appid'] = $this->appid;
        $data['partnerid'] = $this->mch_id;
        $data['prepayid'] = $prepayid;
        $data['package'] = 'Sign=WXPay';
        $data['noncestr'] = $this->genRandomString();
        $data['timestamp'] = time();
        $data['sign'] = $this->makeSign( $data );
        return $data;
    }
    /**
     * 生成签名
     *  @return 签名
     */
    public function makeSign( $params ){
        //签名步骤一:按字典序排序数组参数
        ksort($params);
        $string = $this->ToUrlParams($params);
        //签名步骤二:在string后加入KEY
        $string = $string . "&key=".$this->key;
        //签名步骤三:MD5加密
        $string = md5($string);
        //签名步骤四:所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }
    /**
     * 将参数拼接为url: key=value&key=value
     * @param   $params
     * @return  string
     */
    public function ToUrlParams( $params ){
        $string = '';
        if( !empty($params) ){
            $array = array();
            foreach( $params as $key => $value ){
                $array[] = $key.'='.$value;
            }
            $string = implode("&",$array);
        }
        return $string;
    }
    /**
     * 输出xml字符
     * @param   $params     参数名称
     * return   string      返回组装的xml
     **/
    public function makeArrayToXml( $params ){
        if(!is_array($params)|| count($params) <= 0)
        {
            return false;
        }
        $xml = "<xml>";
        foreach ($params 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 array
     */
    public function parseXmlToArray($xml){
        if(!$xml){
            return false;
        }
        //将XML转为array
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $data;
    }
    /**
     * 获取毫秒级别的时间戳
     */
    private static function getMillisecond(){
        //获取毫秒的时间戳
        $time = explode ( " ", microtime () );
        $time = $time[1] . ($time[0] * 1000);
        $time2 = explode( ".", $time );
        $time = $time2[0];
        return $time;
    }
    /**
     * 产生一个指定长度的随机字符串,并返回给用户
     * @param type $len 产生字符串的长度
     * @return string 随机字符串
     */
    private function genRandomString($len = 32) {
        $chars = array(
            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
            "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
            "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
            "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
            "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
            "3", "4", "5", "6", "7", "8", "9"
        );
        $charsLen = count($chars) - 1;
        // 将数组打乱 
        shuffle($chars);
        $output = "";
        for ($i = 0; $i < $len; $i++) {
            $output .= $chars[mt_rand(0, $charsLen)];
        }
        return $output;
    }
    /**
     * 以post方式提交xml到对应的接口url
     *
     * @param string $xml  需要post的xml数据
     * @param string $url  url
     * @param bool $useCert 是否需要证书,默认不需要
     * @param int $second   url执行超时时间,默认30s
     * @throws WxPayException
     */
    private function postXmlCurl($xml, $url, $useCert = false, $second = 30){
        $ch = curl_init();
        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch,CURLOPT_URL, $url);
        curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
        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, $xml);
        //运行curl
        $data = curl_exec($ch);
        //返回结果
        if($data){
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            return false;
        }
    }
    /**
     * 错误代码
     * @param  $code       服务器输出的错误代码
     * return string
     */
    public function getErrorMessage( $code ){
        $errList = array(
            'NOAUTH'                =>  '商户未开通此接口权限',
            'NOTENOUGH'             =>  '用户帐号余额不足',
            'ORDERNOTEXIST'         =>  '订单号不存在',
            'ORDERPAID'             =>  '商户订单已支付,无需重复操作',
            'ORDERCLOSED'           =>  '当前订单已关闭,无法支付',
            'SYSTEMERROR'           =>  '系统错误!系统超时',
            'APPID_NOT_EXIST'       =>  '参数中缺少APPID',
            'MCHID_NOT_EXIST'       =>  '参数中缺少MCHID',
            'APPID_MCHID_NOT_MATCH' =>  'appid和mch_id不匹配',
            'LACK_PARAMS'           =>  '缺少必要的请求参数',
            'OUT_TRADE_NO_USED'     =>  '同一笔交易不能多次提交',
            'SIGNERROR'             =>  '参数签名结果不正确',
            'XML_FORMAT_ERROR'      =>  'XML格式错误',
            'REQUIRE_POST_METHOD'   =>  '未使用post传递参数 ',
            'POST_DATA_EMPTY'       =>  'post数据不能为空',
            'NOT_UTF8'              =>  '未使用指定编码格式',
        );
        if( array_key_exists( $code , $errList ) ){
            return $errList[$code];
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容