微信小程序 - 云開(kāi)發(fā)輪詢實(shí)現(xiàn)定時(shí)推送訂閱消息

前言

受眾:已有小程序和云開(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ò)后可使用。

image.png

步驟二:獲取下發(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)。

image.png

實(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ā)那里需要使用

image.png
image.png
image.png

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

image.png

3.1、推送

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

配置如下

  "permissions": {
    "openapi": ["subscribeMessage.send"]
  },
image.png

請(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ā)器

image.png
定時(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ā)器

image.png

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

image.png

至此結(jié)束

最后推薦一下我的小程序,我的紀(jì)念日小助手

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容