小程序答題簽到功能,爲了促進日活,須要天天定時向當日未簽到的用戶推送消息提醒簽到。html
讀本篇以前最好已經瞭解微信關於發送模板消息的相關文檔:前端
說明: 做者也是第一次寫小程序的定時模板消息功能,做爲一個純種前端攻城獅,可能在建表操做數據庫等後端代碼上有不嚴謹或不合理的地方,歡迎大佬們拍磚指正(輕拍)。本文以提供解決思路爲主,僅供學習交流,若有不合理的地方還請留言哦。😆node
微信小程序推送模板消息下發條件:mysql
支付 當用戶在小程序內完成過支付行爲,可容許開發者向用戶在 7天 內推送有限條數的模板消息 (1次支付可下發3條,屢次支付下發條數獨立,互相不影響)react
提交表單 當用戶在小程序內發生過提交表單行爲且該表單聲明爲要發模板消息的,開發者須要向用戶提供服務時,可容許開發者向用戶在 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;
複製代碼
表建好了,來捋一捋邏輯:
open_id
,user_id
(根據自身需求存此字段),form_id
,expire
以及status=0
插入到wx_save_form_id
表中wx_save_form_id
,拿到status=0
的數據,而後再調微信的templateMessage.send
接口給對應的用戶發送提示信息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
上一次推送消息的時間,用來判斷當天是否再推送
再從新捋一捋邏輯:
open_id
,user_id
(根據自身需求存此字段),form_id
,expire
以及status=0
插入到wx_save_form_id
表中,同時將wx_message_push_status
表中的count
自身+1wx_message_push_status
,經過篩選條件 count>0
且last_date
不爲當天,拿到能夠推送消息的user_id
再去查詢wx_save_form_id
表user_id=上面拿到的
,status=0
, expire >= 當前時間戳
,而後再調微信的templateMessage.send
接口給對應的用戶發送提示信息status
字段更新爲1
,下次查詢的時候講篩選掉已發送的狀態。wx_save_form_id
,篩選條件status=0
且exprie<當前時間戳
(即未發送,且過時的數據)status
改成2,且查詢wx_message_push_status
表對應的user_id
,將count
自身減1。完美結束。
理清開發邏輯,就準備動手寫碼
頁面的 form
組件,屬性 report-submit
爲 true
時,能夠聲明爲須要發送模板消息,此時點擊按鈕提交表單能夠獲取 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
小程序 前端工程化等相關的技術文章陸續更新中,歡迎訪問和吐槽~
上上一篇: 微信小程序踩坑指南