微信小程序:模板消息推送實現

模板消息是基於微信的通知渠道,爲開發者提供了能夠高效觸達用戶的模板消息能力,以便實現服務的閉環並提供更佳的體驗。javascript

想推送模板消息,得知足一些前提條件:java

  1. 用戶在小程序中完成支付後,小程序能夠向用戶發送模板消息。
  2. 用戶在小程序中有提交表單的行爲,小程序能夠向用戶發送模板消息。

例如:web

  1. 用戶在小程序裏購買了商品,小程序能夠將商品物流的狀況,實時發送給用戶。
  2. 用戶在小程序裏填寫了活動報名表後,小程序能夠將報名狀況(成功或失敗)推送給用戶。
須要注意的是,即便條件達成了,小程序也不能無限制地發送模板消息。(貌似一次支付能夠推送五次消息通知)

具體的發送數量限制是:spring

  1. 用戶完成一次支付,小程序能夠得到 3 次發送模板消息的機會。
  2. 用戶提交一次表單,小程序能夠得到 1 次發送模板消息的機會。
  3. 發送模板消息的機會在用戶完成操做後的 7 天內有效。一旦超過 7 天,這些發送資格將會自動失效。

前置準備工做

內網穿透(須要支持80端口、綁定已備案域名SSL證書)用於開發時調試後端接口。

源碼中已提供該工具

註冊小程序帳號,同時申請或定製對應的模板消息,拿到模板ID和模板結構備用。

https://mp.weixin.qq.com/wxop...


能夠選擇自行定製模板消息格式,可是最終須要微信審覈後方可以使用,這裏咱們測試,就隨意在模板庫中挑選了一款,最終獲得模板消息格式以下:
json

購買地點 {{keyword1.DATA}}
購買時間 {{keyword2.DATA}}
物品名稱 {{keyword3.DATA}}
交易單號 {{keyword4.DATA}}

配置可信服務器域名

此處的可信域名,最終爲內網穿透映射的域名,用於小程序向本地後端接口發送HTTP請求。小程序

相關的微信API

獲取AccessToken [GET]

參數 是否必須 說明
grant_type 獲取access_token填寫client_credential
appid 第三方用戶惟一憑證
secret 第三方用戶惟一憑證密鑰,即appsecret

正常狀況下,微信會返回下述JSON數據包給公衆號:segmentfault

{"access_token":"ACCESS_TOKEN","expires_in":7200}

登陸憑證校驗: 根據js_code換取當前用戶的openId [GET]

先經過小程序獲取當前用戶的js_code,再調用相關接口接口換取openId後端

wx.login(OBJECT)api

調用接口wx.login() 獲取臨時登陸憑證(js_code)服務器

wx.login({
  success: function(res) { if (res.code) { // 獲取到js_code, 可繼續調用接口換取openId } else { console.log('登陸失敗!' + res.errMsg) } } });

https://api.weixin.qq.com/sns...{}&secret={}&js_code={}&grant_type=authorization_code

參數 是否必須 說明
appid 小程序惟一標識
secret 小程序的 app secret
js_code 登陸時獲取的 code
grant_type 填寫爲 authorization_code
//正常返回的JSON數據包
{
    "openid": "OPENID", "session_key": "SESSIONKEY", } //知足UnionID返回條件時,返回的JSON數據包 { "openid": "OPENID", "session_key": "SESSIONKEY", "unionid": "UNIONID" } //錯誤時返回JSON數據包(示例爲Code無效) { "errcode": 40029, "errmsg": "invalid code" }

發送模板消息 [POST]

https://api.weixin.qq.com/cgi...

參數 是否必須 說明
touser 接收者(用戶)的 openid
template_id 所需下發的模板消息的id
page 點擊模板卡片後的跳轉頁面,僅限本小程序內的頁面。支持帶參數,(示例index?foo=bar)。該字段不填則模板無跳轉。
form_id 表單提交場景下,爲 submit 事件帶上的 formId;支付場景下,爲本次支付的 prepay_id
data 模板內容,不填則下發空模板
emphasis_keyword 模板須要放大的關鍵詞,不填則默認無放大

請求示例:

{
  "touser": "OPENID", "template_id": "TEMPLATE_ID", "page": "index", "form_id": "FORMID", "data": { "keyword1": { "value": "339208499" }, "keyword2": { "value": "2015年01月05日 12:30" }, "keyword3": { "value": "粵海喜來登酒店" } , "keyword4": { "value": "廣州市天河區天河路208號" } }, "emphasis_keyword": "keyword1.DATA" }

代碼實現

注意:下面的代碼均爲測試代碼,未考慮嚴謹性,僅爲實現功能。

小程序端

<!--index.wxml-->
<view class="container"> <view class="userinfo"> <button wx:if="{{!hasUserInfo || !hasOpenId}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo" type='primary' size='mini'>獲取用戶信息</button> <block wx:else> <image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname">{{userInfo.nickName}}</text> <text class="userinfo-nickname">{{openId}}</text> </block> </view> <view wx:if="{{hasUserInfo && hasOpenId}}" class='usermotto'> <form bindsubmit="templateSend" report-submit="true"> <button type='primary' formType="submit" size='mini'>發送模板消息</button> </form> </view> <view wx:if="{{logMessage}}"> <p style="color:red"><span>{{logMessage}}</span></p> </view> </view>

須要注意的是,這裏的表單須要加上report-submit="true"屬性,標識該屬性表示能夠得到一次formId的機會,該formId能夠用來推送模板消息,下面是控制器相關的代碼:

//index.js //獲取應用實例 const app = getApp(); const requestHost = "https://wuwz.guyubao.com/wx_small_app"; Page({ data: { userInfo: {}, openId: null, hasUserInfo: false, hasOpenId: false, logMessage: null }, getUserInfo: function(e) { app.globalData.userInfo = e.detail.userInfo this.setData({ userInfo: e.detail.userInfo, hasUserInfo: true, logMessage: '加載用戶信息中..' }) this.getOpenId(); }, getOpenId: function() { var _this = this; wx.login({ success: function(res) { if (res.code) { // 換取openid wx.request({ url: requestHost + "/get_openid_by_js_code", data: { js_code: res.code }, method: 'GET', success: function(res) { if (res.data.openid) { _this.setData({ openId: res.data.openid, hasOpenId: true, logMessage: '加載用戶信息完成' }); } }, fail: function (err) { _this.setData({ logMessage: '[fail]' + JSON.stringify(err) }); } }); } } }) }, templateSend: function(e) { var _this = this; var openId = _this.data.openId; // 表單需設置report-submit="true" var formId = e.detail.formId; if (!formId || 'the formId is a mock one' === formId) { _this.setData({ logMessage: '[fail]請使用真機調試,不然獲取不到formId' }); return; } // 發送隨機模板消息 wx.request({ url: requestHost + "/template_send", data: { openId: openId, formId: formId }, method: 'POST', success: function(res) { if (res.data.status === 0) { _this.setData({ logMessage: '發送模板消息成功[' + new Date().getTime()+']' }); } }, fail: function(err) { _this.setData({ logMessage: '[fail]' + JSON.stringify(err) }); } }); } })

後端接口

先針對須要使用的微信API作一個簡單的封裝:

package com.wuwenze.wechatsmallapptmplmsg.wechat; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; /** * @author wwz * @version 1 (2018/8/20) * @since Java7 */ @Slf4j public class WechatApi { private final static LoadingCache<String, String> mAccessTokenCache = CacheBuilder.newBuilder() .expireAfterWrite(7200, TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String key) { // key: appId#appSecret String[] array = key.split("#"); if (null == array || array.length != 2) { throw new IllegalArgumentException("load access_token error, key = " + key); } return getAccessToken(array[0], array[1]); } }); public static String getAccessToken() { String cacheKey = WechatConf.appId + "#" + WechatConf.appSecrct; try { return mAccessTokenCache.get(cacheKey); } catch (ExecutionException e) { log.error("#getAccessToken error, cacheKey=" + cacheKey, e); } return null; } private static String getAccessToken(String appId, String appSecret) { String apiUrl = StrUtil.format(// "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}",// appId, appSecret ); String body = HttpRequest.get(apiUrl).execute().body(); return throwErrorMessageIfExists(body).getString("access_token"); } public static void templateSend(String accessToken, WechatTemplate template) { String apiUrl = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token="// + (StrUtil.isEmpty(accessToken) ? getAccessToken() : accessToken); String body = HttpRequest.post(apiUrl).body(JSON.toJSONString(template)).execute().body(); throwErrorMessageIfExists(body); } public static JSONObject getOpenIdByJSCode(String js_code) { String apiUrl = StrUtil.format(// "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",// WechatConf.appId, WechatConf.appSecrct, js_code ); String body = HttpRequest.get(apiUrl).execute().body(); return throwErrorMessageIfExists(body); } private static JSONObject throwErrorMessageIfExists(String body) { String callMethodName = (new Throwable()).getStackTrace()[1].getMethodName(); log.info("#0820 {} body={}", callMethodName, body); JSONObject jsonObject = JSON.parseObject(body); if (jsonObject.containsKey("errcode") && jsonObject.getIntValue("errcode") > 0) { throw new RuntimeException(StrUtil.format("#WechatApi[{}] call error: {}", callMethodName, body)); } return jsonObject; } }

對外開放相關的接口:

package com.wuwenze.wechatsmallapptmplmsg.controller; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.wuwenze.wechatsmallapptmplmsg.util.MapUtil; import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatApi; import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatConf; import com.wuwenze.wechatsmallapptmplmsg.util.SecurityUtil; import com.wuwenze.wechatsmallapptmplmsg.util.WebUtil; import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatTemplate; import com.wuwenze.wechatsmallapptmplmsg.wechat.WechatTemplateItem; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; import java.util.stream.Stream; /** * @author wwz * @version 1 (2018/8/16) * @since Java7 */ @Slf4j @RestController @RequestMapping("/wx_small_app") public class WechatController { @GetMapping("/get_openid_by_js_code") public Map<String, Object> getOpenIdByJSCode(String js_code) { return WechatApi.getOpenIdByJSCode(js_code); } @PostMapping("/template_send") public Map<String, Object> templateSend() { String accessToken = WechatApi.getAccessToken(); JSONObject body = JSON.parseObject(WebUtil.getBody()); // 填充模板數據 (測試代碼,寫死) WechatTemplate wechatTemplate = new WechatTemplate() .setTouser(body.getString("openId")) .setTemplate_id(WechatConf.templateId) // 表單提交場景下爲formid,支付場景下爲prepay_id .setForm_id(body.getString("formId")) // 跳轉頁面 .setPage("index") /** * 模板內容填充:隨機字符 * 購買地點 {{keyword1.DATA}} * 購買時間 {{keyword2.DATA}} * 物品名稱 {{keyword3.DATA}} * 交易單號 {{keyword4.DATA}} * -> {"keyword1": {"value":"xxx"}, "keyword2": ...} */ .setData(MapUtil.newHashMap(// "keyword1", new WechatTemplateItem(RandomUtil.randomString(10)),// "keyword2", new WechatTemplateItem(DateUtil.now()),// "keyword3", new WechatTemplateItem(RandomUtil.randomString(10)),// "keyword4", new WechatTemplateItem(RandomUtil.randomNumbers(10)) // )); WechatApi.templateSend(accessToken, wechatTemplate); return MapUtil.newHashMap("status", 0); } @GetMapping("/validate") public void validate(String signature, String timestamp, String nonce, String echostr) { final StringBuilder attrs = new StringBuilder(); Stream.of(WechatConf.token, timestamp, nonce)// .sorted()// .forEach((item) -> attrs.append(item)); String sha1 = SecurityUtil.getSha1(attrs.toString()); if (StrUtil.equalsIgnoreCase(sha1, signature)) { WebUtil.write(echostr); return; } log.error("#0820 WechatController.validate() error, attrs = {}", attrs); } }

其餘:突破發送模板消息的限制

如非必要,儘可能不要這樣作,一旦發現小程序濫用模板消息,微信是有權進行封禁的。

簡單來講,咱們能夠將小程序的表單組件進行封裝,假裝小程序中其餘功能按鈕。當用戶點擊按鈕時,表單組件就自動把formId上傳給服務器保存(7天后過時),當收集到必定的用戶點擊事件後,就能夠拿來使用了(主動消息推送羣發),哈哈哈。

 

原文地址:https://segmentfault.com/a/1190000016183735?utm_source=tag-newest

相關文章
相關標籤/搜索