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;
    }

 

四、项目总结

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

 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
基于ThinkPHP 5.1框架打造的多商户电商平台,是目前完善度领先的电商管理平台标准化产品,全新的产品模式很好地诠释电子商务在现今及未来的发展模式,强大的插件可插拔扩展制,让您在的行业电商所向披靡,系统拥有PC、手机WAP、微商城、买家APP、卖家APP、微信小程序,六端合一,六端互通,真正实现全网营销,极其适合企业及个人快速上线商务平台。 系统代码清晰易懂,大量的可视化报表便于运营者决策,丰富的营销功能让系统的应用场景广阔,良好的插件机制使系统更加易于扩展。系统操作简单,安全稳定,更新迭代快速,是广大用户直接使用和二次开发的最佳选择。 电商系统功能 (一) 系统管理:菜单权限、前台菜单、角色管理、职员管理、登录日志、操作日志、图片空间、商城消息、风格设置、计划任务 (二) 基础设置:商城配置、导航管理、广告管理、广告位置、银行管理、支付管理、地区管理、友情链接、快递管理、消息模板 (三) 会员管理:会员等级、会员管理、账号管理 (四) 文章管理:文章管理、文章分类 (五) 运营管理:推荐管理、商品推荐、店铺推荐、品牌推荐、财务管理、资金管理、提现申请、结算管理、商家结算、充值送、退款原路返回 (六) 订单管理:订单管理、投诉管理、退款管理 (七) 店铺管理:店铺认证、开店申请、店铺管理、停用店铺 (八) 商品管理:商品管理、已上架商品、待审核商品、违规商品、商品分类、商品属性、品牌管理、商品规格、评价管理 (九) 虚拟物品自动发货:虚拟商品上架、卡密管理、自动发货 (十) 扩展管理:插件管理、钩子管理 (十一) 分销管理:分销管理菜单、分销商家列表、分销商品列表、佣金分成列表、推广用户列表 (十二) 数据分析:商品销售排行、店铺销售统计、销售额统计、销售订单统计、新增会员统计、会员登录统计 (十三) 营销管理:三级分销,商品团购,限时拍卖、微砍价、优惠券、满减、满送、满包邮、签到送积分、积分商城,拼团 (十四) 微信管理:公众号设置、自定义菜单、用户管理、主动回复文本信息、主动回复图文信息、微信消息模板 (十五) 支付管理:支付支付微信支付、银联支付、货到付款、积分支付、余额支付 (十六) 短信接口:中国网建、阿里大于、阿里云-云通信、短信宝 (十七) 登录接口:QQ登录、微信登录、微博登录、支付宝登录 (十八) 物流接口:快递100接口 (十九) 其他接口:LBS、UCenter通信接口(可与社区论坛等同步登录退出)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值