訂閱消息是小程序能力中的重要組成,當(dāng)用戶自主訂閱之后,可以向用戶以服務(wù)通知的方式發(fā)送消息的能力,當(dāng)用戶點擊訂閱消息卡片可以跳轉(zhuǎn)到小程序的頁面,這樣就可以實現(xiàn)服務(wù)的閉環(huán)和更優(yōu)的體驗,提高活躍度和用戶粘性。
要獲取訂閱消息授權(quán),首先要調(diào)用接口wx.requestSubscribeMessage,這個接口會調(diào)起小程序訂閱消息界面,返回用戶訂閱消息的操作結(jié)果。注意這個接口只能在小程序端使用tap點擊或支付完成后觸發(fā)。如果是使用頁面加載或其他非用戶點擊類的事件來調(diào)用這個接口,就會報requestSubscribeMessage:fail can only be invoked by user TAP gesture
的錯誤。
要調(diào)用wx.requestSubscribeMessage,需要我們首先要有訂閱消息的模板ID,一次性模板 id 和永久模板 id 不可同時使用,基礎(chǔ)庫2.8.4之后一次性可以調(diào)起3個模板ID(不能多于3個)。
使用開發(fā)者工具新建一個頁面,如subscribe,然后在subscribe.wxml里輸入以下代碼,我們通過點擊tap來觸發(fā)事件處理函數(shù):
<button bindtap="subscribeMessage">訂閱訂閱消息</button>
然后再在subscribe.js里輸入以下代碼,我們在事件處理函數(shù)subscribeMessage里調(diào)用wx.requestSubscribeMessage接口:
subscribeMessage() {
wx.requestSubscribeMessage({
tmplIds: [
"qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44",//模板
"RCg8DiM_y1erbOXR9DzW_jKs-qSSJ9KF0h8lbKKmoFU",
"EGKyfjAO2-mrlJQ1u6H9mZS8QquxutBux1QbnfDDtj0"
],
success(res) {
console.log("訂閱消息API調(diào)用成功:",res)
},
fail(res) {
console.log("訂閱消息API調(diào)用失?。?,res)
}
})
},
建議大家在手機上進行真機調(diào)試這個接口,點擊訂閱消息button,就能彈出授權(quán)彈窗。
errcode":"43101","errmsg":"user refuse to accept the msg hint...
。注意該接口調(diào)用成功之后返回的對象,[TEMPLATE_ID]是動態(tài)的鍵,即模板id,值包括'accept'、'reject'、'ban'。'accept'表示用戶同意訂閱該條id對應(yīng)的模板消息,'reject'表示用戶拒絕訂閱該條id對應(yīng)的模板消息,'ban'表示已被后臺封禁,如下所示(以下值僅為案例):
{errMsg: "requestSubscribeMessage:ok", RCg8DiM_y1erbOXR9DzW_jKs-qSSJ9KF0h8lbKKmoFU: "accept", qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44: "reject", EGKyfjAO2-mrlJQ1u6H9mZS8QquxutBux1QbnfDDtj0: "accept"}
訂閱消息的累積次數(shù)決定了我們是否可以給用戶發(fā)送訂閱消息,也決定了可以發(fā)送幾次,因此記錄用戶給某個模板ID授權(quán)了多少次這個也就顯得很重要了,比如我們可以結(jié)合接口返回的res對象和inc原子自增在數(shù)據(jù)庫里記錄訂閱次數(shù),當(dāng)發(fā)送一次也會消耗一次,再用inc自減:
subscribeMessage() {
const tmplIds= [
"qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44",
"RCg8DiM_y1erbOXR9DzW_jKs-qSSJ9KF0h8lbKKmoFU",
"EGKyfjAO2-mrlJQ1u6H9mZS8QquxutBux1QbnfDDtj0"
];
wx.requestSubscribeMessage({
tmplIds:tmplIds,
success(res) {
console.log("訂閱消息API調(diào)用成功:",res)
tmplIds.map(function(item,index){
if(res[item] === "accept"){
console.log("該模板ID用戶同意了",item)
//可以使用原子自增指令inc往數(shù)據(jù)庫里某個記錄授權(quán)次數(shù)的字段+1
}
})
},
fail(res) {
console.log("訂閱消息API調(diào)用失?。?,res)
}
})
},
wx.requestSubscribeMessage的參數(shù)tmplIds是數(shù)組可以容納3個模板ID,當(dāng)用戶點擊授權(quán)彈窗,三個模板ID都是默認勾選的,只要用戶點擊允許,就會同時給三個模板ID累積次數(shù);如果用戶取消勾選了其中一個模板ID,并點擊總是允許,那另外兩個勾選的模板ID將不會再有授權(quán)彈窗。
訂閱消息最核心的在于用戶的授權(quán)與授權(quán)次數(shù),也就是你在寫訂閱消息代碼時或在發(fā)送訂閱消息之前,最好是先用數(shù)據(jù)庫記錄用戶是否已經(jīng)授權(quán)以及授權(quán)的次數(shù),關(guān)于訂閱消息的授權(quán)次數(shù)的累積需要再說明的是:
訂閱消息的種類很多,比如有的訂閱消息用戶接收一次之后就不會再接收,這時我們側(cè)重于記錄訂閱消息是否被用戶同意就可以了;但是有的訂閱消息記錄用戶授權(quán)的次數(shù)有利于我們可以更好的為用戶服務(wù),比如日報、周報、活動消息等一些與用戶交互比較頻繁的信息。在前面我們已經(jīng)多次強調(diào)了云數(shù)據(jù)庫的原子操作,這里再以訂閱消息次數(shù)累積的增加(授權(quán)只能增加)為例,來看原子操作是如何處理的。
使用云開發(fā)控制臺新建一個messages集合,messages集合的記錄結(jié)構(gòu)如下所示,在設(shè)計上我們把同一個用戶多個不同類型的訂閱消息內(nèi)嵌到一個數(shù)組templs里面。
_id:"" //可以直接為用戶的openid,這樣我們可以使用db.collection('messages').doc(openid)來處理;不過我們的案例的_id不是openid
_openid:"" //云開發(fā)自動生成的openid
templs:[{ //把用戶授權(quán)過的模板列表都記錄在這里
templateId:"qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44",//訂閱
page:"",
data:{}, //訂閱消息內(nèi)容對象,建議內(nèi)嵌到里面,免得查兩次
status:1, //用戶對該條模板消息是否接受'accept'、'reject'、'ban',
subStyle:"daily", //訂閱類型,比如是每天daily,還是每周weekly
done:false, //本次是否發(fā)送了
subNum:22, //該條訂閱消息用戶授權(quán)累積的次數(shù);
},{
}]
下面是用戶在小程序端點擊訂閱消息之后的完整代碼,記錄不同的訂閱消息被用戶點擊之后,次數(shù)的累積。代碼沒有記錄用戶是否拒絕reject,如果業(yè)務(wù)上有需要也是可以記錄的,不過拒絕不存在累積次數(shù)的問題。
subscribeMessage() {
const that = this
//模板ID建議放置在數(shù)據(jù)庫中,便于以后修改
const tmplIds= [
"qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44",
"RCg8DiM_y1erbOXR9DzW_jKs-qSSJ9KF0h8lbKKmoFU",
"EGKyfjAO2-mrlJQ1u6H9mZS8QquxutBux1QbnfDDtj0"
];
wx.requestSubscribeMessage({
tmplIds:tmplIds,
success: res => {
console.log("訂閱消息API調(diào)用成功:",res)
that.addMessages().then( id =>{
tmplIds.map(function(item,index){
if(res[item] === "accept"){
console.log("該模板ID用戶同意了",item)
that.subscribeNum(item,id)
}
})
})
},
fail(res) {
console.log("訂閱消息API調(diào)用失?。?,res)
}
})
},
async addMessages(){
//查詢用戶訂閱過的訂閱消息,只會有一條記錄,所以沒有l(wèi)imit等限制
const messages = await db.collection('messages').where({
_openid:'{openid}'
}).get()
//如果用戶沒有訂閱過訂閱消息,就創(chuàng)建一條記錄
if(messages.data.length == 0){
var newMsg = await db.collection('messages').add({
data:{
templs:[]
}
})
}
var id = messages.data[0] ? messages.data[0]._id : newMsg._id
return id
},
async subscribeNum(item,id){
//注意傳入的item是遍歷,id為addMessages的id
const subs = await db.collection('messages').where({
_openid:'{openid}',
"templs":_.elemMatch({
templateId:item
})
}).get()
console.log('用戶訂閱列表',subs)
//如果用戶之前沒有訂閱過訂閱消息就創(chuàng)建一個訂閱消息的記錄
if(subs.data.length == 0){
db.collection('messages').doc(id).update({
data: {
templs:_.push({
each:[{templateId:item,//訂閱
page:"",
data:{},
status:1,
subStyle:"daily",
done:false,
subNum:1}],
position:2
})
}
})
}else{
db.collection('messages').where({
_id:id,
"templs.templateId":item
})
.update({
data:{
"templs.$.subNum":_.inc(1)
}
})
}
}
這里的"templs.$.subNum":_.inc(1)
就是當(dāng)用于同意哪條訂閱消息,就會給該訂閱消息的授權(quán)次數(shù)進行原子加1。
當(dāng)我們在小程序端累積了某個模板ID的授權(quán)次數(shù)之后,就可以通過云函數(shù)來調(diào)用subscribeMessage.send接口發(fā)送訂閱消息了。而這個云函數(shù)我們可以在小程序端調(diào)用,也可以使用云函數(shù)來調(diào)用云函數(shù),還能使用定時觸發(fā)器來調(diào)用云函數(shù)。
云函數(shù)調(diào)用subscribeMessage.send接口的方式有兩種,一種是HTTPS調(diào)用,還有一種就是云調(diào)用,建議使用云調(diào)用。調(diào)用subscribeMessage.send接口時有很多細節(jié)需要注意,尤其是data格式,必須符合格式要求。
訂閱消息的data必須與模板消息一一對應(yīng)
比如我們申請到一個訂閱課程開課提醒的模板,它的格式如下:
姓名{{phrase1.DATA}}
課程標題{{thing2.DATA}}
課程內(nèi)容{{thing3.DATA}}
時間{{date5.DATA}}
課程進度{{character_string6.DATA}}
與之相應(yīng)的data的寫法如下phrase1、thing2、thing3、date5、character_string6,這些需要一一對應(yīng),參數(shù)不能多也不能少,參數(shù)后面的數(shù)字比如date5不能改成date6,否則會報"openapi.subscribeMessage.send:fail argument invalid! hint:
的錯誤,也就是模板里有什么參數(shù),你就只能按部就班寫什么參數(shù):
data: {
"phrase1": {
"value": '李東'
},
"thing2": {
"value": '零基礎(chǔ)云開發(fā)技術(shù)訓(xùn)練營第7課'
},
"thing3": {
"value": '列表渲染與條件渲染'
},
"date5": {
"value": '2019年10月20日 20:00'
},
"character_string6": {
"value": 3
}
}
訂閱消息參數(shù)值的內(nèi)容格式必須要符合要求
在技術(shù)文檔里,有一個關(guān)于訂閱消息參數(shù)值的內(nèi)容格式要求,這個在寫訂閱消息內(nèi)容的時候需要嚴格的一一對應(yīng),否則會出現(xiàn)格式錯誤。
參數(shù)類別 | 參數(shù)說明 | 參數(shù)值限制 | 說明 |
---|---|---|---|
thing.DATA | 事物 | 20個以內(nèi)字符 | 可漢字、數(shù)字、字母或符號組合 |
number.DATA | 數(shù)字 | 32位以內(nèi)數(shù)字 | 只能數(shù)字,可帶小數(shù) |
letter.DATA | 字母 | 32位以內(nèi)字母 | 只能字母 |
symbol.DATA | 符號 | 5位以內(nèi)符號 | 只能符號 |
character_string.DATA | 字符串 | 32位以內(nèi)數(shù)字、字母或符號 | 可數(shù)字、字母或符號組合 |
time.DATA | 時間 | 24小時制時間格式(支持+年月日) | 例如:15:01,或:2019年10月1日 15:01 |
date.DATA | 日期 | 年月日格式(支持+24小時制時間) | 例如:2019年10月1日,或:2019年10月1日 15:01 |
amount.DATA | 金額 | 1個幣種符號+10位以內(nèi)純數(shù)字,可帶小數(shù),結(jié)尾可帶“元” | 可帶小數(shù) |
phone_number.DATA | 電話 | 17位以內(nèi),數(shù)字、符號 | 電話號碼,例:+86-0766-66888866 |
car_number.DATA | 車牌 | 8位以內(nèi),第一位與最后一位可為漢字,其余為字母或數(shù)字 | 車牌號碼:粵A8Z888掛 |
name.DATA | 姓名 | 10個以內(nèi)純漢字或20個以內(nèi)純字母或符號 | 中文名10個漢字內(nèi);純英文名20個字母內(nèi);中文和字母混合按中文名算,10個字內(nèi) |
phrase.DATA | 漢字 | 5個以內(nèi)漢字 | 5個以內(nèi)純漢字,例如:配送中 |
下面列舉一些在使用過程中容易犯的錯誤:
姓名{{phrase1.DATA}}
,因為姓名只能是中文,且必須5個字以內(nèi),那你就沒法擅自改動,只能去申請或復(fù)用其他的模板ID;在前面我們說過,在小程序端哪個用戶點擊授權(quán)就只會給哪個用戶增加授權(quán)次數(shù),而借助于云函數(shù)發(fā)送訂閱消息則用戶可以給任何人發(fā)送訂閱消息,發(fā)給哪個人就需要哪個人有授權(quán)次數(shù),就會減少哪個人的授權(quán)次數(shù),這一點要注意區(qū)分。
新建一個云函數(shù)比如subscribeMessage,然后再在config.json的添加subscribeMessage.send權(quán)限,使用云函數(shù)增量上傳更新這個配置文件。
{
"permissions": {
"openapi": [
"subscribeMessage.send"
]
}
}
然后再在index.js里輸入以下代碼,注意這里的openid,是用戶自己的,這種適用于用戶在小程序端完成某個業(yè)務(wù)操作之后,就給用戶自己發(fā)訂閱消息;當(dāng)然這里的openid可以是其他累積了授權(quán)次數(shù)的用戶的,也就是當(dāng)我們在小程序端調(diào)用該云函數(shù)就能給其他人發(fā)訂閱消息了,這主要適用于管理員:
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
exports.main = async (event, context) => {
const { OPENID } = cloud.getWXContext()
try {
const result = await cloud.openapi.subscribeMessage.send({
touser: "oUL-m5FuRmuVmxvbYOGuXbuEDsn8",
page: 'index',
templateId: "qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44",
data: {
"phrase1": {
"value": '小明'
},
"thing2": {
"value": '零基礎(chǔ)云開發(fā)技術(shù)訓(xùn)練營第7課'
},
"thing3": {
"value": '列表渲染與條件渲染'
},
"date5": {
"value": '2019年10月20日 20:00'
},
"character_string6": {
"value": 3
}
}
})
return result
} catch (err) {
console.log(err)
return err
}
}
由于subscribeMessage.send的參數(shù)templateId和touser都是字符串,因此執(zhí)行一次subscribeMessage.send只能給一個用戶發(fā)送一條訂閱消息,那要給更多用戶比如1000人以內(nèi)(云函數(shù)一次可以獲取到1000條數(shù)據(jù))發(fā)訂閱消息,則需要結(jié)合數(shù)據(jù)庫的查詢數(shù)據(jù)庫內(nèi)所有有授權(quán)次數(shù)的用戶然后循環(huán)執(zhí)行來發(fā)消息,并在發(fā)完之后使用inc自減來減去授權(quán)次數(shù)。
由于我們把用戶授權(quán)的所有訂閱消息內(nèi)嵌到templs這個數(shù)組里,而要發(fā)送的訂閱消息的內(nèi)容則來自templs數(shù)組里符合條件的對象,這里涉及到相對比較復(fù)雜的數(shù)組的處理,因此數(shù)據(jù)分析處理神器聚合就派上用場了(當(dāng)然我們也可以使用普通查詢,普通查詢得到的是記錄列表,再使用一些數(shù)組方法如filter、map等取出列表里的templs嵌套的對象列表)。
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()
const _ = db.command
const $ = db.command.aggregate
exports.main = async (event, context) => {
const templateId ="qY7MhvZOnL0QsRzK_C7FFsXTT7Kz0-knXMwkF1ewY44"
try {
const messages = (await db.collection('messages').aggregate()
.match({ //使用match匹配查詢
"templs.templateId":templateId, //注意這里templs.templateId的寫法
"done":false,
"status":1
})
.project({
_id:0,
templs: $.filter({ //從嵌套的templs數(shù)組里取出模板ID滿足條件的對象
input: '$templs',
as: 'item',
cond: $.eq(['$$item.templateId',templateId])
})
})
.project({
message:$.arrayElemAt(['$templs', 0]), //符號條件的是只有1個對象的數(shù)組,取出這個對象
})
.end()).list //使用聚合查詢到的是一個list對象
const tasks = []
for (let item in messages) {
const promise = cloud.openapi.subscribeMessage.send({
touser: item.message._openid,
page: 'index',
templateId: item.message.templateId,
data: item.message.data
})
tasks.push(promise)
}
return (await Promise.all(tasks)).reduce((acc, cur) => {
return {
data: acc.data.concat(cur.data),
errMsg: acc.errMsg,
}
})
} catch (err) {
console.log(err);
return err;
}
}
特別注意的是,不要把查詢數(shù)據(jù)庫的語句放到循環(huán)里面,也就是我們可以一次性取出1000條需要發(fā)訂閱消息的用戶,然后再結(jié)合map和Promise.all方法給這1000個用戶發(fā)送訂閱消息,然后再一次性給所有這1000條數(shù)據(jù)進行原子自增,不能一條一條處理,否則會造成數(shù)據(jù)庫性能的極大浪費以及超出最大連接數(shù),而且也會導(dǎo)致云函數(shù)在最高60s的生命周期里也發(fā)送不了幾百條訂閱消息。
但是當(dāng)要發(fā)送訂閱消息的用戶有幾十萬幾百萬,那應(yīng)該怎么處理呢?如果全部讓云函數(shù)來執(zhí)行,即使將云函數(shù)的執(zhí)行超時時間修改為60s,也應(yīng)該會超時,這時候我們可以結(jié)合定時器來發(fā)送訂閱消息。
使用定時觸發(fā)器來發(fā)送訂閱消息,也就是在小程序的云開發(fā)服務(wù)端,用定時觸發(fā)器調(diào)用訂閱消息的云調(diào)用接口openapi.subscribeMessage.send。當(dāng)我們每天要給數(shù)十萬人定時發(fā)送訂閱消息時,這時候定時觸發(fā)器就不僅僅需要比如每天早上9點觸發(fā),而且還需要在9點之后能夠每隔一段時間比如40s,就來執(zhí)行一次云函數(shù)以便給數(shù)十萬用戶發(fā)送訂閱消息。
這時候Cron表達式可以這樣寫,意思是每天早上9點到11點每隔40s執(zhí)行一次云函數(shù):
0/40 * 9-11 * * * *
當(dāng)然這里的周期設(shè)置可以結(jié)合云函數(shù)實際執(zhí)行的時間來定,要充分考慮到云函數(shù)的超時時間。
云調(diào)用還支持組合模板并添加至帳號下的個人模板庫的接口
subscribeMessage.addTemplate
、刪除帳號下的個人模板subscribeMessage.deleteTemplate
、獲取小程序賬號的類目subscribeMessage.getCategory
、獲取當(dāng)前帳號下的個人模板列表subscribeMessage.getTemplateList
等等接口,這里就不一一介紹啦。
更多建議: