thinkphp5微信公众号支付

实习期间,参与了微信公众号开发,接触到了微信公众号支付,在开发过程中踩了不少的坑,好在顺利完成了任务,在这里,我觉得有必要和大家分享一下,也便于自己以后参考。

一、场景介绍

用户通过微信公众号打开网页, 进入到如下界面,选择或输入相应金额为网站中用户账号充值。

二、开发步骤

这里就不再介绍商户如何接入微信支付,官方文档中已经有了详细介绍,具体请参考微信公众号支付官方文档。直接上业务流程。首先我们来看看微信官方给我们的流程图:

刚开始接触,看到这个流程图可能会有点懵逼,没关系,这里我给大家画了一个简单的流程图:

概括一下:

1)获取用户授权(当用户进入到充值界面的时候)

2)后台调用统一下单接口获取预支付订单,并返回给前端

3)前端H5调起微信支付(调起成功后,会提示输入支付密码),

4)微信向前端和回调地址发送支付结果通知(两者时序不分前后,以回调地址通知为准)

5)回调处理(很重要,后面详细介绍)

好了,相信你已经对微信公众号支付的整个流程有了一定的了解,那我们就开始编码吧。

三、代码实现

1、前端表单提交充值金额,核心代码如下:

<div class="weui-cell">
    <div class="weui-cell__hd">
        <label class="weui-label">金额:</label>
    </div>
    <div class="weui-cell__bd">
        <input class="weui-input" name="money" id = "money" type="number"placeholder="金额">
    </div>
</div>
<div class="weui-btn-area">
    <button class="weui-btn weui-btn_primary" id="chargebtn" type="button">确定</button>
</div>

ajax请求将数据发送给后台

//请求支付,提交支付金额,让后台进行统一下单操作
function charge() {
    var money = $('#money').val();
    $.ajax({
        url:header_url+'pay/pay', //后台接收数据,进行统一下单操作的地址,填你自己的
        dataType:'json',
        type:'POST',
        data:{'money':money},
            success:function (data) {
                //后台统一下单完成,返回前端数据中包含预支付订单的各种参数
                var res = eval('('+data+')');
                //调起支付
                callpay(res['data']);
        }
    })
}

统一下单和前端调起支付,在下面详细讲解 ↓↓↓↓↓↓

2、基本配置、代码引入及统一下单操作

1)tp5引入官方案例代码

下载官方案例文件:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

在tp5项目文件extend目录下创建pay目录,在pay目录下创建wxpay目录,将代码解压到此目录。(你的项目可能不止会使用到微信支付,所有我们建立的目录结构要清晰规范),并且在wxpay目录下创建cert目录保存商户证书文件(apiclient_cert.pem和apiclient_key.pem)

2)微信支付配置

配置官方案例文件下的lib文件夹下的WxPay.Config.php,至于怎么填这里就不再赘述,案例中有详细解释

3)代码引入

我创建了一个server模块,在pay控制器里专门处理微信支付。首先引入必要文件

<?php
namespace app\server\controller;
use app\server\model\Receive;
use app\server\model\Recharge;
use app\server\model\Wuser;
use think\Loader;
use think\Controller;
Loader::import('pay.wxpay.lib.WxPay', EXTEND_PATH,'.Api.php');
Loader::import('pay.wxpay.example.WxPay', EXTEND_PATH,'.JsApiPay.php');
Loader::import('pay.wxpay.example.log', EXTEND_PATH,'.php');
Loader::import('pay.wxpay.lib.WxPay', EXTEND_PATH,'.Config.php');
class Pay extends Controller {

至此,我们就可以开始进行微信公众号支付后台开发了。前面说到了前端发送充值金额到后台(之前你要获取到了用户授权,得到用户openID,保存在session中),那么后台接收数据,进行统一下单操作。

4)统一下单(API:  https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 )

        构造统一下单对象,对象的字段包括里面的必填字段,有额外需要的可以自己添加。特别注意里面对请求参数的描述。下面是我使用到的参数:

appid  -> 公众账号ID (发起支付请求的公众号)

mch_id -> 商户号(收款人)

openid -> 微信用户openid(付款人)

out_trade_no  -> 商户订单号(商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一)

total_fee -> 订单总金额

body  -> 商品描述

attach  -> 附加数据(在查询API和支付通知中原样返回,可作为自定义参数使用)

time_start -> 交易开始时间

time_expire -> 交易结束时间

goods_tag -> 订单优惠标记(订单优惠标记,使用代金券或立减优惠功能时需要的参数,实际上这里用不到,可以不要该参数)

notify_url -> 回调地址(异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数)

trade_type -> 交易类型(支付类型,这里用到的是JSAPI 公众号支付)

spbill_create_ip ->调用微信支付API的机器IP

nonce_str -> 随机字符串(随机字符串,长度要求在32位以内)

sign -> 签名(用上面的参数按照规定签名之后得到的结果,具体前面步骤请查看文档和官方案例代码,已有详细描述)

具体代码如下:

    public function pay() {
        $tools = new \JsApiPay();
        if($this->request->isPost()){
            $data = input('post.');
            $money = $data['money'] *100; //微信支付以分为单位
            $logHandler= new \CLogFileHandler(EXTEND_PATH."pay/wxpay/logs/".date('Y-m-d').'.log');
            $log = \Log::Init($logHandler, 15);
            //①、获取用户openid
            $openId = session('openid');
            $userId = session('id');                               
            $input = new \WxPayUnifiedOrder();
            $input->SetBody("test"); //商品描述
            $input->SetAttach($userId); //附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
            $input->SetOut_trade_no(\WxPayConfig::MCHID.date("YmdHis"));//商户订单号
            $input->SetTotal_fee($money);//订单金额
            $input->SetTime_start(date("YmdHis"));//交易起始时间
            $input->SetTime_expire(date("YmdHis", time() + 600));//交易结束时间
            $input->SetGoods_tag("test"); //订单优惠标记,使用代金券或立减优惠功能时需要的参数,实际上这里可以不要
            $input->SetNotify_url("http://www.xxxx.com/wechat/index.php/server/pay/notify");//接收回调通知地址
            $input->SetTrade_type("JSAPI"); //支付类型
            $input->SetOpenid($openId); //用户openid
            $order = \WxPayApi::unifiedOrder($input); //统一下单,该方法中包含了签名算法
            $jsApiParameters = $tools->GetJsApiParameters($order); //统一下单参数
            //将统一下单接口生成的预支付订单参数返回给前端,前端就可以调取支付了
            return getBack(1,$jsApiParameters);//getBack是我自定义的方法,就是给前端ajax请求返回json格式数据,1代表成功,这里你要自己修改。
        }else {
            //下面是展示前端页面的,与统一下单无关
            $openId = session('openid');
            $this->assign('user',session('username'));
            $this->assign('openId',$openId);
            return $this->fetch('recharge');
        }
    }

在上面的方法中我们只需要给必要的参数就行了,签名和具体下单操作,在官方案例已经给我们实现了,具体请查看unifiedOrder()和GetJsApiParameters()方法代码。当然官方案例中可能会存在一些错误,比如我就遇到,一个参数(好像是设置请求过期时间的)没有定义就直接使用了,我直接给他设置了一个默认值。打断点改错误,我相信大家还是有一定debug能力的。

现在我们在后台调用统一下单接口,得到了预支付订单,并返回给前端,前端就可以通过后台返回的预支付订单参数来调起支付,调起成功(参数没有问题,统一下单无误)会提示输入支付密码。

3、前端h5调起支付

//前端吊起支付
//jsApiParameters是后台返回的预支付订单各种参数的json格式数据
function callpay(jsApiParameters) {
    if (typeof WeixinJSBridge == "undefined"){
        if( document.addEventListener ){
            document.addEventListener('WeixinJSBridgeReady', jsApiCall(jsApiParameters), false);
        }else if (document.attachEvent){
            document.attachEvent('WeixinJSBridgeReady', jsApiCall(jsApiParameters));
            document.attachEvent('onWeixinJSBridgeReady', jsApiCall(jsApiParameters));
        }
    }else{
        jsApiCall(jsApiParameters);
    }
}

这里我就不得不说我遇到的最大的一个坑了,先看看官方给我们的代码

	//调用微信JS api 支付
	function jsApiCall()
	{
		WeixinJSBridge.invoke(
			'getBrandWCPayRequest',
			<?php echo $jsApiParameters; ?>,
			function(res){
				WeixinJSBridge.log(res.err_msg);
				alert(res.err_code+res.err_desc+res.err_msg);
			}
		);
	}

我之前用官方这个代码,用PHP代码直接输出jsApiParameters,始终提示签名验证失败,我反反复复验证我的统一下单操作,还是提示签名验证失败,实在是找不到错误原因了,最后阅读文档,发现调起支付时参数顺序有要求,会不会是官方案例中的预支付订单参数顺序出错了呢?于是进行下面的修改

//前端吊起支付
function jsApiCall(jsApiParameters) {
    var jsApiParameters = eval('(' + jsApiParameters + ')');
    console.log(jsApiParameters);
    WeixinJSBridge.invoke(
            'getBrandWCPayRequest',{
            "appId":jsApiParameters['appId'],     //公众号名称,由商户传入
            "timeStamp":jsApiParameters['timeStamp'],         //时间戳,自1970年以来的秒数
            "nonceStr":jsApiParameters['nonceStr'], //随机串
            "package":jsApiParameters['package'],
            "signType":jsApiParameters['signType'],         //微信签名方式:
            "paySign":jsApiParameters['paySign']//微信签名
        },
        //上面参数一定要按照一定的顺序排列,否则会出错(签名验证失败)
        function(res) {
                //前端接收到支付结果通知,get_brand_wcpay_request:ok,支付成功
              //(但是不一定就是真的成功了,一切以回调地址中的结果为准,前端接收到支付通知后只做跳转,不做任何处理)
             //商户订单处理(更新用户账号余额)要放在回调地址中处理
                if(res.err_msg == "get_brand_wcpay_request:ok" ) {
                    //支付成功,跳转到其他页面
                    location.href=header_url+'index/balance';
                }
        }
    );
}

 最终成功调起支付,输入支付密码,满怀欣喜的为公司贡献了1分钱!!!

然后你以为这就完了?我们冲了钱,但是我网站账户上面的余额是0啊!订单操作应该放在哪里进行了?是前端接收到成功,再次ajax请求到后台,给用户充钱?NO NO NO ! 这样的做法极不安全!不是还有一个回调地址也能接收到支付结果通知吗,下面我们就来讲讲回调处理。

4、回调处理

首先看看微信官方的解释:

在完成支付之后,微信会返回支付结果给前端,并且也会向回调地址中发送支付结果通知。这里需要注意的是,前端和回调地址接收到微信支付结果通知的顺序是不确定的,前端接收到的结果不是完全可靠的,所以一切以回调地址中收到的结果为准。在前端接收到返回的支付结果时,只做页面跳转,不做其他处理,应该在回调地址中处理商户订单逻辑(接收到支付成功,更新用户账号余额),看官方给我们的解释:

 回调地址中接收到的支付结果数据格式具体请参考api https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8

这里还有一个重点,回调地址中接收到的数据,就一定是微信服务器发送过来的吗?也有可能是数据泄露,别人知道了你的回调url,发送伪造的支付结果通知数据到该地址,所以在接收到数据时一定要签名验证过后才做支付成功处理。

 最后在接收到数据完成订单业务操作之后,千万不要忘了返回给微信处理结果(告诉微信这个订单我处理完了,你不要再发消息过来了)试想,如果你不返回处理结果给微信,微信会再次发送支付结果通知到你的回调地址,这时你接收到数据,又做同样的操作,用户一次付款,后台多次给账号充值,导致公司财产严重损失!

说了这么多,来看看代码上改如何处理:

    public function notify() {
        ini_set('date.timezone','Asia/Shanghai');
        error_reporting(E_ERROR);
        //初始化日志
        $logHandler= new \CLogFileHandler(EXTEND_PATH.'pay/wxpay/logs/'.date('Y-m-d').'.log');
        $log = \Log::Init($logHandler, 15);
        $xml = $this->postdata();
        $xmlTpl = "<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>";
        if(!$xml) {
            $result = sprintf($xmlTpl,'FAIL','xml数据异常!');
        }
        //日志记录接收到的数据
        \Log::DEBUG("begin notify");
        \Log::DEBUG("$xml");
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $obj = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        ksort($obj);
        $str = $this->ToUrlParams($obj);
        $string = $str."&key=".\WxPayConfig::KEY;
        $user_sign = strtoupper(md5($string));
        if($user_sign == $obj['sign']) {
            \Log::DEBUG("回调签名验证成功");
            //验证成功
            $order = $obj['out_trade_no'];//订单号
            $userid = $obj['attach'];//用户id
            $money = $obj['total_fee'];//金额
            $transaction_id = $obj['transaction_id'];//微信支付订单号
            $recharge_record = new Recharge();
            //检查该订单是否已经处理过,处理过就直接返回微信
            $status = $recharge_record->where('wechat_order_code',$transaction_id)->find();
            if($status) {
                $result = sprintf($xmlTpl,'SUCCESS','OK');
                echo $result;
                exit();
            }
            //更新用户账号余额
            $user = new Wuser();
            $res = $user->where('id',$userid)->field('property')->find();
            //最好两张表关联写入
            $money = $money*0.01;
            \Log::DEBUG('账号余额为:'.$res['property']+$money);
            $ret = $user->save([
                'property'=>$res['property']+$money,
                'update_time'=>time()
            ],['id'=>$userid]);
            if($ret){
                \Log::DEBUG("充值成功");
                $recharge_record->save([
                    'user_id'=>$userid,
                    'money'=>$money,
                    'create_time'=>time(),
                    'out_trade_no'=>$order,
                    'wechat_order_code'=>$transaction_id
                ]);
                $result = sprintf($xmlTpl,'SUCCESS','OK');
            }else{
                \Log::DEBUG("充值失败");
                $result = sprintf($xmlTpl,'FAIL','充值失败');
            }
        }else{
            \Log::DEBUG("签名错误");
            $result = sprintf($xmlTpl,'FAIL','签名错误!');
        }
        echo $result;
        exit();
    }
    /*
     * 接收post数据
    */
    public function postdata() {
            $receipt = $_REQUEST;
            if($receipt==null){
                $receipt = file_get_contents("php://input");
                if($receipt == null) {
                    $receipt = $GLOBALS['HTTP_RAW_POST_DATA'];
                }
            }
            return $receipt;
        }
    /**
     * 格式化参数格式化成url参数
     */
    public function ToUrlParams($value) {
        $buff = "";
        foreach ($value as $k => $v)
        {
            if($k != "sign" && $v != "" && !is_array($v)){
                $buff .= $k . "=" . $v . "&";
            }
        }

        $buff = trim($buff, "&");
        return $buff;
    }

四、项目总结

        至此,我们整个微信公众号支付的开发流程就结束了,希望这篇博文能对大家有所帮助。以上微信公众号支付处理过程是根据自己的理解归纳总结的,不足之处,欢迎大家指正。

猜你喜欢

转载自blog.csdn.net/Forever_and_ever/article/details/81072895