微信支付之公众号支付

 提示:转载请注明原文链接

 本文永久链接:http://360us.net/article/22.html

最近把支付宝、银联和微信支付全都做了一遍,目前做的都还只涉及到消费的功能。
做下来感觉就是各个平台的支付流程都是大同小异,签名方式也是一样的。

这里主要总结一下微信支付公众号支付的一些东西。

微信公众号支付的主要流程如下:
1、生成我们自己系统的订单。
2、调用微信支付的统一下单接口把订单信息推给微信。
3、在第二部会返回一个预支付会话标识,然后凭这个标识用JS去调用支付操作。


关于支付页面的url问题,微信要求是最后必须要有“/”,我看到很多文章说不适合MVC结构的程序,我的情况是否定的,MVC结构一样可以。
比如url是这个:http://www.example.com/payment/wechatpay/ ,url里面paymentcontroller,wechatpayaction,这有问题吗?
一样可以访问,可以支付,是不是一个真正的目录,在微信看来就是,实际上其实不是。


好,下面进入正题。


微信支付配置如下:

$config = [
    'mch_id' => '1234455666', //商户号
    'signType' => 'MD5', //签名方式,目前只有MD5
    'key' => 'sdsfdhgjh34343krn3453tnelt', //api密钥
];

Weixinpay代码清单如下:

<?php
namespace weixin\components; //这个是命名空间,可以根据需要修改

/**
* @link http://www.360us.net/ 
* @author dyllen_zhong@qq.com
*/
class WeixinPay
{
    //支付配置
    public $config;
    
    //支付参数
    public $params;
    
    //统一下单url
    const POST_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
    
    //订单查询url
    const ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery';
    
    /**
     * 创建微信js发起支付参数
     * @return array
     */
    public function createJsPayData()
    {
        $this->params['nonce_str'] = $this->getRandomStr();
        $this->params['sign'] = $this->sign();
        
        $xmlStr = $this->arrayToXml();
        
        $res = $this->postUrl(self::POST_ORDER_URL, $xmlStr);
        $res = $this->xmlToArray($res);
        if( $res['return_code'] == 'SUCCESS' && $res['result_code'] == 'SUCCESS' && $this->verifySignResponse($res) ) {
            $params = [
                'appId' => $this->params['appid'],
                'timeStamp' => (string)time(),
                'nonceStr' => $this->getRandomStr(),
                'package' => 'prepay_id='.$res['prepay_id'],
                'signType' => 'MD5'
            ];
            
            $this->params = $params;
            $this->params['paySign'] = $this->sign();
            return $this->params;
        }
        if($res['return_code'] == 'FAIL') {
            throw new \Exception("提交预支付交易单失败:{$res['return_msg']}");
        }
        
        throw new \Exception("提交预支付交易单失败,{$res['err_code']}:{$res['err_code']}");
    }
    
    /**
     * 验证异步通知
     * @return boolean
     */
    public function verifyNotify()
    {
        $this->params = $this->xmlToArray($this->params);
        if( empty($this->params['sign']) ) {
            return false;
        }
        $sign = $this->sign();
        return $this->params['sign'] == $sign;
    }
    
    /**
     * 取成功响应
     * @return string
     */
    public function getSucessXml()
    {
        $xml = '<xml>';
        $xml .= '<return_code><![CDATA[SUCCESS]]></return_code>';
        $xml .= '<return_msg><![CDATA[OK]]></return_msg>';
        $xml .= '</xml>';
        return $xml;
    }
    
    public function getFailXml()
    {
        $xml = '<xml>';
        $xml .= '<return_code><![CDATA[FAIL]]></return_code>';
        $xml .= '<return_msg><![CDATA[OK]]></return_msg>';
        $xml .= '</xml>';
        return $xml;
    }
    
    /**
     * 数组转成xml字符串
     * 
     * @return string
     */
    protected function arrayToXml()
    {
        $xml = '<xml>';
        foreach($this->params as $key => $value) {
            $xml .= "<{$key}>";
            $xml .= "<![CDATA[{$value}]]>";
            $xml .= "</{$key}>";
        }
        $xml .= '</xml>';
        
        return $xml;
    }
    
    /**
     * xml 转换成数组
     * @param string $xml
     * @return array
     */
    protected function xmlToArray($xml)
    {
        $xmlObj = simplexml_load_string(
                $xml,
                'SimpleXMLIterator',   //可迭代对象
                LIBXML_NOCDATA
        );
        
        $arr = [];
        $xmlObj->rewind(); //指针指向第一个元素
        while (1) {
            if( ! is_object($xmlObj->current()) )
            {
                break;
            }
            $arr[$xmlObj->key()] = $xmlObj->current()->__toString();
            $xmlObj->next(); //指向下一个元素
        }
        
        return $arr;
    }
    
    //验证统一下单接口响应
    protected function verifySignResponse($arr)
    {
        $tmpArr = $arr;
        unset($tmpArr['sign']);
        ksort($tmpArr);
        $str = '';
        foreach($tmpArr as $key => $value) {
            $str .= "$key=$value&";
        }
        $str .= 'key='.$this->config['key'];
        
        if($arr['sign'] == $this->signMd5($str)) {
            return true;
        }
        return false;
    }
    
    
    /**
     * 签名
     * 规则:
     * 先按照参数名字典排序
     * 用&符号拼接成字符串
     * 最后拼接上API秘钥,str&key=密钥
     * md5运算,全部转换为大写
     * 
     * @return string
     */
    protected function sign()
    {
        ksort($this->params);
        $signStr = $this->arrayToString();
        $signStr .= '&key='.$this->config['key'];
        if($this->config['signType'] == 'MD5') {
            return $this->signMd5($signStr);
        }        
        
        throw new \InvalidArgumentException('Unsupported sign method');
    }
    
    /**
     * 数组转成字符串
     * @return string
     */
    protected  function arrayToString()
    {
        $params = $this->filter($this->params);
        $str = '';
        foreach($params as $key => $value) {
            $str .= "{$key}={$value}&";
        }
        
        return substr($str, 0, strlen($str)-1);
    }
    
    /*
     * 过滤待签名数据,sign和空值不参加签名
     * 
     * @return array
     */
    protected function filter($params)
    {
        $tmpParams = [];
        foreach ($params as $key => $value) {
            if( $key != 'sign' && ! empty($value) ) {
                $tmpParams[$key] = $value;
            }
        }
        
        return $tmpParams;
    }
    
    /**
     * MD5签名
     * 
     * @param string $str 待签名字符串
     * @return string 生成的签名,最终数据转换成大写
     */
    protected function signMd5($str)
    {
        $sign = md5($str);
        
        return strtoupper($sign);
    }
    
    /**
     * 获取随机字符串
     * @return string 不长于32位
     */
    protected function getRandomStr()
    {
        return substr( rand(10, 999).strrev(uniqid()), 0, 15 );
    }
    
    /**
     * 通过POST方法请求URL
     * @param string $url
     * @param array|string $data post的数据
     *
     * @return mixed
     */
    protected function postUrl($url, $data) {
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); //忽略证书验证
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        $result = curl_exec($curl);
        return $result;
    }
}

拿发起支付参数:

try{
    $weixinPay = new WeixinPay();
    $weixinPay->config = $config;
    $weixinPay->params = [
            'appid' => 'sdfdgf1234345', //APP ID
            'mch_id' => $config['mch_id'], //商户号
            'body' => 'test', //商品描述
            'out_trade_no' => 'esdfrdgegtr234365546', //订单号
            'total_fee' => 100, //总金额,单位分
            'spbill_create_ip' => '192.168.100.100', //终端IP
            'notify_url' => 'http://www.example.com/paynofify', //异步通知地址
            'trade_type' => 'JSAPI', //交易类型
            'openid' => 'xxxxdfdfdgdfxcvcvgfg', //用户标识
    ];
        
    $return = $weixinPay->createJsPayData();
} catch (\Exception $e) {
    Yii::error('微信支付错误:'.$e->getMessage());
    return [
            'code' => 0,
            'errmsg' => '创建支付参数失败',
    ];
}


变量$return的内容如下,就是网页调起支付api的参数:

[
    'appId' => 'dfgfg',  //APP ID
    'timeStamp' => (string)time(),  //时间戳
    'nonceStr' => 'dfdsfdgfgdsg',  //随机字符串
    'package' => 'prepay_id=sdsfgdhgfh4565756',  //预支付会话标识
    'signType' => 'MD5'
];

这里有个提示,timeStamp参数必须是字符串类型,不能是整数类型,否则在iPhone上面会报缺少timeStamp参数的错误。

我们这里可以直接响应json格式的数据。
然后拿到这个数据之后直接放进微信jsapi的参数里面就行。
js发起支付请求如下:

WeixinJSBridge.invoke(
    "getBrandWCPayRequest",
    params,   //这个就是上面$return变量的json格式
    //下面是支付完成后的回调,可以直接提示成功
    function(res) {
        if(res.err_msg == "get_brand_wcpay_request:ok") {
            //.......
        }
    }
);


如果支付成功之后,微信会发起主动调用,通知商户支付成功,业务处理可以放在那里进行。

$weixinPay = new WeixinPay();
$weixinPay->config = $config;
$weixinPay->params = 'xxxxx'; //微信通知提交过来的xml

if(empty($weixinPay->params) || !$weixinPay->verifyNotify()) {
    return $weixinPay->getFailXml();
}

if($weixinPay->params['return_code'] == 'SUCCESS' && $weixinPay->params['result_code'] == 'SUCCESS') {
    //处理业务....
    //.....
    return $weixinPay->getSucessXml();
}

return $weixinPay->getFailXml();


至此微信支付的整个过程就结束了。
需要注意的一点是微信5.0以下版本不支持微信支付功能。
还有就是在支付url后面加上showwxpaytitle=1字符串,会有“微信安全支付”的文字提示,最终的url就变成了http://www.example.com/payment/wechatpay/?showwxpaytitle=1

 评论
ulooper 2016-10-14 17:55:04
现在showwxpaytitle=1是不是没用了 加了也没用
prime 2016-08-10 14:49:06
很多第三方支付都要求链接不能带参数,我觉得主要原因是防止回调时参数冲突
xiaoba 2015-06-29 19:38:07
boss 我用yii实现微信支付依然不行啊,请问步奏详细有吗
昵称
邮箱
网址
最多500个字符