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

背景描述

小程序答題簽到功能,為了促進(jìn)日活广恢,需要每天定時(shí)向當(dāng)日未簽到的用戶推送消息提醒簽到鸠姨。

讀本篇之前最好已經(jīng)了解微信關(guān)于發(fā)送模板消息的相關(guān)文檔:

  1. 模板消息指南
  2. 模板消息服務(wù)接口

說明: 作者也是第一次寫小程序的定時(shí)模板消息功能菩颖,作為一個(gè)純種前端攻城獅噪珊,可能在建表操作數(shù)據(jù)庫等后端代碼上有不嚴(yán)謹(jǐn)或不合理的地方谤职,歡迎大佬們拍磚指正(輕拍)饰豺。本文以提供解決思路為主,僅供學(xué)習(xí)交流允蜈,如有不合理的地方還請(qǐng)留言哦冤吨。??

實(shí)現(xiàn)思路

官方限制

微信小程序推送模板消息下發(fā)條件:

  1. 支付
    當(dāng)用戶在小程序內(nèi)完成過支付行為,可允許開發(fā)者向用戶在 7天 內(nèi)推送有限條數(shù)的模板消息 (1次支付可下發(fā)3條饶套,多次支付下發(fā)條數(shù)獨(dú)立漩蟆,互相不影響)

  2. 提交表單
    當(dāng)用戶在小程序內(nèi)發(fā)生過提交表單行為且該表單聲明為要發(fā)模板消息的,開發(fā)者需要向用戶提供服務(wù)時(shí)妓蛮,可允許開發(fā)者向用戶在 7天 內(nèi)推送有限條數(shù)的模板消息 (1次提交表單可下發(fā)1條怠李,多次提交下發(fā)條數(shù)獨(dú)立,相互不影響)

根據(jù)官方的規(guī)則蛤克,顯然用戶1次觸發(fā)7天內(nèi)推送1條通知是明顯不夠用的捺癞,比如簽到功能,只有用戶在前一天簽到情況下才能獲取一次推送消息的機(jī)會(huì)构挤,然后用于第二天向該用戶發(fā)送簽到提醒髓介。倘若用戶忘記了簽到,系統(tǒng)便失去了提醒用戶的權(quán)限筋现,導(dǎo)致和用戶斷開了聯(lián)系唐础。

如何突破限制?

既然用戶1次提及表單可以下發(fā)1條消息通知夫否,且多次提交下發(fā)條數(shù)獨(dú)立且互不影響彻犁。
那我們可以合理利用規(guī)則叫胁,將頁面綁定點(diǎn)擊事件的按鈕都用form表單 report-submit=true 包裹 button form-type=submit 偽裝起來凰慈,收集formId,將formId存入數(shù)據(jù)庫中驼鹅,然后通過定時(shí)任務(wù)再去向用戶發(fā)送模板消息微谓。

開發(fā)步驟

后臺(tái)配置消息模板

微信公眾平臺(tái)->功能->模板消息->我的模板中添加模板消息,如下:

消息模板

其中模板ID和關(guān)鍵詞需要在發(fā)送模板消息的時(shí)候用到输钩。

數(shù)據(jù)庫設(shè)計(jì)

建表之前豺型,思考一下都需要存哪些數(shù)據(jù)?

根據(jù)微信的發(fā)送消息接口templateMessage.send可知,要給用戶發(fā)送一條消息需要將touser(即用戶的openid),form_id需要存入數(shù)據(jù)庫买乃。
另外獲取用戶form_id時(shí)的expire(過期時(shí)間)也需要存下來姻氨,另外還需要知道form_id是否使用以及過期的狀態(tài)需要存一下。

于是表的結(jié)構(gòu)為:

表: 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過期時(shí)間(時(shí)間戳)',
  `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(根據(jù)自身需求存此字段),form_id前联,expire 以及status=0插入到wx_save_form_id表中
  2. 開啟定時(shí)任務(wù)(比如每天10:00執(zhí)行),到固定時(shí)間查詢表wx_save_form_id娶眷,拿到status=0的數(shù)據(jù)似嗤,然后再調(diào)微信的templateMessage.send接口給對(duì)應(yīng)的用戶發(fā)送提示信息
  3. 發(fā)送完的用戶將status字段更新為1,下次查詢的時(shí)候講篩選掉已發(fā)送的狀態(tài)届宠。

想想是不是漏掉點(diǎn)什么?

一條form_id的過期時(shí)間是7天烁落,那如果過期了怎么去將狀態(tài)改完已過期呢?

一個(gè)解決辦法是豌注,再開一個(gè)定時(shí)任務(wù)(比如20min執(zhí)行一次)伤塌,去查詢哪條form_id已經(jīng)過期,然后再更改狀態(tài)轧铁。如果數(shù)據(jù)只存在wx_save_form_id一張表中感覺效率會(huì)很低寸谜,不方便,也不合理属桦。于是想到再去建立一張表:

表: 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 '可推送消息次數(shù)',
  `last_date` bigint(20) NOT NULL DEFAULT '0' COMMENT '最后一次推送消息時(shí)間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

其中 user_id(根據(jù)自身需求熊痴,也可以是open_id) 用戶id, count 可向用戶推送消息的次數(shù) last_date 上一次推送消息的時(shí)間,用來判斷當(dāng)天是否再推送

再重新捋一捋邏輯:

  1. 用戶提交表單聂宾,將open_id,user_id(根據(jù)自身需求存此字段),form_id果善,expire 以及status=0插入到wx_save_form_id表中,同時(shí)將wx_message_push_status表中的count自身+1
  2. 開啟定時(shí)任務(wù)(比如每天10:00執(zhí)行)系谐,到固定時(shí)間查詢表wx_message_push_status巾陕,通過篩選條件 count>0last_date不為當(dāng)天,拿到可以推送消息的user_id再去查詢wx_save_form_id
  3. 查詢條件user_id=上面拿到的纪他,status=0, expire >= 當(dāng)前時(shí)間戳鄙煤,然后再調(diào)微信的templateMessage.send接口給對(duì)應(yīng)的用戶發(fā)送提示信息
  4. 發(fā)送完的用戶將status字段更新為1,下次查詢的時(shí)候講篩選掉已發(fā)送的狀態(tài)茶袒。
  5. 開啟另一個(gè)定時(shí)任務(wù)(比如間隔20分鐘執(zhí)行一次)梯刚,先去查詢wx_save_form_id,篩選條件status=0exprie<當(dāng)前時(shí)間戳(即未發(fā)送,且過期的數(shù)據(jù))
  6. 將篩選到的數(shù)據(jù)status改為2薪寓,且查詢wx_message_push_status表對(duì)應(yīng)的user_id亡资,將count自身減1。

完美結(jié)束向叉。

理清開發(fā)邏輯锥腻,就準(zhǔn)備動(dòng)手寫碼

代碼實(shí)現(xiàn)

前端頁面

頁面的 form 組件,屬性 report-submittrue 時(shí)母谎,可以聲明為需要發(fā)送模板消息瘦黑,此時(shí)點(diǎn)擊按鈕提交表單可以獲取 formId

demo.wxml

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

可以將頁面中的綁定事件都用form組件來偽裝,換取更多的formId

注: 獲取form_id必須在真機(jī)上獲取幸斥,模擬器會(huì)報(bào)the formId is a mock one;

demo.js

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

server.js //node中間層 去調(diào)底層接口

async updateFormIdAction(){
    /*
     *我們的userId和openId是存在server端存崖,不需從前端傳回。
     *不必糾結(jié)接口的實(shí)現(xiàn)語法睡毒,和自身框架有關(guān)来惧。
     */
    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);
}

底層接口以及定時(shí)任務(wù)

service.js //Node 操作數(shù)據(jù)庫接口

const request = require('request');

/*
 * 根據(jù)用戶userId openId 保存用戶的formId
 * 存儲(chǔ)formId的表 wx_save_form_id
 */
async saveUserFormIdAction(){
    const http = this.http;
    const req = http.req;
    const body = req.body;
    
    //7天后過期時(shí)間戳
    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實(shí)例 
    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: '成功'
    });
}

// 更新用戶可推送消息次數(shù)
_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的記錄 則更新數(shù)據(jù)
            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){
        ...
    }
}

//發(fā)送消息的定時(shí)任務(wù)
async sendMessageTaskAction(){
    const http = this.http;
    const Today = utils.getCurrentDateInt(); //當(dāng)天日期 返回YYYYMMDD格式 具體實(shí)現(xiàn)忽略
    //篩選count>0 且當(dāng)天沒有推送過的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未過期 且 過期時(shí)間最近的數(shù)據(jù)
                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記錄 發(fā)送消息
                const { open_id, form_id } = getUserFormIds[0];
                let sendStatus = await this._sendMessageToUser(open_id, form_id);
                /*
                 *發(fā)送完消息之后
                 * 無論成功失敗 將這條form_id置為已使用 最后推送時(shí)間為當(dāng)天
                 * 將可發(fā)消息次數(shù)減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
    });
}

//發(fā)送模板消息
_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: "日領(lǐng)積分"
                    },
                    keyword2: {
                        value: '已經(jīng)連續(xù)答題N天,連續(xù)答題7天有驚喜演顾,加油~'
                    },
                    keyword3: {
                        value: "叮供搀!該簽到啦~鍥而不舍,金石可鏤。"
                    },
                    keyword4: {
                        value: time
                    }
                }
            }
        },(res)=>{
            ...
        })
    }
}

/*
 * 檢查wx_save_form_id表中的 expire字段是否過期钠至,如果過期則將status 置為2 并且
 * 對(duì)應(yīng)的 wx_message_push_status表中的count字段減1
 */
 async amendExpireTaskAction(){
    let now = new Date().getTime();
    try {
        //篩選已經(jīng)過期且未使用的記錄
        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)=>{
                //將過期的記錄狀態(tài)更新我為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);

                //將推送次數(shù)減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
    });
 }
 

執(zhí)行定時(shí)任務(wù)發(fā)送消息

呼~ 完整代碼碼完了葛虐。
大概思路是這樣的,操作數(shù)據(jù)庫沒有考慮性能問題棉钧,如果數(shù)據(jù)量大會(huì)出現(xiàn)的問題屿脐,也沒有考慮事務(wù),索引等操作(主要是不會(huì)T_T),讀者可以自行優(yōu)化宪卿。

最后需要開兩個(gè)定時(shí)任務(wù)分別執(zhí)行sendMessageTask接口和amendExpireTask接口的诵,我們的定時(shí)任務(wù)也是找的開源的node框架,具體實(shí)現(xiàn)不陳述佑钾。

最終效果:

消息提醒

參考文獻(xiàn)

突破微信小程序模板消息限制西疤,實(shí)現(xiàn)無限制主動(dòng)推送

人人貸大前端技術(shù)博客中心

最后廣而告之。
歡迎訪問人人貸大前端技術(shù)博客中心

里面有關(guān)nodejs react reactNative 小程序 前端工程化等相關(guān)的技術(shù)文章陸續(xù)更新中,歡迎訪問和吐槽~

上一篇: 小程序打怪之在線客服自動(dòng)回復(fù)功能(node版)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末休溶,一起剝皮案震驚了整個(gè)濱河市代赁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽掰,老刑警劉巖芭碍,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異孽尽,居然都是意外死亡窖壕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門泻云,熙熙樓的掌柜王于貴愁眉苦臉地迎上來艇拍,“玉大人,你說我怎么就攤上這事宠纯。” “怎么了层释?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵婆瓜,是天一觀的道長。 經(jīng)常有香客問我,道長廉白,這世上最難降的妖魔是什么个初? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮猴蹂,結(jié)果婚禮上院溺,老公的妹妹穿的比我還像新娘。我一直安慰自己磅轻,他們只是感情好珍逸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著聋溜,像睡著了一般谆膳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撮躁,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天漱病,我揣著相機(jī)與錄音,去河邊找鬼把曼。 笑死杨帽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嗤军。 我是一名探鬼主播睦尽,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼型雳!你這毒婦竟也來了当凡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤纠俭,失蹤者是張志新(化名)和其女友劉穎沿量,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冤荆,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡朴则,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钓简。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乌妒。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖外邓,靈堂內(nèi)的尸體忽然破棺而出撤蚊,到底是詐尸還是另有隱情,我是刑警寧澤损话,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布侦啸,位于F島的核電站槽唾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏光涂。R本人自食惡果不足惜庞萍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忘闻。 院中可真熱鬧钝计,春花似錦、人聲如沸齐佳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽重虑。三九已至践付,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缺厉,已是汗流浹背永高。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留提针,地道東北人命爬。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像辐脖,于是被迫代替她去往敵國和親饲宛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容