模版消息推送是微信小程序采用的通知形式, 用户本人在小程序页面有交互行为后,可触发下发通知 ,经过微信聊天列表中的服务通知可快捷进入查看消息。此外,点击查看详情还能跳转到下发消息的小程序的指定页面。可是为了不这种通知被滥用,带来很差的用户体验,小程序也对模板消息推送作了相应的限制。为了更好的优化小打卡小程序的打卡通知功能,我在开发的过程当中自行摸索了一套突破推送限制的解决方案。能够实现 7天内向用户推送多条模板消息,甚至向用户群发消息的功能 。html
消息通知是一个很重要的功能,如QQ空间的回复状态通知,QQ邮箱的邮件通知,微信支付成功提通知等。这种常规的 服务跟踪类 消息,便于用户掌握产品对自身服务的进度,方便客户获取必要的信息,提升效率;保证用户的知情权,让用户有安全感。同时,对于产品自己来讲,能够引导用户进行下一步行为, 增长了产品的曝光率,便于用户留存,加强用户粘性。前端
服务通知及模板消息redis
如上图,呈如今微信聊天列表的 服务通知 ,收纳了各个小程序向用户推送模板消息,这个服务通知是用户查看模板消息的入口,用户点击服务通知后能够查看到通知列表页面,每条通知以卡片的形式呈现,包括小程序的logo、名称、通知时间、通知内容等信息。数据库
所谓『模板消息』,就如上面的通知卡片,首先通知卡片形式样子是固定的,其实卡片中的通知内容部分,能够看到天天通知的内容都具有日程描述、日程主题、日程时间等要素,通知之间不一样的地方在于这些要素后面的文案,将这些通知要素制做成模板,每次针对不一样的通知内容 只须要填充每条要素对应的具体的文本 便可推送给用户。上面图中两条模板消息的日程主题和时间不同,其余的信息要素保持一致,这就是模板消息。json
提到模板消息的好处,第一印象是 "多、快、好、省" 的特色。小程序
"快"即快捷,体如今微信用户侧的通知体验,因为在微信客户端服务通知在聊天列表中,保留了用户以往处理聊天通知的习惯,因此用户能够很 便捷地触及服务通知 ,查看小程序推送的模板消息。vim
"好"即效果好,小程序的模板消息具有 跳转直达小程序特定页面 的能力,这样用户接收消息后,查看消息的通知就能便捷地回到小程序进行相应的业务处理、信息查看等后续操做,必定程度上提高了用户的活跃度,小打卡小程序的近30天访问来源数据显示,有20%左右的用户经过模板消息这个入口进入小打卡,在各类来源中排名第三位,能够见模板消息是用户使用你的小程序的重要入口。后端
"省"即省钱呗,有了模板推送,天然 下降了消息通知的成本 ,节省费用。消息通知优先经过模板消息这种方式来推送给指定用户,只有才没法触及用户的状况下,才使用传统的付费短信推送等形式。微信小程序
"多"呢?上面提到"没法触及用户的状况",实际上是由于小程序不具有"多"的特色。物以稀为贵,模板消息虽好,可是微信小程序官方为了保证用户体验, 平衡通知和骚扰行为 ,对模板推送作了相应限制。接下来就聊聊这个限制。api
微信小程序容许下发模板消息的条件分为两类, 支付或者提交表单 。
目前支付的限制有所放开,即1次支付能够下发3条模板消息。经过提交表单来下发模板消息的方式限制为一次的触发行为,7天内能够向用户推送一条模板消息。 这种消息的控制放的太宽的话,很容易对用户的体验形成很大冲击,给用户带来必定的骚扰。
可是,用户1次触发、7天内推送1条通知明显是不够用的,好比小打卡小程序利用模板消息的推送来提醒用户天天打卡,只能在用户前一天打卡的状况下,获取一次推送模板消息的机会,而后用于次日向用户发送打卡通知。可是不少状况下,用户若是某一天忘记打卡,小打卡便 失去了提醒用户的权限,和用户断开了联系 。
在小打卡中还有一个迫切须要多条模板消息推送的场景,好比打卡活动每次有新的成员进入,须要通知管理员进行审核,这种状况也须要及时地通知管理员,以便管理员快速响应,处理成员的审核请求并通知成员审核结果。
注意到下发条件中,每次触发的到的 推送码能够在将来7天内使用,屡次提交触发下发的消息条数独立,相互不影响 ,那能不能突破模板消息的发送限制,更好地优化打卡提醒功能呢?
微信小程序官方最近已经透露出可能对模板消息进一步放宽限制的信号,不过在这以前,咱们能够在遵照官方相关运营规范、保证用户体验的状况下,倒腾一个 "让用户一次触发、屡次推送,甚至群发模板消息" 的解决方案。
其实仔细分析消息下发条件"1次提交表单可下发1条,屡次提交下发条数独立,相互不影响",突破口就明显了,只需 收集到足够推送码 ,即每次提交表单时获取到的formId就是咱们所需的“推送权限”。它是一次性的,表明着开发者有向当前用户推送模板消息的权限。
为了打造这样一个突破限制的模版消息推送功能,作到7天内任性推送,咱们将小程序先后端的工做明确一下,小程序前端,即运行在用户微信上的小程序负责 收集推送码 ,小程序后端,即运行在服务器上的应用程序负责将推送码 存储到数据库 中,并在须要推送的模版消息的时候从中取出推送码formId判断有效性并加以运用。整个方案的先后端业务流程以下:
方案先后端流程
接下来咱们设计一个可以突破当前模板消息推送限制的方案。结合 小程序前端界面、小程序逻辑层、服务器程序、数据库、异步任务系统 各自分工,来实现将小程序模板消息推送所需的推送码收集、上报、存储、调用。最终作到7日内更好地推送模板消息、触及用户。
每次表单提交能够触发一次下发模版消息的机会,表单组件
以下:
Page({ formSubmit: function(e) { let formId = event.detail.formId; console.log('form发生了submit事件,推送码为:', formId) } })
组件中属性report-submit为true时,表明须要请求发模板消息的推送码,此时点击按钮提交表单能够获取formId,用于发送模板消息。接下来只须要对原来的页面进行改造,将用户原来绑定了点击事件的界面用表单组件中的button按钮组件来代替,也就是 把用户的交互点击的bindtap事件经过表单bindsubmit来取代 ,从而捕获用户的点击事件来产生更多的推送码formId,这里还须要对按钮组件的样式进行稍微的修改,以便更好地包裹原来界面的代码。
/*wxss*/ /*修改按钮样式,使其可以包裹其余组件*/ .btn { border:none; text-align:left; padding:0; margin:0; line-height:1.5; }
//js Page({ formSubmit: function(e) { let formId = e.detail.formId; this.dealFormIds(formId); //处理保存推送码 let type = e.detail.target.dataset.type; //根据type的值来执行相应的点击事件 //... }, dealFormIds: function(formId) { let formIds = app.globalData.gloabalFomIds;//获取全局数据中的推送码gloabalFomIds数组 if (!formIds) formIds = []; let data = { formId: formId, expire: parseInt(new Date().getTime() / 1000)+604800 //计算7天后的过时时间时间戳 } formIds.push(data);//将data添加到数组的末尾 app.globalData.gloabalFomIds = formIds; //保存推送码并赋值给全局变量 }, })
上面的代码主要实现了模拟表单提交事件来取代原来的点击事件,用户在点击界面进行交互的同时,可以得到多个推送码保存app.js的全局变量globalData中,等待用户下一次发起网络请求时,便可将gloabalFomIds数组数据发送给服务器。
小打卡上的点击区域
上图以小打卡的打卡详情页为例,用户在这个页面的点击操做能够很快收集到多个formId,因此将界面上用户高频点击的事件用表单的形式从新封装后,能够静默、快速收集到所需的"模板消息推送权限" 。
Page({ onLoad:function(){ this. saveFormIds(); }, saveFormIds: function(){ var formIds = app.globalData.gloabalFomIds; // 获取gloabalFomIds if (formIds.length) {//gloabalFomIds存在的状况下 将数组转换为JSON字符串 formIds = JSON.stringify(formIds); app.globalData.gloabalFomIds = ''; } wx.request({//经过网络请求发送openId和formIds到服务器 url: 'https://www.x.com', method: 'GET', data: { openId: 'openId', formIds: formIds }, success: function(res) { } }); }, })
在小程序的逻辑层中,经过全局变量gloabalFomIds收集到多个formId后,能够在新页面载入时,在onLoad生命周期函数中发送网络请求获取数据, gloabalFomIds不为空时,把gloabalFomIds数组格式化为字符串发送到服务器,并清空当前的gloabalFomIds ,以便继续获取新的formId。
由于这个保存是一个高频IO的操做,咱们 后端以PHP结合高性能的key-value数据库Redis来实现推送码的存储 。相关关键代码以下,简单表达了思路,针对不一样的后端环境和开发语言,你可能须要作相应的调整。
//关键代码 public function saveFormIds(){ $openId = $_GET['openId']; $formIds = $_GET['formIds'];;//获取formIds数组 if($formIds){ $formIds = json_decode($formIds,TRUE);//JSON解码为数组 $this -> _saveFormIdsArray($openId,$formIds);//保存 } } private function _get($openId){ $cacheKey = md5('user_formId'.$openId); $data = $this->cache->redis->get($cacheKey);//修改成你本身的Redis调用方式 if($data)return json_decode($data,TRUE); else return FALSE; } private function _save($openId,$data){ $cacheKey = md5('user_formId'.$openId); return $this->cache->redis->save($cacheKey,json_encode($data),60*60*24*7);//修改成你本身的Redis调用方式 } private function _saveFormIdsArray($openId,$arr){ $res = $this->_get($openId); if($res){ $new = array_merge($res, $arr);//合并数组 return $this->_save($openId,$new); }else{ $result = $arr; return $this->_save($openId,$result); } }
这一步主要是构建服务器程序高效存储用户的推送码formId,这下推送机会有了,接下来咱们考虑如何 利用后端程序来想特定用户发送模板消息 ,考虑怎样去合理运用推送机会。
构建高性能的服务器端异步任务推送,能够知足 模板消息的群发、以及定时发送 的需求,如小打卡就采用了高性能分布式内存队列系统 BEANSTALKD,来实现模板消息的异步定时推送。实现发送模板消息的群发、定时发送分为2个步骤:
普通的模板消息的发送就不赘述了,可参考 官方文档中的模板消息功能 一步步进行操做,咱们重点来看高性能异步任务推送的实现方法。涉及到的关键代码以下:
//设置异步任务 public function put_task($data,$priority=2,$delay=3,$ttr=60){//任务数据、优先级、时间定时、任务处理时间 $pheanstalk = new Pheanstalk('127.0.0.1:11300'); return $pheanstalk ->useTube('test') ->put($data,$priority,$delay,$ttr); } //执行异步任务 public function run() { while(1) { $job = $this->pheanstalk->watch('test')->ignore('default')->reserve();//监放任务 $this->send_notice_by_key($job->getData());//执行模板消息的发送 $this->pheanstalk->delete($job);//删除任务 $memory = memory_get_usage(); usleep(10); } } //1.取出一个可用的用户openId对应的推送码 public function getFormId($openId){ $res = $this->_get($openId); if($res){ if(!count($res)){ return FALSE; } $newData = array(); $result = FALSE; for($i = 0;$i < count($res);$i++){ if($res[$i]['expire'] > time()){ $result = $res[$i]['formId'];//获得一个可用的formId for($j = $i+1;$j < count($res);$j++){//移除本次使用的formId array_push($newData,$res[$j]);//从新获取可用formId组成的新数组 } break; } } $this->_save($openId,$newData); return $result; }else{ return FALSE; } } //2.拼装模板,建立通知内容 private function create_template($openId,$formId,$content){ $templateData['keyword1']['value'] = '打卡即将开始'; $templateData['keyword1']['color'] = '#d81e06'; $templateData['keyword2']['value'] = '打卡名称'; $templateData['keyword2']['color'] = '#1aaba8'; $templateData['keyword3']['value'] = '05:00'; $templateData['keyword4']['value'] = '备注说明'; $data['touser'] = $openId; $data['template_id'] = '模板id'; $data['page'] = 'pages/detail/detail?id=1000';//用户点击模板消息后的跳转页面 $data['form_id'] = $formId; $data['data'] = $templateData; return json_encode($data); } //3.执行模板消息发布 public function send_notice($key){ $openId = '用户openId'; $formId = $this -> getFormId($openId);//获取formId $access_token = '获取access_token'; $content='通知内容';//可经过$key做为键来获取对应的通知数据 if($access_token){ $templateData = $this->create_template($openId,$formId,$content);//拼接模板数据 $res = json_decode($this->http_post('https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token='.$access_token,$templateData)); if($res->errcode == 0){ return $res; }else{ return false; } } }
Beanstalkd是一个 高性能、轻量级的分布式内存队列系统 ,咱们经过Beanstalkd将模板消息推送任务的建立以及任务的执行分开进行。
在建立推送任务时, 设置任务的执行时间以及定义推送消息的类型和通知内容等数据 。
在任务执行时,经过Beanstalkd的任务监听函数来捕获任务。经过预先在建立任务时标记的数据来肯定模板消息的具体推送内容,好比用户openId,经过用户openId获取一个可用的推送码formId,获取推送内容等,最后在调用微信小程序模板消息下发接口完成推送。
getFormId函数主要实现每次取出一个未过时可用的推送码formId,而且删除不可用的邀请码和当前已选中的邀请码,以保证必定数额的推送码formId在将来一周内可用。
关于Beanstalkd的使用介绍,可用参考一下文章,深刻研究。
最后总结一下,整个方案涉及到的关键词有 表单、按钮、formId、模板消息、Redis、Beanstalkd 等,涉及了多项技术的组合,包括 前端开发、后端开发、数据库技术 等,且先后端分工明确,共同支撑整个方案地实现。
模板消息推送方案
正如我以前文章里所说的, 微信小程序开发的难点不在于小程序自己,小程序开发技术是先后端一系列的技术的组合,开发者须要持续学习,掌握、提高更多的相关开发技术,来更好地支撑产品的功能实现 。最后,这个方案能够在用户最后一次使用小程序后的7天内,对用户发送多条模板消息唤回用户,可是请 必定要在遵循微信官方的运营规范的前提下 ,合理使用这样的模板消息推送功能。