背景描述
小程序答題簽到功能,為了促進(jìn)日活广恢,需要每天定時(shí)向當(dāng)日未簽到的用戶推送消息提醒簽到鸠姨。
讀本篇之前最好已經(jīng)了解微信關(guān)于發(fā)送模板消息的相關(guān)文檔:
說明: 作者也是第一次寫小程序的定時(shí)模板消息功能菩颖,作為一個(gè)純種前端攻城獅噪珊,可能在建表操作數(shù)據(jù)庫等后端代碼上有不嚴(yán)謹(jǐn)或不合理的地方谤职,歡迎大佬們拍磚指正(輕拍)饰豺。本文以提供解決思路為主,僅供學(xué)習(xí)交流允蜈,如有不合理的地方還請(qǐng)留言哦冤吨。??
實(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ī)會(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;
表建好了剪验,來捋一捋邏輯:
- 用戶提交表單肴焊,將
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
接口給對(duì)應(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
一張表中感覺效率會(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)天是否再推送
再重新捋一捋邏輯:
- 用戶提交表單聂宾,將
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
接口給對(duì)應(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
表對(duì)應(yīng)的user_id
亡资,將count
自身減1。
完美結(jié)束向叉。
理清開發(fā)邏輯锥腻,就準(zhǔn)備動(dòng)手寫碼
代碼實(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ī)上獲取幸斥,模擬器會(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版)
上上一篇: 微信小程序踩坑指南