# 订阅消息
# 介绍
消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验。
- 订阅消息推送位置:服务通知
- 订阅消息下发条件:用户自主订阅
- 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面
# 消息类型
1. 一次性订阅消息
一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。
2. 长期订阅消息
一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。
目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。
# 使用步骤
步骤一:获取模板 ID
在微信公众平台手动配置获取模板 ID: 登录 https://mp.weixin.qq.com (opens new window) 获取模板,如果没有合适的模板,可以申请添加新模板,审核通过后可使用。
步骤二:获取下发权限
详见小程序端消息订阅接口 wx.requestSubscribeMessage (opens new window)
步骤三:调用接口下发订阅消息
详见服务端消息发送接口 subscribeMessage.send (opens new window)
注意事项:用户勾选 “总是保持以上选择,不再询问” 之后,下次订阅调用 wx.requestSubscribeMessage 不会弹窗,保持之前的选择,修改选择需要打开小程序设置进行修改。
# 封装订阅消息工具js
作用:
- 发起订阅请求;
- 判断用户是否开启了通知;
- 提示未开启通知的用户,并跳转设置页面;
export default (arr, cb, mute = false) => {
uni.requestSubscribeMessage({
tmplIds: arr.length > 3 ? arr.splice(0, 3) : arr,
// 在调试工具中,无论订阅成功还是取消,都可以在complete取到状态
// 调试工具中,无法直接测试关闭订阅的状态
// 在真机上,可以获取用户拒绝订阅的状态
// 而且在complete部分可以获取到success/fail的回调内容
complete: (res) => {
// 2.1 如果用户未订阅,并未拒绝,正常发起订阅
// 2.2 如果用户拒绝了订阅,需要给用户一个轻提示 -> 手动打开订阅消息的
// wx.openSetting
if (arr.includes(item => res[item] === 'reject') || res.errCode === 20004) {
uni.showModal({
title: '您关闭了订阅通知',
content: '需要打开设置进行手动设置吗?',
success: function (res) {
if (res.confirm) {
uni.openSetting()
} else if (res.cancel) {
uni.showToast({
icon: 'error',
title: '您取消了订阅',
duration: 2000
})
}
}
})
} else if (!arr.some(item => res[item] === 'reject')) {
!mute && uni.showToast({
icon: 'none',
title: '您已经订阅了该消息',
duration: 1500
})
} else if (res.errCode === 10002 || res.errCode === 10003) {
uni.showToast({
title: '网络问题订阅失败,请重新订阅',
duration: 1500
})
} else {
// 其他的逻辑 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/subscribe-message/wx.requestSubscribeMessage.html
}
cb && cb()
}
})
}
# 前端主动订阅
在App.vue中添加判断用户订阅逻辑:
export default {
onLaunch: function () {
// console.warn('当前组件仅支持 uni_modules 目录结构 ,请升级 HBuilderX 到 3.1.0 版本以上!')
// console.log('App Launch')
uni.getSetting({
withSubscriptions: true,
success: async (res) => {
const app = getApp()
app.globalData.subscriptionsSetting = res.subscriptionsSetting
const arr = [
'S7zrpjN9Kq05-4ZG_nlTAYxnARMLWlSW09h54A2JCZo',
'ANN2-LhDgrhdFjs7jHOLdTnaxWpQU1LqS3kDIMF9GDs',
'FSQZganmBgaRRoNNlelQ1Qm2u4gx6pVSt69EJfkLbPA',
'g9FFU43_deHRuez-2FcrASorTSITsJJPYx-GhzvHEIU'
]
// 1. 获取用户已经订阅的消息
const { itemSettings: keys, mainSwitch } = res.subscriptionsSetting
// 相当于用户未打开订阅开关
if (!mainSwitch) {
return
}
// 用户开启订阅消息 -> 如果未设置任何消息
if (!keys) {
app.globalData.tmplIds = arr
} else {
// 用户开启了订阅消息 -> 已经有部分订阅 -> reject, accept
const keysArr = Object.keys(keys)
app.globalData.tmplIds = arr.filter((item) => keysArr.indexOf(item) === -1)
}
}
})
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
}
}
在需要订阅的位置,加入订阅逻辑,比如在个人中心中,点击去登录时:
import sub from '@/common/utils/subscribe'
// ...
const app = getApp()
const tmplIds = app.globalData.tmplIds || []
sub(tmplIds.splice(0, 3), () => {
if (!this.isLogin) {
uni.navigateTo({
url: '/subcom-pkg/auth/auth'
})
}
}, true)
注意:订阅消息一次最多只可以订阅3条,所以需要加入一个splice方法截断未订阅的消息。
也可以自己在指定的业务部分,给
sub
方法,传递指定的模板ID。
# 订阅消息模板API
应用场景:
- 后台控制哪些消息可以被订阅;
- 当模板消息的id发生了变化时,如果保证订阅的准确性;
前端请求模板API:
// 获取模板id
const getSubIds = () => axios.get('/public/subids')
调整App.vue中的模板IDs的判断逻辑,处理:
// 模板ID
// 改装成一个API接口 -> key:value -> value, key -> 业务场景
const { code, data } = await this.$u.api.getSubIds()
let arr
if (code === 200) {
// {key: value, key1: value1} => [value, value1]
arr = Object.entries(data).map(o => o[1])
} else {
// 默认的前端模板数据
arr = [
'S7zrpjN9Kq05-4ZG_nlTAYxnARMLWlSW09h54A2JCZo',
'ANN2-LhDgrhdFjs7jHOLdTnaxWpQU1LqS3kDIMF9GDs',
'FSQZganmBgaRRoNNlelQ1Qm2u4gx6pVSt69EJfkLbPA',
'g9FFU43_deHRuez-2FcrASorTSITsJJPYx-GhzvHEIU'
]
}
后端创建路由与接口:
路由:
// 获取微信模板id
router.get('/subids', publicController.getSubIds)
接口:
async getSubIds (ctx) {
ctx.body = {
code: 200,
data: config.subIds
}
}
# 维护accessToken
后端发送订阅消息需要accessToken:
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
见官方文档:链接 (opens new window)
由于acessToken的有效时间是2小时,而且每天微信请求的限制为2000次(链接 (opens new window)),如下图:

所以,需要自己定时维护accessToken:
创建
wxGetAccessToken
方法:import log4js from '@/config/Log4j' const logger = log4js.getLogger('error') import { getValue, setValue } from '@/config/RedisConfig' // flag 强制刷新,默认false - 不强制刷新 export const wxGetAccessToken = async (flag = false) => { // https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET // 1.判断redis中是否有accessToken // 2.有 & flag -> 则直接返回 // 3.没有 -> 请求新的token let accessToken = await getValue('accessToken') if (!accessToken || flag) { try { const result = await instance.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.AppID}&secret=${config.AppSecret}`) // console.log('🚀 ~ file: WxUtils.js ~ line 60 ~ wxGetAccessToken ~ result', result) if (result.status === 200) { await setValue('accessToken', result.data.access_token, result.data.expires_in) accessToken = result.data.access_token if (result.data.errcode && result.data.errmsg) { logger.error(`wxGetAccessToken error${result.data.errcode} - ${result.data.errmsg}`) } } } catch (error) { logger.error(`wxGetAccessToken error: ${error.message}`) } } return accessToken }
安装依赖
npm i cron
配置定时任务:
// 每隔7200秒执行一次 刷新accessToken import { CronJob } from 'cron' import { wxGetAccessToken } from './WxUtils' // Seconds: 0-59 // Minutes: 0-59 // Hours: 0-23 // Day of Month: 1-31 // Months: 0-11 (Jan-Dec) // Day of Week: 0-6 (Sun-Sat) const job = new CronJob('* * */1 * * *', () => { wxGetAccessToken(true) }) job.start()
在入口文件处理,添加该文件:
import './common/Cron'
常见问题,如果不小心accessToken请求超限怎么办:
# 后台发送订阅消息
发送订阅消息:官方文档 (opens new window)
调用方式:
# HTTPS调用接口说明
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
请求参数:
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
access_token / cloudbase_access_token | string | 是 | 接口调用凭证 (opens new window) | |
touser | string | 是 | 接收者(用户)的 openid | |
template_id | string | 是 | 所需下发的订阅模板id | |
page | string | 否 | 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 | |
data | Object | 是 | 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } | |
miniprogram_state | string | 否 | 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 | |
lang | string | 否 | 进入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN |
说明一下关于data
:
例如,模板的内容为
姓名: {{name01.DATA}}
金额: {{amount01.DATA}}
行程: {{thing01.DATA}}
日期: {{date01.DATA}}
则对应的json为
{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"page": "index",
"data": {
"name01": {
"value": "某某"
},
"amount01": {
"value": "¥100"
},
"thing01": {
"value": "广州至北京"
} ,
"date01": {
"value": "2018-01-01"
}
}
}
这里的data有内容限制(举例如下表),更多查看官方文档详情:
参数类别 | 参数说明 | 参数值限制 | 说明 |
---|---|---|---|
thing.DATA | 事物 | 20个以内字符 | 可汉字、数字、字母或符号组合 |
number.DATA | 数字 | 32位以内数字 | 只能数字,可带小数 |
letter.DATA | 字母 | 32位以内字母 | 只能字母 |
symbol.DATA | 符号 | 5位以内符号 | 只能符号 |
character_string.DATA | 字符串 | 32位以内数字、字母或符号 | 可数字、字母或符号组合 |
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位以内纯数字,可带小数,结尾可带“元” | 可带小数 |
phone_number.DATA | 电话 | 17位以内,数字、符号 | 电话号码,例:+86-0766-66888866 |
car_number.DATA | 车牌 | 8位以内,第一位与最后一位可为汉字,其余为字母或数字 | 车牌号码:粤A8Z888挂 |
name.DATA | 姓名 | 10个以内纯汉字或20个以内纯字母或符号 | 中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内 |
phrase.DATA | 汉字 | 5个以内汉字 | 5个以内纯汉字,例如:配送中 |
返回的 JSON 数据包
属性 | 类型 | 说明 |
---|---|---|
errcode | number | 错误码 |
errmsg | string | 错误信息 |
errcode 的合法值
值 | 说明 |
---|---|
40003 | touser字段openid为空或者不正确 |
40037 | 订阅模板id为空不正确 |
43101 | 用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系 |
47003 | 模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错 |
41030 | page路径不正确,需要保证在现网版本小程序中存在,与app.json保持一致 |
# 创建发送消息工具js
export const wxSendMessage = async (options) => {
// POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
let accessToken = await wxGetAccessToken()
try {
const { data } = await instance.post(`https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`, options)
return data
} catch (error) {
logger.error(`wxSendMessage error: ${error.message}`)
}
}
举例:
在用户登录之后,发送订阅消息通知:
// 推送消息
// 字段限制:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
const notify = await wxSendMessage({
touser: tmpUser.openid,
template_id: 'FSQZganmBgaRRoNNlelQ1Qm2u4gx6pVSt69EJfkLbPA',
data: {
phrase1: {
value: '登录安全'
},
date2: {
value: moment().format('YYYY年MM月DD HH:mm')
},
thing4: {
value: '通过微信授权登录成功,请注意信息安全'
}
},
miniprogram_state: process.env.NODE_ENV === 'development' ? 'developer' : 'formal'
})
# 前后端联调
联调需要注意的点:
- 设置
miniprogram_state
属性; - 使用真机进行调试 -> 配置局域网的访问的IP -> 让电脑与手机处于同一个网段;
- 检查发送订阅消息的数据格式、模板id、用户openid数据是否正确;
- 使用单步调试:发送订阅消息的接口,查看返回的errcode如果是0,说明发送成功;如果失败,则根据errcode来调整程序代码;
