# 订阅消息

# 介绍

消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验。

  • 订阅消息推送位置:服务通知
  • 订阅消息下发条件:用户自主订阅
  • 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面

intro

# 消息类型

1. 一次性订阅消息

一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。

2. 长期订阅消息

一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。

目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。

# 使用步骤

  • 步骤一:获取模板 ID

    在微信公众平台手动配置获取模板 ID: 登录 https://mp.weixin.qq.com (opens new window) 获取模板,如果没有合适的模板,可以申请添加新模板,审核通过后可使用。

    intro

  • 步骤二:获取下发权限

    详见小程序端消息订阅接口 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)),如下图:

image-20210812102933432

所以,需要自己定时维护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请求超限怎么办:

img

# 后台发送订阅消息

发送订阅消息:官方文档 (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来调整程序代码;
image-20210812111502555