微信公众号开发功能:关注,取消关注,普通定位,jssdk定位,消息等都属于接收事件推送都在index方法中完成
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许。
推送和接收方式都以xml数据包的方式:
<xml><ToUserName>< ![CDATA[toUser] ]></ToUserName><FromUserName>< ![CDATA[FromUser] ]></FromUserName><CreateTime>123456789</CreateTime><MsgType>< ![CDATA[event] ]></MsgType><Event>< ![CDATA[subscribe] ]></Event></xml>
注意:微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。
关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。
服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。
xml包中包含多个属性值,均以双标签的方式编写:
<xml> <ToUserName>< ![开发者微信号]></ToUserName> <FromUserName>< ![发送方帐号(一个OpenID)]></FromUserName> <CreateTime>消息创建时间 (整型)</CreateTime> <MsgType>< ![消息类型,event ]></MsgType> <Event>< ![事件类型,subscribe(订阅)、unsubscribe(取消订阅) ]></Event> //扫描带参数二维码事件 <EventKey>< ![事件KEY值,qrscene_为前缀,后面为二维码的参数值 ]></EventKey> <Ticket>< ![二维码的ticket,可用来换取二维码图片] ]></Ticket> </xml>
下面我们通过一个函数和固定参数,来接收一下微信服务器发给我们的xml文件,看看到底是什么样子的
public function __construct(){ parent::__construct(); $this->model = model('Weixin'); } // 微信推送事件 public function index(){ $xmldata = file_get_contents("php://input"); file_put_contents('D://subscribe.txt',$xmldata); exit; }
手机微信关注一下开发微信公众号,查看 subscribe.txt 文件结果:
<xml> <ToUserName><![CDATA[gh_d4bfd8590800]]></ToUserName> //开发者微信号 <FromUserName><![CDATA[oJUC10z7f9ToE8FS-Kcd5GUhUQnE]]></FromUserName> //消息发送者帐号openid <CreateTime>1528247102</CreateTime> //消息创建时间 <MsgType><![CDATA[event]]></MsgType> //消息类型 : event <Event><![CDATA[subscribe]]></Event> //事件类型 : 关注/订阅 <EventKey><![CDATA[]]></EventKey> //事件key值 : 空 </xml>
当开发者拿到XML数据后,要对数据进行解析:
解析方法:simplexml_load_string 解析成一个obj对象 再用array强制转换成数组
public function index(){ $xmldata = '<xml><ToUserName><![CDATA[gh_d4bfd8590800]]></ToUserName> <FromUserName><![CDATA[oJUC10z7f9ToE8FS-Kcd5GUhUQnE]]></FromUserName> <CreateTime>1528247102</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[subscribe]]></Event> <EventKey><![CDATA[]]></EventKey> </xml>'; $postObj = simplexml_load_string($xmldata, 'SimpleXMLElement', LIBXML_NOCDATA); dump($postObj); echo '<hr color="red">'; $data = (array)$postObj; exit(dump($data)); }
得到xml数据后 我们开始对事件推送判断,做出相应的功能反应:
// 微信推送事件 public function index(){ // 校验数据来源 $valid = $this->model->valid(); if(!$valid){ exit('signature error'); } //获取xml数据包,并对xml数据包进行解析处理 $xmldata = file_get_contents("php://input"); $postObj = simplexml_load_string($xmldata, 'SimpleXMLElement', LIBXML_NOCDATA); $data = (array)$postObj; exit(dump($data)); // 事件推送 if(isset($data['MsgType']) && $data['MsgType'] == 'event'){ // 关注 if($data['Event'] == 'subscribe'){ $this->model->subscribe($data); } file_put_contents('D://unsubscribe.txt',$xmldata); // 取消关注 if($data['Event'] == 'unsubscribe'){ $this->model->unsubscribe($data); } // 定位 file_put_contents('D://location.txt',$xmldata); if($data['Event'] == 'LOCATION'){ $this->model->location($data); //file_put_contents('D://location.txt', var_export($data,true)); exit('success'); } }
由于事件推送涉及到数据库 weixin,下面我们创建一个数据库表:user
CREATE TABLE `user` ( `uid` int(10) NOT NULL AUTO_INCREMENT COMMENT '用户uid', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(32) NOT NULL COMMENT '密码', `nickname` varchar(50) NOT NULL COMMENT '昵称', `phone` varchar(11) NOT NULL COMMENT '手机号', `openid` varchar(255) NOT NULL COMMENT '微信openid', `avatar` varchar(255) NOT NULL COMMENT '头像', `sub_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '关注状态:0取消关注,1已关注', `lat` decimal(9,6) NOT NULL COMMENT '维度', `lng` decimal(9,6) NOT NULL COMMENT '经度', `add_time` int(10) NOT NULL COMMENT '添加时间', PRIMARY KEY (`uid`) ) ENGINE=MyISAM AUTO_INCREMENT=38 DEFAULT CHARSET=utf8;
配置好数据库连接:datebase.php
// 数据库类型 'type' => 'mysql', // 服务器地址 'hostname' => '127.0.0.1', // 数据库名 'database' => 'weixin', // 用户名 'username' => 'root', // 密码 'password' => 'root',
微信公众号开发:关注
用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。为保护用户数据隐私,开发者收到用户取消关注事件时需要删除该用户的所有信息。
在模型中封装成 subscribe 方法:
public function subscribe($data){ // 检查用户是否已存在,查询数据库 $user = Db::name('user')->where(array('openid'=>$data['FromUserName']))->find(); if(!$user){//如果不存在,插入用户信息数据 Db::name('user')->insertGetId(array('openid'=>$data['FromUserName'],'sub_status'=>1,'add_time'=>time())); }else{//如果存在,更新一下sub_status关注状态为1 Db::name('user')->where(array('openid'=>$data['FromUserName']))->update(array('sub_status'=>1)); } }
插入3项数据:【openid 关注者ID,sub_status,关注状态,add_time 新增时间】更新1项数据:【sub_status 关注状态】
在控制中判断事件类型,再调用模型中的subscribe方法:
if($data['Event'] == 'subscribe'){ $this->model->subscribe($data); } //file_put_contents('D://unsubscribe.txt',$xmldata); //保存查看一下结果
关注的处理步骤:
1) 查询数据库user表,根据openid值,检查用户是否存在
2) 如果不存在,插入用户信息数据
3) 如果存在,更新一下sub_status关注状态为1
微信公众号开发:取消关注 update(array('sub_status'=>0))
在模型中封装成 unsubscribe 方法:
// 取消关注 public function unsubscribe($data){ Db::name('user')->where(array('openid'=>$data['FromUserName']))->update(array('sub_status'=>0)); }
在控制中判断事件类型,再调用模型中的unsubscribe方法:
// 取消关注 if($data['Event'] == 'unsubscribe'){ $this->model->unsubscribe($data); }
微信公众号开发:普通定位
用户同意上报地理位置后,每次进入公众号会话时,都会在进入时上报地理位置,或在进入会话后每5秒上报一次地理位置,公众号可以在公众平台网站中修改以上设置。上报地理位置时,微信会将上报地理位置事件推送到开发者填写的URL。
选择-用户进行对话后每隔上报一次; 就是用户在关注的时候,就会频繁上报一次用户的地理位置,对数据库会产生压力
在模型中封装成 location 方法: 将用户的地理位置即 经度lng 纬度lat 更新到用户数据表中
// 定位 public function location($data){ Db::name('user')->where(array('openid'=>$data['FromUserName']))->update(array('lat'=>$data['Latitude'],'lng'=>$data['Longitude'])); }
当用户关注后,同意获取地理位置,就在控制中调用模型中的unsubscribe方法:
if($data['Event'] == 'LOCATION'){ $this->model->location($data); file_put_contents('D://location.txt', var_export($data,true)); exit('success'); }
微信公众号开发:js-sdk定位--需要的时候进行定位
JSSDK使用步骤:
一. 绑定域名:先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
二. 引入JS文件: http://res.wx.qq.com/open/js/jweixin-1.0.0.js
此时需创建一个视频图文件: ../index/view/weixin/location.php 目录要与项目名同名,文件名与控制器方法名同名
<!DOCTYPE html> <html> <head> <title></title> <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script> </head> <body> </body> </html>
注意: scr="//...." 相当于 scr="http//...." 和 scr="https//...." 支持两种协议
三. 通过CONFIG接口注入权限验证配置
写到视频图文件: ../index/view/weixin/location.html 中 注意:wx.config 代表只能在微信客户端打开
<!DOCTYPE html> <html> <head> <title></title> <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script> </head> <body> </body> </html> <script type="text/javascript"> wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '{$appid}', // 必填,公众号的唯一标识 timestamp: {$timestamp}, // 必填,生成签名的时间戳 nonceStr: '{$nonceStr}', // 必填,生成签名的随机串 signature: '{$signature}',// 必填,签名 jsApiList: ['getLocation','openLocation'] // 必填,需要使用的JS接口列表 }); </script>
在按制器中用 return $this->fetc()方法传值到视图中:
public function location(){ $data['appid'] = config('app.appid'); //配置ID $data['timestamp'] = time(); // 当前时间戳 $data['nonceStr'] = md5(time().rand(1,999)); //时间戳+随机生成的三位数,再用sha1加密生成随机串 $data['signatur'] = 签名 return $this->fetch('',$data); //传值方法,''不填表示文件名与方法一样 location }
signature: '',生成签名的过程比较复杂,下面分三步完成这个过程:
1、获取jsapi_ticket:jsapi_ticket是公众号用于调用微信JS接口的临时票据。
A. 正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。通过写好的access_token方法拿到签名
$access_token = $this->model->access_token();
B. 由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket 。
先在模型中写个获取jsapi_ticket的方法:
先获取到ticket
public function jsapi_ticket($access_token,$iscache = true){ $appid = config('app.appid'); $appsecret = config('app.appsecret'); $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token='.$access_token.'&type=jsapi'; $res = http_get($url); $res = json_decode($res,true); //解析成数组 dump($res); }
打印出来的结果,很明显已经获取到了ticket,获得jsapi_ticket之后,就可以生成JS-SDK权限验证的签名了。
获得jsapi_ticket之后,还要做两项判断:
A. 判断是否需要有缓存ticket,如果没有,就删除重拿,如果有,直接从缓存拿
B. 判断有没有获得ticket,获取就缓存起来,没拿到就要重新拿
// 获取jsapi_ticket public function jsapi_ticket($access_token,$iscache = true){ //判断是否有ticket缓存数据 $key = 'jsapi_ticket'; if(!$iscache){ Cache::rm($key); } $data = Cache::get($key); if($data && $iscache){ return $data; } //获取ticket过程 $appid = config('app.appid'); $appsecret = config('app.appsecret'); $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token='.$access_token.'&type=jsapi'; $res = http_get($url); $res = json_decode($res,true); //判断是否获取到ticket,获取就缓存起来 if(!isset($res['ticket'])){ return false; } Cache::set($key,$res['ticket'],($res['expires_in']-100)); return $res['ticket']; }
再在控制器中调用获取jsapi_ticket的方法:
//1、获取jsapi_ticket $access_token = $this->model->access_token(); $jsapi_ticket = $this->model->jsapi_ticket($access_token);
2、做签名算法:构造签名字符串
签名生成规则如下:
参与签名的字段包括:
a. noncestr(随机字符串) $params['noncestr'] = $data['nonceStr'];
b. 有效的jsapi_ticket, timestamp(时间戳) $params['jsapi_ticket'] = $jsapi_ticket;
c. url $params['url'] = 'http://11e16.cn/index.php/index/weixin/location';
这里需要注意url不对可能会导致 整个签名错误,可以用以下代码进行查阅和对比:alert(location.href.split('#')[0]);
<script type="text/javascript"> wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '{$appid}', // 必填,公众号的唯一标识 timestamp: {$timestamp}, // 必填,生成签名的时间戳 nonceStr: '{$nonceStr}', // 必填,生成签名的随机串 signature: '{$signature}',// 必填,签名 jsApiList: ['getLocation','openLocation'] // 必填,需要使用的JS接口列表 }); alert(location.href.split('#')[0]); </script>
对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
排序函数方法:http_build_query($params)【注意:这里是对数据的键key进行排序】
echo http_build_query($params); 打印输出排序后的测试效果
这里需要注意的是所有参数名均为小写字符,字段名和字段值都采用原始值,不进行URL 转义。
$params['noncestr'] = $data['nonceStr']; $params['jsapi_ticket'] = $jsapi_ticket; $params['timestamp'] = $data['timestamp']; $params['url'] = 'http://11e16.cn/index.php/index/weixin/location.html'; ksort($params); $str = urldecode(http_build_query($params)); // 防止url字符转义
3. 生成签名,对签名进行sha1加密,得到signature:传到视图模版中
$data['signature'] = sha1($str); return $this->fetch('',$data);
加载js接口: jsApiList: ['getLocation','openLocation'] 接口文件写到视图模版的JS代码下
<script type="text/javascript"> wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '{$appid}', // 必填,公众号的唯一标识 timestamp: {$timestamp}, // 必填,生成签名的时间戳 nonceStr: '{$nonceStr}', // 必填,生成签名的随机串 signature: '{$signature}',// 必填,签名 jsApiList: ['getLocation','openLocation'] // 必填,需要使用的JS接口列表 }); wx.ready(function(){ getLocation(openLocation); }); // 获取地理位置接口 function getLocation(callback){ wx.getLocation({ type: 'gcj02', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02' success: function (res) { var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90 var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。 var speed = res.speed; // 速度,以米/每秒计 var accuracy = res.accuracy; // 位置精度 if(callback!=undefined){ callback(res); } } }); } // 使用微信自带地图显示位置 function openLocation(res){ wx.openLocation({ latitude: res.latitude, // 纬度,浮点数,范围为90 ~ -90 longitude: res.longitude, // 经度,浮点数,范围为180 ~ -180。 name: '', // 位置名 address: '', // 地址详情说明 scale: 15, // 地图缩放级别,整形值,范围从1~28。默认为最大 infoUrl: '' // 在查看位置界面底部显示的超链接,可点击跳转 }); } </script>
此处有提示语: 可以关闭程序中的调试模式:wx.config({ debug: true, 修改成 debug: false,
四. 通过READY接口处理成功验证
wx.ready(function(){ // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 });
五. 通过error接口处理失败验证
wx.error(function(res){ // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 });
总结:关注和取消关注,相对来说比较简单,主要涉及到数据库的查询,插入,更新操作,通过openid查询用户信息,对sub_status的值进行判断。
JSSDK定位,重点在通过access_token获取jsapi_ticket并生成签名,生成过程拼接,转义,加密,与微信服务器进行对比,对比成功开始相关接口的对接。。