微信小程序打怪之定時發送模板消息(node版)

背景描述

小程序答題簽到功能,爲了促進日活,須要天天定時向當日未簽到的用戶推送消息提醒簽到。html

讀本篇以前最好已經瞭解微信關於發送模板消息的相關文檔:前端

  1. 模板消息指南
  2. 模板消息服務接口

說明: 做者也是第一次寫小程序的定時模板消息功能,做爲一個純種前端攻城獅,可能在建表操做數據庫等後端代碼上有不嚴謹或不合理的地方,歡迎大佬們拍磚指正(輕拍)。本文以提供解決思路爲主,僅供學習交流,若有不合理的地方還請留言哦。😆node

實現思路

官方限制

微信小程序推送模板消息下發條件:mysql

  1. 支付 當用戶在小程序內完成過支付行爲,可容許開發者向用戶在 7天 內推送有限條數的模板消息 (1次支付可下發3條,屢次支付下發條數獨立,互相不影響)react

  2. 提交表單 當用戶在小程序內發生過提交表單行爲且該表單聲明爲要發模板消息的,開發者須要向用戶提供服務時,可容許開發者向用戶在 7天 內推送有限條數的模板消息 (1次提交表單可下發1條,屢次提交下發條數獨立,相互不影響)git

根據官方的規則,顯然用戶1次觸發7天內推送1條通知是明顯不夠用的,好比簽到功能,只有用戶在前一天簽到狀況下才能獲取一次推送消息的機會,而後用於次日向該用戶發送簽到提醒。假若用戶忘記了簽到,系統便失去了提醒用戶的權限,致使和用戶斷開了聯繫。github

如何突破限制?

既然用戶1次說起表單能夠下發1條消息通知,且屢次提交下發條數獨立且互不影響。 那咱們能夠合理利用規則,將頁面綁定點擊事件的按鈕都用form表單 report-submit=true 包裹 button form-type=submit 假裝起來,收集formId,將formId存入數據庫中,而後經過定時任務再去向用戶發送模板消息。sql

開發步驟

後臺配置消息模板

微信公衆平臺->功能->模板消息->個人模板中添加模板消息,以下:數據庫

消息模板

其中模板ID和關鍵詞須要在發送模板消息的時候用到。json

數據庫設計

建表以前,思考一下都須要存哪些數據?

根據微信的發送消息接口templateMessage.send可知,要給用戶發送一條消息須要將touser(即用戶的openid),form_id須要存入數據庫。 另外獲取用戶form_id時的expire(過時時間)也須要存下來,另外還須要知道form_id是否使用以及過時的狀態須要存一下。

因而表的結構爲:

表: wx_save_form_id

id open_id user_id form_id expire status
1 xxxxxx 1234 xxxx 1562642733399 0

sql

CREATE TABLE `wx_save_form_id` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `open_id` char(100) NOT NULL DEFAULT '',
  `user_id` int(11) NOT NULL,
  `form_id` char(100) NOT NULL DEFAULT '',
  `expire` bigint(20) NOT NULL COMMENT 'form_id過時時間(時間戳)',
  `status` int(1) DEFAULT '0' COMMENT '0 未推送 1已推送 2 過時',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8;

複製代碼

表建好了,來捋一捋邏輯:

  1. 用戶提交表單,將open_id,user_id(根據自身需求存此字段),form_idexpire 以及status=0插入到wx_save_form_id表中
  2. 開啓定時任務(好比天天10:00執行),到固定時間查詢表wx_save_form_id,拿到status=0的數據,而後再調微信的templateMessage.send接口給對應的用戶發送提示信息
  3. 發送完的用戶將status字段更新爲1,下次查詢的時候講篩選掉已發送的狀態。

想一想是否是漏掉點什麼?

一條form_id的過時時間是7天,那若是過時了怎麼去將狀態改完已過時呢?

一個解決辦法是,再開一個定時任務(好比20min執行一次),去查詢哪條form_id已通過期,而後再更改狀態。若是數據只存在wx_save_form_id一張表中感受效率會很低,不方便,也不合理。因而想到再去創建一張表:

表: wx_message_push_status

id user_id count last_push
1 1234 5 20190701

sql

CREATE TABLE `wx_message_push_status` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `count` int(11) NOT NULL DEFAULT '1' COMMENT '可推送消息次數',
  `last_date` bigint(20) NOT NULL DEFAULT '0' COMMENT '最後一次推送消息時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
複製代碼

其中 user_id(根據自身需求,也能夠是open_id) 用戶id, count 可向用戶推送消息的次數 last_date 上一次推送消息的時間,用來判斷當天是否再推送

再從新捋一捋邏輯:

  1. 用戶提交表單,將open_id,user_id(根據自身需求存此字段),form_idexpire 以及status=0插入到wx_save_form_id表中,同時將wx_message_push_status表中的count自身+1
  2. 開啓定時任務(好比天天10:00執行),到固定時間查詢表wx_message_push_status,經過篩選條件 count>0last_date不爲當天,拿到能夠推送消息的user_id再去查詢wx_save_form_id
  3. 查詢條件user_id=上面拿到的status=0, expire >= 當前時間戳,而後再調微信的templateMessage.send接口給對應的用戶發送提示信息
  4. 發送完的用戶將status字段更新爲1,下次查詢的時候講篩選掉已發送的狀態。
  5. 開啓另外一個定時任務(好比間隔20分鐘執行一次),先去查詢wx_save_form_id,篩選條件status=0exprie<當前時間戳(即未發送,且過時的數據)
  6. 將篩選到的數據status改成2,且查詢wx_message_push_status表對應的user_id,將count自身減1。

完美結束。

理清開發邏輯,就準備動手寫碼

代碼實現

前端頁面

頁面的 form 組件,屬性 report-submittrue 時,能夠聲明爲須要發送模板消息,此時點擊按鈕提交表單能夠獲取 formId

demo.wxml

<form report-submit="true" bindsubmit="uploadFormId">
    <button form-type="submit" hover-class="none" >提交</button>
</form>
複製代碼

能夠將頁面中的綁定事件都用form組件來假裝,換取更多的formId

注: 獲取form_id必須在真機上獲取,模擬器會報the formId is a mock one;

demo.js

Page({
    ...
    uploadFormId(e){
        //上傳form_id 發模板消息
        wx.request({
            url: 'xx/xx/uploadFormId',
            data: {
                form_id: e.detail.formId
            }
        });
    }
    ...
})
複製代碼
服務端接口

server.js //node中間層 去調底層接口

async updateFormIdAction(){
    /*
     *咱們的userId和openId是存在server端,不需從前端傳回。
     *沒必要糾結接口的實現語法,和自身框架有關。
     */
    const {ctx} = this;
    const user = ctx.user;
    const userId = user ? user.userId : '';
    const loginSession = ctx.loginSession;
    const body = ctx.request.body;

    let openId = loginSession.getData().miniProgram_openId || '';

    const result = await this.callService('nodeMarket.saveUserFormId', openId, userId, body.form_id);
    return this.json(result);
}

複製代碼
底層接口以及定時任務

service.js //Node 操做數據庫接口

const request = require('request');

/*
 * 根據用戶userId openId 保存用戶的formId
 * 存儲formId的表 wx_save_form_id
 */
async saveUserFormIdAction(){
    const http = this.http;
    const req = http.req;
    const body = req.body;
    
    //7天后過時時間戳
    let expire = new Date().getTime() + (7 * 24 * 60 * 60 *1000); 
    const sql = `INSERT INTO wx_save_form_id (open_id, user_id, form_id, expire) VALUES(${body.openId}, ${body.userId}, ${body.formId}, ${expire}) `;
    //自行封裝好的mysql實例 
    let tmpResult = await mysqlClient.query(sql);
    let result = tmpResult.results;
    if (! result || result.affectedRows !== 1) {
        ...
    }
    
    await this._updateMessagePushStatusByUserId(body.userId);
    return this.json({
        status: 0,
        message: '成功'
    });
}

// 更新用戶可推送消息次數
_updateMessagePushStatusByUserId(user_id){
    const http = this.http;
    try{
        const selectSql = `SELECT user_id, count from wx_message_push_status WHERE user_id = ${user_id}`;
        let temp = await mysqlClient.query(sql);
        let result = temp.results;
        if(result.length){
            //有該user_id的記錄 則更新數據
            const updateSql = `UPDATE wx_message_push_status SET count = count + 1 WHERE user_id = ${user_id}`;
            await mysqlClient.query(sql);
            ...
        }else {
            //無記錄 則插入新的記錄
            const insertSql = `INSERT INTO wx_message_push_status user_id VALUES $(user_id)`;
            await mysqlClient.query(sql);
            ...
        }
    }catch(err){
        ...
    }
}

//發送消息的定時任務
async sendMessageTaskAction(){
    const http = this.http;
    const Today = utils.getCurrentDateInt(); //當天日期 返回YYYYMMDD格式 具體實現忽略
    //篩選count>0 且當天沒有推送過的user_id
    const selectCanPushSql = `select user_id from wx_message_push_status WHERE count > 0 AND last_date != ${Today}`;
    let temp = await mysqlClient.query(selectCanPushSql);
    let selectCanPush = temp.results;
    
    if(selectCanPush.length){
        selectCanPush.forEach(async (record)=>{
            try{
                let user_id = record.user_id;
                //篩選出 status = 0, 且formId未過時 且 過時時間最近的數據
                const currentTime = new Date().getTime();
                const getFormIdSql = `select open_id, user_id, form_id from wx_save_form_id WHERE user_id = ${user_id} AND status = 0 AND expire >= ${currentTime} AND form_id != 'the formId is a mock one' ORDER BY expire ASC`;
                let getFormIdTemp = await mysqlClient.query(getFormIdSql);
                //獲取可用的form_id列表
                let getUserFormIds = getFormIdTemp.results;
                //取出第一條可用的formId記錄 發送消息
                const { open_id, form_id } = getUserFormIds[0];
                let sendStatus = await this._sendMessageToUser(open_id, form_id);
                /*
                 *發送完消息以後
                 * 不管成功失敗 將這條form_id置爲已使用 最後推送時間爲當天
                 * 將可發消息次數減1
                 */
                let updateCountSql = `UPDATE wx_message_push_status SET count = count - 1, last_date = ${Today} WHERE count >0 AND user_id = ${user_id}; ` ;
                await mysqlClient.query(updateCountSql);
                
                let updateStatusSql = `UPDATE wx_save_form_id SET status = 1 WHERE user_id = ${user_id} AND open_id = ${open_id} AND form_id = ${form_id}`;
                await mysqlClient.query(updateStatusSql);
                ...
            }catch(err){
                ...
            }
        });
    }
    this.json({
        status: 0
    });
}

//發送模板消息
_sendMessageToUser(open_id, form_id){
    let accessToken = await this._getAccessToken();//獲取token方法省略
    const oDate = new Date();
    const time = oDate.getFullYear() + '.' + (oDate.getMonth()+1) + '.' + oDate.getDate();
    if(accessToken){
        const url = `https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=${accessToken}`;
        request({
            url,
            method: 'POST',
            data: {
                access_token,
                touser: open_id,
                form_id,
                page: 'pages/xxx/xxx',
                template_id: '你的模板ID',
                data: {
                    keyword1: {
                        value: "日領積分"
                    },
                    keyword2: {
                        value: '已經連續答題N天,連續答題7天有驚喜,加油~'
                    },
                    keyword3: {
                        value: "叮!該簽到啦~持之以恆,金石可鏤。"
                    },
                    keyword4: {
                        value: time
                    }
                }
            }
        },(res)=>{
            ...
        })
    }
}

/*
 * 檢查wx_save_form_id表中的 expire字段是否過時,若是過時則將status 置爲2 而且
 * 對應的 wx_message_push_status表中的count字段減1
 */
 async amendExpireTaskAction(){
    let now = new Date().getTime();
    try {
        //篩選已通過期且未使用的記錄
        const expiredSql = `select * from wx_save_form_id WHERE status = 0 AND expire < ${now}`;
        let expiredTemp = await mysqlClient.query(expiredSql);
        let expired = expiredTemp.results;
        if (expired.length){
            expired.forEach(async (record)=>{
                //將過時的記錄狀態更新我爲2
                const updateStatusSql = `UPDATE wx_save_form_id SET status = 2 WHERE open_id = '${record.open_id}' AND user_id = ${record.user_id} AND form_id = '${record.form_id}' `;
                await mysqlClient.query(updateStatusSql);

                //將推送次數減1
                let updateCountSql = `UPDATE wx_message_push_status SET count = count - 1 WHERE count >0 AND user_id = ${record.user_id}; ` ;
                await mysqlClient.query(updateCountSql);
            });
        }

    }catch (e) {
    }
    this.json({
        status: 0
    });
 }
 
複製代碼

執行定時任務發送消息

呼~ 完整代碼碼完了。 大概思路是這樣的,操做數據庫沒有考慮性能問題,若是數據量大會出現的問題,也沒有考慮事務,索引等操做(主要是不會T_T),讀者能夠自行優化。

最後須要開兩個定時任務分別執行sendMessageTask接口和amendExpireTask接口,咱們的定時任務也是找的開源的node框架,具體實現不陳述。

最終效果:

消息提醒

參考文獻

突破微信小程序模板消息限制,實現無限制主動推送

人人貸大前端技術博客中心

最後廣而告之。 歡迎訪問人人貸大前端技術博客中心

裏面有關nodejs react reactNative 小程序 前端工程化等相關的技術文章陸續更新中,歡迎訪問和吐槽~

上一篇: 小程序打怪之在線客服自動回覆功能(node版)

上上一篇: 微信小程序踩坑指南

相關文章
相關標籤/搜索