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

一、场景介绍

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



二、开发步骤

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



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



概括一下:

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

四、项目总结

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

 

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信