1. 程式人生 > >【轉】微信小程式模板訊息無限制群發

【轉】微信小程式模板訊息無限制群發

模版訊息推送是微信小程式採用的通知形式, 使用者本人在小程式頁面有互動行為後,可觸發下發通知 ,通過微信聊天列表中的服務通知可快捷進入檢視訊息。此外,點選檢視詳情還能跳轉到下發訊息的小程式的指定頁面。但是為了避免這種通知被濫用,帶來不好的使用者體驗,小程式也對模板訊息推送做了相應的限制。為了更好的優化小打卡小程式的打卡通知功能,我在開發的過程中自行摸索了一套突破推送限制的解決方案。可以實現 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)
  }
})

元件中屬性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。

三.後端程式如何儲存推送碼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個步驟:

設定任務執行時間並將該傳送任務推送到非同步任務佇列。
通過任務傳送服務輪詢執行任務,獲取access_token、指定你需要推送訊息的使用者的openId,根據openId獲取使用者的推送碼formId,並結合模板id拼裝模板上的通知內容,呼叫模板訊息傳送介面來非同步傳送。
普通的模板訊息的傳送就不贅述了,可參考 官方文件中的模板訊息功能 一步步進行操作,我們重點來看高效能非同步任務推送的實現方法。涉及到的關鍵程式碼如下:

//設定非同步任務
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天內,對使用者傳送多條模板訊息喚回使用者,但是請 一定要在遵循微信官方的運營規範的前提下 ,合理使用這樣的模板訊息推送功能。

閱讀原文