实习期间,参与了微信公众号开发,接触到了微信公众号支付,在开发过程中踩了不少的坑,好在顺利完成了任务,在这里,我觉得有必要和大家分享一下,也便于自己以后参考。
一、场景介绍
用户通过微信公众号打开网页, 进入到如下界面,选择或输入相应金额为网站中用户账号充值。
二、开发步骤
这里就不再介绍商户如何接入微信支付,官方文档中已经有了详细介绍,具体请参考微信公众号支付官方文档。直接上业务流程。首先我们来看看微信官方给我们的流程图:
刚开始接触,看到这个流程图可能会有点懵逼,没关系,这里我给大家画了一个简单的流程图:
概括一下:
1)获取用户授权(当用户进入到充值界面的时候)
2)后台调用统一下单接口获取预支付订单,并返回给前端
3)前端H5调起微信支付(调起成功后,会提示输入支付密码),
4)微信向前端和回调地址发送支付结果通知(两者时序不分前后,以回调地址通知为准)
5)回调处理(很重要,后面详细介绍)
好了,相信你已经对微信公众号支付的整个流程有了一定的了解,那我们就开始编码吧。
三、代码实现
1、前端表单提交充值金额,核心代码如下:
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控制器里专门处理微信支付。首先引入必要文件
至此,我们就可以开始进行微信公众号支付后台开发了。前面说到了前端发送充值金额到后台(之前你要获取到了用户授权,得到用户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',
,
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 = " ";
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;
}
四、项目总结
至此,我们整个微信公众号支付的开发流程就结束了,希望这篇博文能对大家有所帮助。以上微信公众号支付处理过程是根据自己的理解归纳总结的,不足之处,欢迎大家指正。