模版消息推送是微信小程序採用的通知形式, 用戶本人在小程序頁面有交互行爲後,可觸發下發通知 ,經過微信聊天列表中的服務通知可快捷進入查看消息。此外,點擊查看詳情還能跳轉到下發消息的小程序的指定頁面。可是爲了不這種通知被濫用,帶來很差的用戶體驗,小程序也對模板消息推送作了相應的限制。爲了更好的優化小打卡小程序的打卡通知功能,我在開發的過程當中自行摸索了一套突破推送限制的解決方案。能夠實現 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天內,對用戶發送多條模板消息喚回用戶,可是請 必定要在遵循微信官方的運營規範的前提下 ,合理使用這樣的模板消息推送功能。