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