微信扫码支付 php
仔细看了一遍官方的那幅流程图,我来简化理解一下
(注意:我这里针对的是扫码支付模式一,模式二没什么说的)
网站后台生成二维码,当然是跟据前台传来的参数有条件的生成
买家扫描二维码,扫描过程中,微信后台系统回调我们预先设定的url地址
(栗如:http://xx.com/a.php)
在回调php文件中设置商品价格,商品信息,支付结果异步通知地址(比如:http://xx.com/b.php)等,
然后发送给微信系统
微信系统根据我们发送的信息 返回一个预支付id给我们
我们将收到的数据原封不动返回去(这一步我真的无力吐槽了,完全是凭
感觉猜的,文档写的是返回预支付id,我试了只返回id提示错误)
此时买家扫描界面已经出现支付界面了,买家支付后,微信系统将把付款结果发送到上面我们设置的异步通知url上
然后我们就可以在异步url上完成我们自己的业务,比如会员充值成功,我们在数据库对应的添加金币
需要注意的是收到异步通知后一定要返回一个success回去,告诉微信系统我们已经收到通知,否则微信将多次发送异步通知
有时候由于网络原因可能你返回了成功但是也有可能收到多次通知,所以你在为会员添加金币的时候一定要根据订单号添加日志
在接收异步通知的时候首先去日志表看看订单号是否已经存
============================================================================
枯草的说明结束了 然后我们用代码来完整走一遍
首先我们来一个这样的前端页面,当然了,正式环境下怎么可能让用户来填写会员id呢,必须把id字段隐藏了,根据当前登陆的用户自动确定
代码如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>微信支付测试</title> 6 <script src="jquery.min.js" ></script> 7 <script src="jquery.qrcode.min.js" ></script> 8 </head> 9 <body> 10 <div> 11 金额:<input type="text" id="price"><br> 12 用户userid: <input type="text" id="userid" ><br> 13 <button onclick="getUrl()">支付</button> 14 <hr> 15 <div id="code"></div> 16 </div> 17 </body> 18 </html> 19 20 <script> 21 function getUrl() 22 { 23 var url = "http://xxxx.com/xxx/action.php"; 24 $.post(url,{'price':$("#price").val()*100,'userid':$("#userid").val()},function(re_url){ 25 createQR(re_url); 26 }) 27 } 28 function createQR(url) 29 { 30 $("#code").children().remove(); 31 $("#code").qrcode({ 32 // render: "table", 33 width: 200, //宽度 34 height:200, //高度 35 text: url 36 }); 37 } 38 </script>
当用户输入金额和会员id后,点击支付,然后用ajax去请求action.php这个文件,这个文件负责返回一个二维码的url地址
前台接到url后使用query.qrcode.min.js 这个库生成二维码,
下面看看action.php完整代码:
1 //接收两个参数 用addslashes过滤了一下数据 2 $p = addslashes($_POST['price']); //充值金额 3 $u = addslashes($_POST['userid']); //充值的用户(网站会员id) 4 5 $tmpArr = array( 6 'appid'=>'xxxxxxxxxxxxxxxxxx', //不要填成了 公众号原始id 7 'mch_id'=>'xxxxxxxxxx', 8 'nonce_str'=>'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 9 'time_stamp'=>time(), 10 'product_id'=>$p."s".$u, 11 ); 12 // 生成签名需要上面五个参数,文档上没有,不要问我是怎么知道的,我只知道二维码格式是这样的: 13 // weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX& 14 // product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX 15 // 还有就是看一下他的sdk也能看到他是怎么生成签名的,用了那些参数。 16 // 有好几个坑我都是通过看sdk明白的,所以你有必要看一下他的sdk 17 18 // 注意:上面五个参数是固定的 这个地方不可以自己加额外参数 否则报 原生url参数错误 19 // 但是如果不传参数我的业务逻辑怎么做呢 我这里用了一个比较巧妙的方法,没错,我们可以在产品编号product_id上 20 // 做文章,看我这里产品编号是 价格+当前网站用户userid组成的字符串,用 21 // 字符s分割,方便后面我们拆开,文档中说明了在用户扫描后只会返回openid和 22 // product_id给回调地址,再次证明你即便在上面新增额外参数也没有任何意义。我们完全可以 23 // 把需要的参数组装成字符串,然后用product_id来传递 24 25 ksort($tmpArr); //根据键值排序数组 26 // 把数组转换成这种格式:appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA 27 $buff = ""; 28 foreach ($tmpArr as $k => $v) 29 { 30 $buff .= $k . "=" . $v . "&"; 31 } 32 $buff = trim($buff, "&"); 33 // 这个地方有的人可能会想到 http_build_query 函数直接了当,干净利索 34 // 我刚开始就是用的这个函数,坑了老半天。。。 意外发现生成的字符串里面居然有几个字节的乱码 35 // 乍一看完全和上面生成的一样,各位可以尝试一下 36 // 这些步骤官方文档还是有的 不多说 37 $stringSignTemp=$buff."&key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 38 $sign= strtoupper(MD5($stringSignTemp)); 39 40 // 生成的二维码url 到这里就可以返回给前台 前端使用 jquery.qrcode.min.js 这个库可以生成二维码了 41 // 我试了一下 url太长 生成的二维码太复杂 像素差的手机就悲哀了,接着往下看 42 $reurl = "weixin://wxpay/bizpayurl?appid=xxxxxxxxxxxxxxxxxx&mch_id=xxxxxxxxxx&nonce_str=".$tmpArr['nonce_str']."& 43 product_id=".$tmpArr['product_id']."&time_stamp=".$tmpArr['time_stamp']."&sign=".$sign; 44 45 // 官方文档中介绍了有个长url转短url的API 写的还是很清楚的 没遇到坑 46 $posarr = array( 47 'appid'=>'xxxxxxxxxxxxxxxxxx', 48 'mch_id'=>'xxxxxxxxxx', 49 'nonce_str'=>'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 50 'long_url'=>urlencode($reurl) 51 // 这个地方文档中也说了 长url地址需要urlencode一下,不然你很可能得到签名错误 52 ); 53 ksort($posarr); 54 $buff = ""; 55 foreach ($posarr as $k => $v) 56 { 57 $buff .= $k . "=" . $v . "&"; 58 } 59 $buff = trim($buff, "&"); 60 61 $stringSignTemp=$buff."&key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 62 $sign= strtoupper(MD5($stringSignTemp)); 63 64 // 官方文档中说了 所有传输必须采用xml格式 post方式 https协议 65 66 $xml = "<xml> 67 <appid>xxxxxxxxxxxxxxxxxx</appid> 68 <mch_id>xxxxxxxxxx</mch_id> 69 <nonce_str>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</nonce_str> 70 <sign>".$sign."</sign> 71 <long_url>".$posarr['long_url']."</long_url> 72 </xml>"; 73 74 // 短连接请求地址 75 $posturl = "https://api.mch.weixin.qq.com/tools/shorturl"; 76 //下面使用curl来请求 77 $ch = curl_init($posturl); 78 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //返回文件流 79 curl_setopt($ch, CURLOPT_POST, 1); //使用post提交 80 curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); //post数据 81 $response = curl_exec($ch); 82 curl_close($ch); 83 84 // simplexml_load_string php内置的解析简单xml文件的扩展 85 $xmlobj = simplexml_load_string ($response, 'SimpleXMLElement', LIBXML_NOCDATA ); 86 // 这个地方我直接输出 $xml->short_url 居然是空的 非要经过下面几步才得行 难道是我php版本低了 87 $arr = array(); 88 foreach ($xmlobj as $key => $value) { 89 // file_put_contents("mylog.php", $value."\n",FILE_APPEND); 90 $arr[$key] = $value; 91 } 92 //这个链接就很短了 生成的二维码很简单 像素超低的手机都可以扫 93 echo $arr['short_url']; 94 95 // 最后,扫码支付只需要设置回调地址 ,至于支付授权目录 测试目录 白名单那些 都不用设置
各种解释各种坑已经在代码里面言尽了,请仔细看
然后用户可以拿起手机扫描二维码了,扫描过程中微信系统会发送一个xml数据到你预先设置的回调地址上
比如我这个地方就是http://xxx.com/a.php
具体这个a.php要做些什么呢,官方文档有说明,文章开头也说了这个回调地址的任务是什么,所以我们直接看代码:
1 // file_get_contents("php://input") 接收原始输入流 2 $postObj = simplexml_load_string (file_get_contents("php://input"), 'SimpleXMLElement', LIBXML_NOCDATA ); 3 4 $arr = array(); 5 foreach ($postObj as $key => $value) { 6 $arr[$key] = $value; 7 } 8 $pos = explode("s",$arr['product_id']); 9 $price = $pos[0]; 10 // $price = $pos[0]*0.01; 11 $userid = $pos[1]; 12 // 这些参数可以到文档去看看 有的参数是必填 有的是选填 13 $tmparr = array( 14 'appid'=>'xxxxxxxxxxxxxxxxxx', 15 'mch_id'=>'xxxxxxxxxx', 16 'nonce_str'=>'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 17 'body'=>'情缘网现金充值', 18 'detail'=>'关注情缘网公众号,充值享8折优惠', 19 'product_id'=>$price, 20 'trade_type'=>'NATIVE', 21 'out_trade_no'=>time()."s".$userid.'s'.$price, 22 'spbill_create_ip'=>'211.149.241.155', 23 'notify_url'=>'http://xxx.com/b.php', 24 'total_fee'=>$price 25 ); 26 // 注意这个地方我在订单号out_trade_no上做文章了,因为支付结果异步通知到另外一个url上, 27 // 而那个url上只能收到订单号而不是产品编号,所以我把运载在产品编号上的信息转移到了订单号上 28 // 我在异步url上才能处理我的业务 29 30 // 当然你在这个页面处理业务也是可以的,比如你现在就把订单号,价格,会员信息等插入数据库,在异步url上在根据订单号查询出本次 31 // 订单信息,然后操作,两种放啊都是可取的 32 // 我这里业务简单,直接把数据寄托在订单号上了,最好是在这个页面处理一部分逻辑,在异步通知后完善逻辑 33 34 ksort($tmparr); 35 $buff = ""; 36 foreach ($tmparr as $k => $v) 37 { 38 $buff .= $k . "=" . $v . "&"; 39 } 40 $buff = trim($buff, "&"); 41 42 $stringSignTemp=$buff."&key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 43 $sign= strtoupper(md5($stringSignTemp)); 44 $xml = " 45 <xml> 46 <appid>xxxxxxxxxxxxxxxxxx</appid> 47 <mch_id>xxxxxxxxxx</mch_id> 48 <nonce_str>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</nonce_str> 49 <sign>".$sign."</sign> 50 <body>商品标题</body> 51 <detail>商品描述</detail> 52 <product_id>".$price."</product_id> 53 <out_trade_no>".$tmparr['out_trade_no']."</out_trade_no> 54 <total_fee>".$price."</total_fee> 55 <spbill_create_ip>xxx.xxx.xxx.xxx</spbill_create_ip> 56 <notify_url>http://xxx.com/b.php</notify_url> 57 <trade_type>NATIVE</trade_type> 58 </xml> 59 "; 60 // 调用统一下单 接口 61 $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; 62 $ch = curl_init ($url); 63 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 64 curl_setopt($ch, CURLOPT_POST, 1); 65 curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); 66 $response = curl_exec($ch); 67 // if(curl_errno($ch)){ 68 // print curl_error($ch); 69 // } 70 curl_close($ch); 71 // 将接收到的数据直接返回,这一步官方没说明,大坑 72 echo $response;
有了第一个php文件的详细说明,相信这个文件中的代码各位可以自行理解了,这里代码简单,没有使用类对象这些东西,
全是过程,一步步往下读就ok了
回调这一步完成过后用户应该已经支付成功了,接下来微信系统还会将支付成功的喜讯发送到我们统一下单中设置的回调地址上
我这里就是http://xxx.com/b.php,直接看代码:
1 $postObj = simplexml_load_string (file_get_contents("php://input"), 'SimpleXMLElement', LIBXML_NOCDATA ); 2 $arr = array(); 3 foreach ($postObj as $key => $value) { 4 $arr[$key] = $value; 5 } 6 $status = $arr['result_code']; 7 $xml = " 8 <xml> 9 <return_code><![CDATA[".$status."]]></return_code> 10 <return_msg><![CDATA[OK]]></return_msg> 11 </xml>"; 12 echo $xml; 13 14 // 下面就根据$status来处理业务逻辑 15 16 //实例化数据库连接句柄 17 $mysqli = new mysqli("localhost", "username", "password", "dbname"); 18 19 $mysqli->query("set names 'utf8'"); 20 21 $danhao = $arr['out_trade_no']; //订单号 22 $sql = " select * from wxpaylog where orderid = '".$danhao."' "; 23 $res = $mysqli->query($sql); 24 //如果已经处理过这个订单号了 就直接忽视 25 if($res->num_rows > 0 ) 26 { 27 return false; die; 28 } 29 30 $pos = explode("s", $danhao); 31 $userid = $pos[1]; //用户id 32 $price = round($pos[2]*0.01); //充值金额 33 // $price = round($pos[2]*100); //充值金额 34 $zengsong = 0; //赠送额度 35 36 //充值金额 和送金币 活动规则 37 if($price<50) $zengsong = round($price*0.2); 38 if($price>=50 && $price <100) $zengsong = round($price*0.25); 39 if($price>=100 && $price <200) $zengsong = round($price*0.35); 40 if($price>200) $zengsong = round($price*0.5); 41 42 $sql = " select money from oepre_user where userid = '".$userid."'"; 43 44 $result = $mysqli->query($sql); 45 $row = $result->fetch_row(); 46 $gold_num = $row[0]; //当前的金币 47 if($status == "SUCCESS") 48 { 49 //要设置的金币数量 50 $now_count = (int)$gold_num+(int)$price+(int)$zengsong; 51 }else 52 { 53 $now_count = (int)$gold_num; 54 } 55 56 $sql = " update oepre_user set money = ".$now_count." where userid = '".$userid."'"; 57 $result = $mysqli->query($sql); 58 //添加记录 59 $sql = " insert into wxpaylog (userid,money,zengsong,atime,orderid,remark) 60 values(".$userid.",".$price.",".$zengsong.",'".date("Y-m-d H:i:s")."','".$danhao."','".$arr['result_code']."') "; 61 $result = $mysqli->query($sql);
我试了多次充值,异步通知全都是成功支付的,如果支付到一班取消,或者其他原因,异步通知是不会到的,所以这个地方我处理的比较粗暴,无论他
来的是成功或者失败,我都是直接返回去,然后把日志写到数据库,如果充值出了问题我在到数据库里面去检查,我们网站一年也没几个充值的,所以,恕我偷懒咯。
正常情况下如果收到的不是success,那么需要继续调用查询订单api,看看具体情况在继续做处理。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(四):结合BotSharp
· Vite CVE-2025-30208 安全漏洞
· 《HelloGitHub》第 108 期
· MQ 如何保证数据一致性?
· 一个基于 .NET 开源免费的异地组网和内网穿透工具