模版消息推送是微信小程序采用的通知形式,用戶本人在小程序頁面有交互行為后,可觸發下發通知,通過微信聊天列表中的服務通知可快捷進入查看消息。此外,點擊查看詳情還能跳轉到下發消息的小程序的指定頁面。但是為 ...
模版消息推送是微信小程序采用的通知形式, 用戶本人在小程序頁面有交互行為后,可觸發下發通知 ,通過微信聊天列表中的服務通知可快捷進入查看消息。此外,點擊查看詳情還能跳轉到下發消息的小程序的指定頁面。但是為了避免這種通知被濫用,帶來不好的用戶體驗,小程序也對模板消息推送做了相應的限制。為了更好的優化小打卡小程序的打卡通知功能,我在開發的過程中自行摸索了一套突破推送限制的解決方案。可以實現 7天內向用戶推送多條模板消息,甚至向用戶群發消息的功能 。
消息通知是一個很重要的功能,如QQ空間的回復狀態通知,QQ郵箱的郵件通知,微信支付成功提通知等。這種常規的 服務跟蹤類 消息,便于用戶掌握產品對自身服務的進度,方便客戶獲取必要的信息,提高效率;保證用戶的知情權,讓用戶有安全感。同時,對于產品本身來說,可以引導用戶進行下一步行為, 增加了產品的曝光率,便于用戶留存,增強用戶粘性。
如上圖,呈現在微信聊天列表的 服務通知 ,收納了各個小程序向用戶推送模板消息,這個服務通知是用戶查看模板消息的入口,用戶點擊服務通知后可以查看到通知列表頁面,每條通知以卡片的形式呈現,包括小程序的logo、名稱、通知時間、通知內容等信息。
所謂『模板消息』,就如上面的通知卡片,首先通知卡片形式樣子是固定的,其實卡片中的通知內容部分,可以看到每天通知的內容都具備日程描述、日程主題、日程時間等要素,通知之間不同的地方在于這些要素后面的文案,將這些通知要素制作成模板,每次針對不同的通知內容 只需要填充每條要素對應的具體的文本 即可推送給用戶。上面圖中兩條模板消息的日程主題和時間不一樣,其他的信息要素保持一致,這就是模板消息。
提到模板消息的好處,第一印象是 "多、快、好、省" 的特點。
"快"即快捷,體現在微信用戶側的通知體驗,由于在微信客戶端服務通知在聊天列表中,保留了用戶以往處理聊天通知的習慣,所以用戶可以很 便捷地觸及服務通知 ,查看小程序推送的模板消息。
"好"即效果好,小程序的模板消息具備 跳轉直達小程序特定頁面 的能力,這樣用戶接收消息后,查看消息的通知就能便捷地回到小程序進行相應的業務處理、信息查看等后續操作,一定程度上提升了用戶的活躍度,小打卡小程序的近30天訪問來源數據顯示,有20%左右的用戶通過模板消息這個入口進入小打卡,在各種來源中排名第三位,可以見模板消息是用戶使用你的小程序的重要入口。
"省"即省錢唄,有了模板推送,自然 降低了消息通知的成本 ,節省費用。消息通知優先通過模板消息這種方式來推送給指定用戶,只有才無法觸及用戶的情況下,才使用傳統的付費短信推送等形式。
"多"呢?上面提到"無法觸及用戶的情況",其實是因為小程序不具備"多"的特點。物以稀為貴,模板消息雖好,但是微信小程序官方為了保證用戶體驗, 平衡通知和騷擾行為 ,對模板推送做了相應限制。接下來就聊聊這個限制。
微信小程序允許下發模板消息的條件分為兩類, 支付或者提交表單 。
目前支付的限制有所放開,即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)
}
})
/*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的使用介紹,可用參考一下文章,深入研究。
高性能分布式內存隊列系統beanstalkd(轉)
beanstalkd消息隊列使用最后總結一下,整個方案涉及到的關鍵詞有 表單、按鈕、formId、模板消息、Redis、Beanstalkd 等,涉及了多項技術的組合,包括 前端開發、后端開發、數據庫技術 等,且前后端分工明確,共同支撐整個方案地實現。
正如我之前文章里所說的, 微信小程序開發的難點不在于小程序本身,小程序開發技術是前后端一系列的技術的組合,開發者需要持續學習,掌握、提升更多的相關開發技術,來更好地支撐產品的功能實現 。最后,這個方案可以在用戶最后一次使用小程序后的7天內,對用戶發送多條模板消息喚回用戶,但是請 一定要在遵循微信官方的運營規范的前提下 ,合理使用這樣的模板消息推送功能。