前言
受眾:已有小程序和云開(kāi)發(fā)經(jīng)驗(yàn)(沒(méi)有的話照著流程和官方文檔也應(yīng)該可以實(shí)現(xiàn))
關(guān)于小程序的消息推送,我了解到的有以下幾種實(shí)現(xiàn)方式
1、模板消息,已于2020 年 1 月 10 日下線
2、通過(guò)服務(wù)端的統(tǒng)一服務(wù)消息下發(fā)推送,因?yàn)槟0逑F(xiàn)已下線,現(xiàn)只支持公眾號(hào)。統(tǒng)一服務(wù)消息官方文檔
2、通過(guò)關(guān)注公眾號(hào)通過(guò)公眾號(hào)實(shí)現(xiàn)長(zhǎng)期的消息推送
3、訂閱消息,包含一次性訂閱消息和長(zhǎng)期訂閱消息
訂閱消息官方文檔
關(guān)于技術(shù)實(shí)現(xiàn)的選擇
關(guān)于小程序的消息推送的幾種實(shí)現(xiàn)方式,先簡(jiǎn)單說(shuō)一下各自的優(yōu)缺點(diǎn):
1、統(tǒng)一服務(wù)消息
優(yōu)點(diǎn):可以長(zhǎng)期多次發(fā)送
缺點(diǎn):因?yàn)槟0逑F(xiàn)已下線,現(xiàn)只支持公眾號(hào)
2、通過(guò)公眾號(hào)實(shí)現(xiàn)
優(yōu)點(diǎn):可以長(zhǎng)期多次發(fā)送
缺點(diǎn):需要引導(dǎo)關(guān)注公眾號(hào),沒(méi)有公眾號(hào)還得注冊(cè)一個(gè),以下還有一些注意事項(xiàng)
1、公眾號(hào)和小程序需要在同一個(gè)微信開(kāi)放平臺(tái)下,保證拿到相同的UnionID
2、如果需要在消息模板上加上小程序的入口,需要微信公眾號(hào)和小程序做關(guān)聯(lián)
3、小程序和公眾號(hào)都必須是認(rèn)證過(guò)的
4、小程序需要提前知道公眾號(hào)的appid和appsecret
5、發(fā)送消息之前需要拿到用戶對(duì)應(yīng)于公眾號(hào)的openid
3、訂閱消息實(shí)現(xiàn)
訂閱消息,包含一次性訂閱消息和長(zhǎng)期訂閱消息,可惜
長(zhǎng)期訂閱消息只對(duì)指定類目開(kāi)放
一次性訂閱消息
優(yōu)點(diǎn):服務(wù)端,云開(kāi)發(fā)都可以實(shí)現(xiàn)推送
缺點(diǎn):每次需要授權(quán),每次授權(quán)同意只推送一次
哎,沒(méi)什么可挑的,
最終選擇的是訂閱消息的一次性訂閱消息,下面是微信官方介紹
小程序訂閱消息
功能介紹
消息能力是小程序能力中的重要組成,我們?yōu)殚_(kāi)發(fā)者提供了訂閱消息能力,以便實(shí)現(xiàn)服務(wù)的閉環(huán)和更優(yōu)的體驗(yàn)。
- 訂閱消息推送位置:服務(wù)通知
- 訂閱消息下發(fā)條件:用戶自主訂閱
-
訂閱消息卡片跳轉(zhuǎn)能力:點(diǎn)擊查看詳情可跳轉(zhuǎn)至該小程序的頁(yè)面
image.png
消息類型
1. 一次性訂閱消息
一次性訂閱消息用于解決用戶使用小程序后,后續(xù)服務(wù)環(huán)節(jié)的通知問(wèn)題。用戶自主訂閱后,開(kāi)發(fā)者可不限時(shí)間地下發(fā)一條對(duì)應(yīng)的服務(wù)消息;每條消息可單獨(dú)訂閱或退訂。
2. 長(zhǎng)期訂閱消息
一次性訂閱消息可滿足小程序的大部分服務(wù)場(chǎng)景需求,但線下公共服務(wù)領(lǐng)域存在一次性訂閱無(wú)法滿足的場(chǎng)景,如航班延誤,需根據(jù)航班實(shí)時(shí)動(dòng)態(tài)來(lái)多次發(fā)送消息提醒。為便于服務(wù),我們提供了長(zhǎng)期性訂閱消息,用戶訂閱一次后,開(kāi)發(fā)者可長(zhǎng)期下發(fā)多條消息。
目前長(zhǎng)期性訂閱消息僅向政務(wù)民生、醫(yī)療、交通、金融、教育等線下公共服務(wù)開(kāi)放,后期將逐步支持到其他線下公共服務(wù)業(yè)務(wù)。
3. 設(shè)備訂閱消息
設(shè)備訂閱消息是一種特殊類型的訂閱消息,它屬于長(zhǎng)期訂閱消息類型,且需要完成「設(shè)備接入」才能使用。
設(shè)備訂閱消息用于在設(shè)備觸發(fā)某些需要人工介入的事件時(shí)(例如設(shè)備發(fā)生故障、設(shè)備耗材不足等),向用戶發(fā)送消息通知。詳見(jiàn)設(shè)備訂閱消息文檔。
使用說(shuō)明
步驟一:獲取模板 ID
在微信公眾平臺(tái)手動(dòng)配置獲取模板 ID:
登錄 https://mp.weixin.qq.com 獲取模板,如果沒(méi)有合適的模板,可以申請(qǐng)?zhí)砑有履0澹瑢徍送ㄟ^(guò)后可使用。

步驟二:獲取下發(fā)權(quán)限
一次性訂閱消息、長(zhǎng)期訂閱消息,詳見(jiàn)接口 wx.requestSubscribeMessage
設(shè)備訂閱消息,詳見(jiàn)接口 wx.requestSubscribeDeviceMessage
步驟三:調(diào)用接口下發(fā)訂閱消息
一次性訂閱消息、長(zhǎng)期訂閱消息,詳見(jiàn)服務(wù)端接口 subscribeMessage.send
設(shè)備訂閱消息,詳見(jiàn)服務(wù)端接口 hardwareDevice.send
注意事項(xiàng)
- 用戶勾選 “總是保持以上選擇,不再詢問(wèn)” 之后,下次訂閱調(diào)用 wx.requestSubscribeMessage 不會(huì)彈窗,保持之前的選擇,修改選擇需要打開(kāi)小程序設(shè)置進(jìn)行修改。
實(shí)現(xiàn)
設(shè)計(jì)思路
我這里的需求是紀(jì)念日的推送,到用戶設(shè)置的紀(jì)念日當(dāng)天或者前幾天,需要給用戶推送一條消息,提醒用戶紀(jì)念日已到。因?yàn)槭且淮涡允跈?quán),首先要考慮授權(quán)的時(shí)機(jī),因此考慮是在用戶新增或者編輯紀(jì)念日信息那里,設(shè)置一個(gè)開(kāi)關(guān),開(kāi)啟時(shí)判斷通知權(quán)限,無(wú)權(quán)限提醒到設(shè)置頁(yè)開(kāi)啟通知,有權(quán)限請(qǐng)求授權(quán),授權(quán)成功開(kāi)關(guān)開(kāi)啟,失敗或者未開(kāi)啟通知權(quán)限開(kāi)關(guān)關(guān)閉,最后提交信息到數(shù)據(jù)庫(kù),推送的接口會(huì)每天輪詢一次,根據(jù)這個(gè)狀態(tài)和是否到紀(jì)念日提醒時(shí)間判斷是否進(jìn)行推送,推送成功重置狀態(tài)。推送之后用戶再次開(kāi)啟推送開(kāi)關(guān)可實(shí)現(xiàn)下一次推送,形成一個(gè)閉環(huán)。

實(shí)現(xiàn)流程
- 首先要去小程序后臺(tái)選擇一個(gè)模板
- 然后在新增編輯頁(yè)面實(shí)現(xiàn)設(shè)計(jì)的邏輯(包含通知權(quán)限的判斷,請(qǐng)求一次性訂閱,失敗關(guān)閉開(kāi)關(guān)狀態(tài),保存數(shù)據(jù)到數(shù)據(jù)庫(kù))
- 云開(kāi)發(fā)輪詢推送消息(先查出需要推送的數(shù)據(jù),然后對(duì)應(yīng)模板的數(shù)據(jù)格式發(fā)送推送)
具體實(shí)現(xiàn)
1、選擇模板
登錄小程序開(kāi)發(fā)后臺(tái) - 訂閱消息 - 公共模板庫(kù)搜索選擇一個(gè)合適的模板,然后在我的模板那里可以查看模板詳情,
模板id和詳情里的字段在云開(kāi)發(fā)那里需要使用



2、編輯頁(yè)代碼實(shí)現(xiàn)
首先先把開(kāi)關(guān)相關(guān)的頁(yè)面和邏輯實(shí)現(xiàn),這里就不細(xì)說(shuō)。
然后是判斷通知權(quán)限和請(qǐng)求一次性訂閱
2.1、判斷通知權(quán)限
判斷通知權(quán)限使用的是wx.getSetting
wx.getSetting({
withSubscriptions: true,
success (res) {
console.log(res.authSetting)
// res.authSetting = {
// "scope.userInfo": true,
// "scope.userLocation": true
// }
console.log(res.subscriptionsSetting)
// res.subscriptionsSetting = {
// mainSwitch: true, // 訂閱消息總開(kāi)關(guān)
// itemSettings: { // 每一項(xiàng)開(kāi)關(guān)
// SYS_MSG_TYPE_INTERACTIVE: 'accept', // 小游戲系統(tǒng)訂閱消息
// SYS_MSG_TYPE_RANK: 'accept'
// zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: 'reject', // 普通一次性訂閱消息
// ke_OZC_66gZxALLcsuI7ilCJSP2OJ2vWo2ooUPpkWrw: 'ban',
// }
// }
}
})
2.2、請(qǐng)求一次性訂閱
請(qǐng)求一次性訂閱使用的是wx.requestSubscribeMessage
wx.requestSubscribeMessage({
tmplIds: [''], // 模板id
success (res) { }
})
2.3、把數(shù)據(jù)存儲(chǔ)到云數(shù)據(jù)庫(kù)
每個(gè)項(xiàng)目數(shù)據(jù)結(jié)構(gòu)不一樣,這里也不展開(kāi)說(shuō)
核心代碼
// 是否提醒
onSwitchChange: function (event) {
this.setData({
["dict.isPush"]: event.detail.value
})
if (!event.detail.value) {
this.setData({
["dict.pushTime"]: ''
})
}
if (event.detail.value) {
this.checkAndRequestSubscribeMessage()
}
},
// 檢查訂閱消息權(quán)限,未開(kāi)啟提示前往開(kāi)啟,已開(kāi)啟請(qǐng)求訂閱消息
checkAndRequestSubscribeMessage() {
let that = this
wx.getSetting({
withSubscriptions: true,
success(res) {
console.log(res.subscriptionsSetting)
// 訂閱消息總開(kāi)關(guān)是否開(kāi)啟
if (!res.subscriptionsSetting.mainSwitch) {
that.subscriptionFailed()
wx.showModal({
title: '提示',
content: '當(dāng)前暫未開(kāi)啟接消息提醒,是否前往設(shè)置頁(yè)開(kāi)啟?',
success(res) {
if (res.confirm) {
wx.openSetting()
}
}
})
} else {
let templateId = 'c64Gp5-89xyD55rnDr0oBWQNphWlNm_l4MX-Sduuj2c' // 模板ID
wx.requestSubscribeMessage({
tmplIds: [templateId],
success(res) {
console.log(res)
// 申請(qǐng)訂閱成功,將訂閱信息調(diào)用云函數(shù)存入云開(kāi)發(fā)數(shù)據(jù)
if (res.errMsg === 'requestSubscribeMessage:ok') {
// res[templateId]: 'accept'、'reject'、'ban'、'filter'
if (res[templateId] == 'accept') {} else {
that.subscriptionFailed()
}
}
},
fail(err) {
console.log(err)
that.subscriptionFailed()
wx.showToast({
title: '訂閱失敗',
icon: 'none'
})
}
})
}
}
})
},
// 訂閱失敗
subscriptionFailed() {
this.setData({
["dict.isPush"]: false
})
},
3、云開(kāi)發(fā)輪詢推送
這里才是重點(diǎn)?。?!首先在項(xiàng)目中選擇云函數(shù)目錄右鍵新建一個(gè)云函數(shù),然后就需要在這個(gè)云函數(shù)中實(shí)現(xiàn)輪詢推送的代碼了,我這里建的云函數(shù)是
push

3.1、推送
推送使用的是服務(wù)端的subscribeMessage.send方法。
這里是的是云調(diào)用進(jìn)行推送,這樣可以使用云開(kāi)發(fā)
subscribeMessage.send使用需要在云函數(shù)代碼的config.json文件中配置uniformMessage.sendAPI 的權(quán)限,詳情
配置如下
"permissions": {
"openapi": ["subscribeMessage.send"]
},

請(qǐng)求參數(shù)
| 屬性 | 類型 | 默認(rèn)值 | 必填 | 說(shuō)明 |
|---|---|---|---|---|
| touser | string | 是 | 接收者(用戶)的 openid | |
| templateId | string | 是 | 所需下發(fā)的訂閱模板id | |
| page | string | 否 | 點(diǎn)擊模板卡片后的跳轉(zhuǎn)頁(yè)面,僅限本小程序內(nèi)的頁(yè)面。支持帶參數(shù),(示例index?foo=bar)。該字段不填則模板無(wú)跳轉(zhuǎn)。 | |
| data | Object | 是 | 模板內(nèi)容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } | |
| miniprogramState | string | 否 | 跳轉(zhuǎn)小程序類型:developer為開(kāi)發(fā)版;trial為體驗(yàn)版;formal為正式版;默認(rèn)為正式版 | |
| lang | string | 否 | 進(jìn)入小程序查看”的語(yǔ)言類型,支持zh_CN(簡(jiǎn)體中文)、en_US(英文)、zh_HK(繁體中文)、zh_TW(繁體中文),默認(rèn)為zh_CN |
官方請(qǐng)求示例
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
exports.main = async (event, context) => {
try {
const result = await cloud.openapi.subscribeMessage.send({
"touser": 'OPENID',
"page": 'index',
"lang": 'zh_CN',
"data": {
"number01": {
"value": '339208499'
},
"date01": {
"value": '2015年01月05日'
},
"site01": {
"value": 'TIT創(chuàng)意園'
},
"site02": {
"value": '廣州市新港中路397號(hào)'
}
},
"templateId": 'TEMPLATE_ID',
"miniprogramState": 'developer'
})
return result
} catch (err) {
return err
}
}
miniprogramState在正式環(huán)境要換成formal或注釋掉這行
3.2、輪詢
輪詢使用的云開(kāi)發(fā)的定時(shí)觸發(fā)器

定時(shí)觸發(fā)器官方介紹
該功能需開(kāi)發(fā)者工具 1.02.1811270 及以上版本方可使用 從開(kāi)發(fā)者工具 1.02.1910182 開(kāi)始,新上傳的定時(shí)觸發(fā)器內(nèi)支持使用云調(diào)用
如果云函數(shù)需要定時(shí) / 定期執(zhí)行,也就是定時(shí)觸發(fā),我們可以使用云函數(shù)定時(shí)觸發(fā)器。配置了定時(shí)觸發(fā)器的云函數(shù),會(huì)在相應(yīng)時(shí)間點(diǎn)被自動(dòng)觸發(fā),函數(shù)的返回結(jié)果不會(huì)返回給調(diào)用方。
在需要添加觸發(fā)器的云函數(shù)目錄下新建文件
config.json,格式如下:
{
// triggers 字段是觸發(fā)器數(shù)組,目前僅支持一個(gè)觸發(fā)器,即數(shù)組只能填寫(xiě)一個(gè),不可添加多個(gè)
"triggers": [
{
// name: 觸發(fā)器的名字,規(guī)則見(jiàn)下方說(shuō)明
"name": "myTrigger",
// type: 觸發(fā)器類型,目前僅支持 timer (即 定時(shí)觸發(fā)器)
"type": "timer",
// config: 觸發(fā)器配置,在定時(shí)觸發(fā)器下,config 格式為 cron 表達(dá)式,規(guī)則見(jiàn)下方說(shuō)明
"config": "0 0 2 1 * * *"
}
]
}
官方示例
下面展示了一些 Cron 表達(dá)式和相關(guān)含義的示例:
- */5 * * * * * * 表示每5秒觸發(fā)一次
- 0 0 2 1 * * * 表示在每月的1日的凌晨2點(diǎn)觸發(fā)
- 0 15 10 * * MON-FRI * 表示在周一到周五每天上午10:15觸發(fā)
- 0 0 10,14,16 * * * * 表示在每天上午10點(diǎn),下午2點(diǎn),4點(diǎn)觸發(fā)
- 0 */30 9-17 * * * * 表示在每天上午9點(diǎn)到下午5點(diǎn)內(nèi)每半小時(shí)觸發(fā)
- 0 0 12 * * WED * 表示在每個(gè)星期三中午12點(diǎn)觸發(fā)
triggers中的config字段可以控制觸發(fā)的頻率,具體開(kāi)發(fā)測(cè)試時(shí)我使用的是50秒調(diào)用一次
"config": "*/50 * * * * * *"
這里有個(gè)坑,如果代碼實(shí)現(xiàn)上傳并部署云函數(shù)之后,左等右等在日志中看不到日志,因?yàn)樯倭艘粋€(gè)步驟,在上傳并部署云函數(shù)之后,需要右鍵云函數(shù)上傳觸發(fā)器,這樣才生效,想關(guān)閉可以刪除觸發(fā)器
輪詢推送完整代碼
config.json
{
"permissions": {
"openapi": ["subscribeMessage.send"]
},
"triggers": [{
"name": "myTimer",
"type": "timer",
"config": "0 0 8 * * * *"
}]
}
index.js
// 云函數(shù)入口文件
const cloud = require('wx-server-sdk')
// 初始化 cloud
cloud.init({
// API 調(diào)用都保持和云函數(shù)當(dāng)前所在環(huán)境一致
env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()
const _ = db.command
const $ = db.command.aggregate
const kTableName = '換成自己的表名'
// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {
try {
// 從云開(kāi)發(fā)數(shù)據(jù)庫(kù)中查詢等待發(fā)送的消息列表
const msgArr = await db
.collection(kTableName)
// 查詢條件,已開(kāi)啟推送,并且提醒時(shí)間為今天
.where({
A_IsPush: true,
A_PushTime: timeStampToTime(new Date().getTime(), '{y}/{m}/u0z1t8os')
})
.get()
// 循環(huán)消息列表
const sendPromises = msgArr.data.map(async msgData => {
try {
// 發(fā)送訂閱消息
await cloud.openapi.subscribeMessage.send({
touser: msgData._openid, // 要發(fā)送用戶的openid
page: 'pages/home/home', // 用戶通過(guò)消息通知點(diǎn)擊進(jìn)入小程序的頁(yè)面
lang: 'zh_CN',
templateId: 'c64Gp5-89xyD55rnDr0oBWQNphWlNm_l4MX-Sduuj2c', // 訂閱消息模板ID
// 跳轉(zhuǎn)小程序類型:developer為開(kāi)發(fā)版;trial為體驗(yàn)版;formal為正式版;默認(rèn)為正式版
// miniprogramState: 'developer',
// 要發(fā)送的數(shù)據(jù),要和模板一致
data: {
// 紀(jì)念日名稱
thing5: {
value: msgData.A_Title
},
// 紀(jì)念日時(shí)間
time2: {
value: msgData.A_Time
},
// 備注
thing4: {
value: msgData.A_Remarks ? msgData.A_Remarks : '無(wú)'
},
}
})
// 發(fā)送成功后將數(shù)據(jù)狀態(tài)重置
return db
.collection(kTableName)
.doc(msgData._id)
.update({
data: {
A_IsPush: false,
A_PushTime: '',
A_NextTime: '',
},
})
} catch (e) {
return e
}
})
return Promise.all(sendPromises)
} catch (err) {
console.log(err)
return err
}
}
function timeStampToTime(time, cFormat) {
if (arguments.length === 0) {
return null
}
const format = cFormat || '{y}-{m}-u0z1t8os {h}:{i}:{s}'
let date
if (typeof time === 'object') {} else {
if (('' + time).length === 10) time = parseInt(time) * 1000
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
w: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|w)+}/g, (result, key) => {
let value = formatObj[key]
if (key === 'w') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
// 定時(shí)觸發(fā)器
// https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html
// 50秒一次
// "config": "*/50 * * * * * *"
// 每天上午8點(diǎn)一次
// "config": "0 0 8 * * * *"
package.json
{
"name": "push",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"wx-server-sdk": "~2.6.1"
}
}
3.3、上傳并部署云函數(shù)
所有代碼實(shí)現(xiàn)之后,可以先把項(xiàng)目環(huán)境設(shè)置為開(kāi)發(fā)環(huán)境,然后右鍵上傳并部署云函數(shù),然后上傳觸發(fā)器

然后打開(kāi)云函數(shù)控制臺(tái),選擇云函數(shù)-日志,進(jìn)行查看狀態(tài),如果成功并且有需要推送的數(shù)據(jù),手機(jī)會(huì)收到推送消息,失敗的話根據(jù)日志進(jìn)行修改。

至此結(jié)束
最后推薦一下我的小程序,
我的紀(jì)念日小助手
