微信公众号支付开发细节
开发背景
微信长按二维码识别支付的功能被取消了,所以项目改为使用公众号支付。默认读者已经获得 微信公众号的开发权限和在微信开通了商户号
开发流程
一、微信支付DEMO功能简要介绍
微信平台上有支付的DEMO,我們可以下載下來直接使用。下面我們來分析一下DEMO中主要文件的作用。
example/jsapi.php主要是微信公众号支付
example/native.php主要是微信扫码支付,其中包括模式一和模式二。(模式一:先扫码,再生成订单。模式二:先生成订单,再扫码)
example/notify.php主要是用来处理回调信息的。为什么要处理回调信息呢?我们可以这样的思考:当我们支付成功之后,微信肯定要告诉我们支付是否 成功。那么微信要告诉谁呢?要告诉那个函数呢?这个就需要我们自己来设置了,我们给微信一个回调地址,那么微信就会把支付结果信息发送到这个地址上去。光 接收到信息是不够的,我们呢,还需要函数来处理这些信息。notify.php里面就包括了处理函数。
二、微信公众号支付及其流程
我先从头到尾的按照线性顺序来屡一下流程吧。具体的可以看下图:
要用微信公众号支付,首先我们要做的就是调用jsapi.php。
jsapi.php具体内容如下:一些地方添加有注释帮助大家理解
<?php ini_set('date.timezone','Asia/Shanghai'); //error_reporting(E_ERROR);
//这个是引用所需的文件
require_once "../lib/WxPay.Api.php"; require_once "WxPay.JsApiPay.php"; require_once 'log.php'; //初始化日志 $logHandler= new CLogFileHandler("../logs/".date('Y-m-d').'.log'); $log = Log::Init($logHandler, 15); //打印输出数组信息 function printf_info($data) { foreach($data as $key=>$value){ echo "<font color='#00ff55;'>$key</font> : $value <br/>"; } } //①、获取用户openid $tools = new JsApiPay(); $openId = $tools->GetOpenid(); //②、统一下单 $input = new WxPayUnifiedOrder(); $input->SetBody("test");//一般填写用户的订单号 $input->SetAttach("test");//一般填写用户的订单号 $input->SetOut_trade_no(WxPayConfig::MCHID.date("YmdHis"));//填写用户的订单号 $input->SetTotal_fee("1");//支付费用 $input->SetTime_start(date("YmdHis")); $input->SetTime_expire(date("YmdHis", time() + 600)); $input->SetGoods_tag("test"); $input->SetNotify_url("http://paysdk.weixin.qq.com/example/notify.php");//刚才讲的回调函数所在的地址,要自己设定 $input->SetTrade_type("JSAPI"); $input->SetOpenid($openId); // $order = WxPayApi::unifiedOrder($input); echo '<font color="#f00"><b>统一下单支付单信息</b></font><br/>'; printf_info($order);//打印订单的详细信息 $jsApiParameters = $tools->GetJsApiParameters($order); //获取共享收货地址js函数参数 $editAddress = $tools->GetEditAddressParameters(); //③、在支持成功回调通知中处理成功之后的事宜,见 notify.php /** * 注意: * 1、当你的回调地址不可访问的时候,回调通知会失败,可以通过查询订单来确认支付是否成功 * 2、jsapi支付时需要填入用户openid,WxPay.JsApiPay.php中有获取openid流程 (文档可以参考微信公众平台“网页授权接口”, * 参考http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html) */ ?> <html> <head> <meta http-equiv="content-type" content="text/html;charset=utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>微信支付样例-支付</title> <script type="text/javascript"> //调用微信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); } ); }
//点击立即支付的时候执行的函数 function callpay() { if (typeof WeixinJSBridge == "undefined"){ if( document.addEventListener ){ document.addEventListener('WeixinJSBridgeReady', jsApiCall, false); }else if (document.attachEvent){ document.attachEvent('WeixinJSBridgeReady', jsApiCall); document.attachEvent('onWeixinJSBridgeReady', jsApiCall); } }else{ jsApiCall(); } } </script> <script type="text/javascript"> //获取共享地址 function editAddress() { WeixinJSBridge.invoke( 'editAddress', <?php echo $editAddress; ?>, function(res){ var value1 = res.proviceFirstStageName; var value2 = res.addressCitySecondStageName; var value3 = res.addressCountiesThirdStageName; var value4 = res.addressDetailInfo; var tel = res.telNumber; alert(value1 + value2 + value3 + value4 + ":" + tel); } ); } //这段代码的功能是在支付之前,弹出一个地址填写框 window.onload = function(){ if (typeof WeixinJSBridge == "undefined"){ if( document.addEventListener ){ document.addEventListener('WeixinJSBridgeReady', editAddress, false); }else if (document.attachEvent){ document.attachEvent('WeixinJSBridgeReady', editAddress); document.attachEvent('onWeixinJSBridgeReady', editAddress); } }else{ editAddress(); } }; </script> </head> <body> <br/> <font color="#9ACD32"><b>该笔订单支付金额为<span style="color:#f00;font-size:50px">1分</span>钱</b></font><br/><br/> <div align="center"> <button style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer; color:white; font-size:16px;" type="button" onclick="callpay()" >立即支付</button> </div> </body> </html>
我们先讲一下获取用户openid这段代码$openId = $tools->GetOpenid();吧。
//①、获取用户openid
$tools = new JsApiPay();
$openId = $tools->GetOpenid();
JsApiPay是Wxpay.JsApipay.php中的一个类,$tools = new JsApiPay();这个新建了一个JsApiPay类,然后执行GetOpenid()这个函数。这个函数代码在下面:
/** * * 通过跳转获取用户的openid,跳转流程如下: * 1、设置自己需要调回的url及其其他参数,跳转到微信服务器https://open.weixin.qq.com/connect/oauth2/authorize * 2、微信服务处理完成之后会跳转回用户redirect_uri地址,此时会带上一些参数,如:code * * @return 用户的openid */ public function GetOpenid() { //通过code获得openid if (!isset($_GET['code'])){ //触发微信返回code码 $baseUrl = urlencode('http://'.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].$_SERVER['QUERY_STRING']); $url = $this->__CreateOauthUrlForCode($baseUrl); Header("Location: $url"); exit(); } else { //获取code码,以获取openid $code = $_GET['code']; $openid = $this->getOpenidFromMp($code); return $openid; } }
isset用来判断是不是存在$_GET['code']这个变量,如果不在的话,就执行了if语句,如果存在,就执行了else语句。
当我们第一次调用GetOpenid()的时候当然不存在$_GET['code']这个变量,所以就执行了if语句。接下来我们看看if语句的内容:
$baseUrl是用来获取你当前的url,为什么要获取当前的url呢?我们先看这一段代码$url = $this->__CreateOauthUrlForCode($baseUrl);它调用了一个函数,函数的具体内容如下:
private function __CreateOauthUrlForCode($redirectUrl) { $urlObj["appid"] = WxPayConfig::APPID; $urlObj["redirect_uri"] = $redirectUrl; $urlObj["response_type"] = "code"; $urlObj["scope"] = "snsapi_base"; $urlObj["state"] = "STATE"."#wechat_redirect"; $bizString = $this->ToUrlParams($urlObj); return "https://open.weixin.qq.com/connect/oauth2/authorize?".$bizString; }
我们可以看到有设置一些变量,比如appid、response_type、scope、state等。appid就是你配置的APPID,$urlObj["response_type"]='code'也就是说它会返回一些值,这些值的类型是code类型,看到这里有没有发现一些奇怪的地方?对,刚才有个if语句判断是否存在$_GET['code']这个变量,而现在它给我们返回了code类型的数据。是不是巧合呢?当然不是巧合,别忘了还有一个$urlObj["redirect_uri"] = $redirectUrl;是把我们刚才获取的当前的url作为一个变量传递过去。传到哪里?传到https://api.weixin.qq.com/sns/oauth2/access_token这个地方,然后呢,微信会进行验证,如果你给它的数据没错的话,那么它会返回相关的数据给这个变量$urlObj["redirect_uri"]。
这样就又跳到了下面这个函数的地方,不过不同的是这次我们有了code,于是我们就通过$openid = $this->getOpenidFromMp($code);这个函数获得到了openID.
public function GetOpenid() { //通过code获得openid if (!isset($_GET['code'])){ //触发微信返回code码 $baseUrl = urlencode('http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'].$_SERVER['QUERY_STRING']); $url = $this->__CreateOauthUrlForCode($baseUrl); Header("Location: $url"); exit(); } else { //获取code码,以获取openid $code = $_GET['code']; $openid = $this->getOpenidFromMp($code); return $openid; } }
获取openID之后,如果前面的配置信息没有什么错误的话,那么我们就会弹出这个页面了。
这就说明我们做的成功了。
其中有一点要说明一下,因为它在获取openID的时候,来来回回的跳了好多次,所以$baseUrl 里面的一些参数变量会丢失,所以建议大家自己写url,同时也可传递一些参数。这个问题大家要格外的注意一下。
接着我们再谈谈回调函数的问题:以下是处理回调的函数代码也就是notify.php
<?php ini_set('date.timezone','Asia/Shanghai'); error_reporting(E_ERROR);
require_once "../lib/WxPay.Api.php"; require_once '../lib/WxPay.Notify.php'; require_once 'log.php'; //初始化日志 $logHandler= new CLogFileHandler("../logs/".date('Y-m-d').'.log'); $log = Log::Init($logHandler, 15); class PayNotifyCallBack extends WxPayNotify { //查询订单 public function Queryorder($transaction_id) { $input = new WxPayOrderQuery(); $input->SetTransaction_id($transaction_id); $result = WxPayApi::orderQuery($input); Log::DEBUG("query:" . json_encode($result)); if(array_key_exists("return_code", $result) && array_key_exists("result_code", $result) && $result["return_code"] == "SUCCESS" && $result["result_code"] == "SUCCESS") { return true; } return false; } //重写回调处理函数 public function NotifyProcess($data, &$msg) { Log::DEBUG("call back:" . json_encode($data)); $notfiyOutput = array(); if(!array_key_exists("transaction_id", $data)){ $msg = "输入参数不正确"; return false; } //查询订单,判断订单真实性 if(!$this->Queryorder($data["transaction_id"])){ $msg = "订单查询失败"; return false; } return true; } } Log::DEBUG("begin notify"); $notify = new PayNotifyCallBack(); $notify->Handle(false);
我们直接看$notify->Handle(false);这个是回调函数的入口。但是我们在这段代码中并没有找到handle()函数,这是为什么呢?因为PayNotifyCallBack类继承了WxPayNotify类,所以我们到lib/WxPay.Notify.php中WxPayNotify中去找就可以了。
其中代码如下:
<?php /** * * 回调基础类 * @author widyhu * */ class WxPayNotify extends WxPayNotifyReply { /** * * 回调入口 * @param bool $needSign 是否需要签名输出 */ final public function Handle($needSign = true) { $msg = "OK"; //当返回false的时候,表示notify中调用NotifyCallBack回调失败获取签名校验失败,此时直接回复失败 $result = WxpayApi::notify(array($this, 'NotifyCallBack'), $msg); if($result == false){ $this->SetReturn_code("FAIL"); $this->SetReturn_msg($msg); $this->ReplyNotify(false); return; } else { //该分支在成功回调到NotifyCallBack方法,处理完成之后流程 $this->SetReturn_code("SUCCESS"); $this->SetReturn_msg("OK"); } $this->ReplyNotify($needSign); } /** * * 回调方法入口,子类可重写该方法 * 注意: * 1、微信回调超时时间为2s,建议用户使用异步处理流程,确认成功之后立刻回复微信服务器 * 2、微信服务器在调用失败或者接到回包为非确认包的时候,会发起重试,需确保你的回调是可以重入 * @param array $data 回调解释出的参数 * @param string $msg 如果回调处理失败,可以将错误信息输出到该方法 * @return true回调出来完成不需要继续回调,false回调处理未完成需要继续回调 */ public function NotifyProcess($data, &$msg) { //TODO 用户基础该类之后需要重写该方法,成功的时候返回true,失败返回false return true; } /** * * notify回调方法,该方法中需要赋值需要输出的参数,不可重写 * @param array $data * @return true回调出来完成不需要继续回调,false回调处理未完成需要继续回调 */ final public function NotifyCallBack($data) { $msg = "OK"; $result = $this->NotifyProcess($data, $msg); if($result == true){ $this->SetReturn_code("SUCCESS"); $this->SetReturn_msg("OK"); } else { $this->SetReturn_code("FAIL"); $this->SetReturn_msg($msg); } return $result; } /** * * 回复通知 * @param bool $needSign 是否需要签名输出 */ final private function ReplyNotify($needSign = true) { //如果需要签名 if($needSign == true && $this->GetReturn_code($return_code) == "SUCCESS") { $this->SetSign(); } WxpayApi::replyNotify($this->ToXml()); } }
可以看到$result = WxpayApi::notify(array($this, 'NotifyCallBack'), $msg);这句话是最关键的,它会调用NotifyCallBack函数,然后NotifyCallBack函数会调用$result = $this->NotifyProcess($data, $msg);函数,其中public function NotifyProcess($data, &$msg);默认是返回true的,我们可以在这个函数里写自己要操作的逻辑,比如把订单插入到数据库中等操作。到此,就完成了整个流程
那$data这个数据是什么呢?
{
"appid":"",
"attach":"",
"bank_type":"",
"cash_fee":"1",
"fee_type":"CNY",
"is_subscribe":"Y",
"mch_id":"",
"nonce_str":"",
"openid":"",
"out_trade_no":"",
"result_code":"SUCCESS",
"return_code":"",
"sign":"",
"time_end":"20170409184335",
"total_fee":"1",
"trade_type":"JSAPI",
"transaction_id":""}
那就到此结束啦!!
2017.4.18日补充:
我们同时需要在这个地方填写支付授权目录,如果支付授权目录填写错了的话,会报错redirect_uri,具体过程请参考下面的链接文档:
http://www.cnqn.com/archives/180482.html
http://www.thinkphp.cn/code/1620.html