背景描述
小程序答題簽到功能,為了促進(jìn)日活,需要每天定時(shí)向當(dāng)日未簽到的用戶推送消息提醒簽到。
讀本篇之前最好已經(jīng)了解微信關(guān)于發(fā)送模板消息的相關(guān)文檔:
-
模板消息指南
-
模板消息服務(wù)接口
說明: 作者也是第一次寫小程序的定時(shí)模板消息功能,作為一個(gè)純種前端攻城獅,可能在建表操作數(shù)據(jù)庫等后端代碼上有不嚴(yán)謹(jǐn)或不合理的地方,歡迎大佬們拍磚指正(輕拍)。本文以提供解決思路為主,僅供學(xué)習(xí)交流,如有不合理的地方還請留言哦。??
實(shí)現(xiàn)思路
官方限制
微信小程序推送模板消息下發(fā)條件:
-
支付 當(dāng)用戶在小程序內(nèi)完成過支付行為,可允許開發(fā)者向用戶在 7天 內(nèi)推送有限條數(shù)的模板消息 (1次支付可下發(fā)3條,多次支付下發(fā)條數(shù)獨(dú)立,互相不影響)
-
提交表單 當(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ī)會,然后用于第二天向該用戶發(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ā)步驟
后臺配置消息模板
微信公眾平臺->功能->模板消息->我的模板中添加模板消息,如下:
其中模板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;
表建好了,來捋一捋邏輯:
-
用戶提交表單,將
open_id
,user_id
(根據(jù)自身需求存此字段),form_id
,expire
以及status=0
插入到wx_save_form_id
表中 -
開啟定時(shí)任務(wù)(比如每天10:00執(zhí)行),到固定時(shí)間查詢表
wx_save_form_id
,拿到status=0
的數(shù)據(jù),然后再調(diào)微信的templateMessage.send
接口給對應(yīng)的用戶發(fā)送提示信息 -
發(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
一張表中感覺效率會很低,不方便,也不合理。于是想到再去建立一張表:
表: 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)天是否再推送
再重新捋一捋邏輯:
-
用戶提交表單,將
open_id
,user_id
(根據(jù)自身需求存此字段),form_id
,expire
以及status=0
插入到wx_save_form_id
表中,同時(shí)將wx_message_push_status
表中的count
自身+1 -
開啟定時(shí)任務(wù)(比如每天10:00執(zhí)行),到固定時(shí)間查詢表
wx_message_push_status
,通過篩選條件count>0
且last_date
不為當(dāng)天,拿到可以推送消息的user_id
再去查詢wx_save_form_id
表 -
查詢條件
user_id=上面拿到的
,status=0
,expire >= 當(dāng)前時(shí)間戳
,然后再調(diào)微信的templateMessage.send
接口給對應(yīng)的用戶發(fā)送提示信息 -
發(fā)送完的用戶將
status
字段更新為1
,下次查詢的時(shí)候講篩選掉已發(fā)送的狀態(tài)。 -
開啟另一個(gè)定時(shí)任務(wù)(比如間隔20分鐘執(zhí)行一次),先去查詢
wx_save_form_id
,篩選條件status=0
且exprie<當(dāng)前時(shí)間戳
(即未發(fā)送,且過期的數(shù)據(jù)) -
將篩選到的數(shù)據(jù)
status
改為2,且查詢wx_message_push_status
表對應(yīng)的user_id
,將count
自身減1。
完美結(jié)束。
理清開發(fā)邏輯,就準(zhǔn)備動手寫碼
代碼實(shí)現(xiàn)
前端頁面
頁面的 form
組件,屬性 report-submit
為 true
時(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ī)上獲取,模擬器會報(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
?*?存儲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?并且
?*?對應(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ù)量大會出現(xiàn)的問題,也沒有考慮事務(wù),索引等操作(主要是不會T_T),讀者可以自行優(yōu)化。
最后需要開兩個(gè)定時(shí)任務(wù)分別執(zhí)行sendMessageTask
接口和amendExpireTask
接口,我們的定時(shí)任務(wù)也是找的開源的node框架,具體實(shí)現(xiàn)不陳述。
最終效果:
文章來源:http://www.zghlxwxcb.cn/news/detail-771237.html
參考文獻(xiàn)
突破微信小程序模板消息限制,實(shí)現(xiàn)無限制主動推送文章來源地址http://www.zghlxwxcb.cn/news/detail-771237.html
到了這里,關(guān)于微信小程序打怪之定時(shí)發(fā)送模板消息(node版)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!