摘要:模板消息是基于微信的通知渠道,為開發(fā)者提供了可以高效觸達(dá)用戶的模板消息能力,以便實現(xiàn)服務(wù)的閉環(huán)并提供更佳的體驗。
模板消息是基于微信的通知渠道,為開發(fā)者提供了可以高效觸達(dá)用戶的模板消息能力,以便實現(xiàn)服務(wù)的閉環(huán)并提供更佳的體驗。
想推送模板消息,得滿足一些前提條件:
用戶在小程序中完成支付后,小程序可以向用戶發(fā)送模板消息。
用戶在小程序中有提交表單的行為,小程序可以向用戶發(fā)送模板消息。
例如:
用戶在小程序里購買了商品,小程序可以將商品物流的情況,實時發(fā)送給用戶。
用戶在小程序里填寫了活動報名表后,小程序可以將報名情況(成功或失敗)推送給用戶。
需要注意的是,即使條件達(dá)成了,小程序也不能無限制地發(fā)送模板消息。
具體的發(fā)送數(shù)量限制是:
用戶完成一次支付,小程序可以獲得 3 次發(fā)送模板消息的機(jī)會。
用戶提交一次表單,小程序可以獲得 1 次發(fā)送模板消息的機(jī)會。
發(fā)送模板消息的機(jī)會在用戶完成操作后的 7 天內(nèi)有效。一旦超過 7 天,這些發(fā)送資格將會自動失效。
前置準(zhǔn)備工作 內(nèi)網(wǎng)穿透(需要支持80端口、綁定已備案域名、SSL證書)用于開發(fā)時調(diào)試后端接口。源碼中已提供該工具注冊小程序賬號,同時申請或定制對應(yīng)的模板消息,拿到模板ID和模板結(jié)構(gòu)備用。
https://mp.weixin.qq.com/wxop...
可以選擇自行定制模板消息格式,但是最終需要微信審核后方可使用,這里我們測試,就隨意在模板庫中挑選了一款,最終得到模板消息格式如下:
購買地點(diǎn) {{keyword1.DATA}} 購買時間 {{keyword2.DATA}} 物品名稱 {{keyword3.DATA}} 交易單號 {{keyword4.DATA}}配置可信服務(wù)器域名
此處的可信域名,最終為內(nèi)網(wǎng)穿透映射的域名,用于小程序向本地后端接口發(fā)送HTTP請求。
https://api.weixin.qq.com/cgi...
參數(shù) | 是否必須 | 說明 |
---|---|---|
grant_type | 是 | 獲取access_token填寫client_credential |
appid | 是 | 第三方用戶唯一憑證 |
secret | 是 | 第三方用戶唯一憑證密鑰,即appsecret |
正常情況下,微信會返回下述JSON數(shù)據(jù)包給公眾號:
{"access_token":"ACCESS_TOKEN","expires_in":7200}登錄憑證校驗: 根據(jù)js_code換取當(dāng)前用戶的openId [GET]
先通過小程序獲取當(dāng)前用戶的js_code,再調(diào)用相關(guān)接口接口換取openId
wx.login(OBJECT)
調(diào)用接口wx.login() 獲取臨時登錄憑證(js_code)
wx.login({ success: function(res) { if (res.code) { // 獲取到j(luò)s_code, 可繼續(xù)調(diào)用接口換取openId } else { console.log("登錄失??!" + res.errMsg) } } });
https://api.weixin.qq.com/sns...{}&secret={}&js_code={}&grant_type=authorization_code
參數(shù) | 是否必須 | 說明 |
---|---|---|
appid | 是 | 小程序唯一標(biāo)識 |
secret | 是 | 小程序的 app secret |
js_code | 是 | 登錄時獲取的 code |
grant_type | 是 | 填寫為 authorization_code |
//正常返回的JSON數(shù)據(jù)包 { "openid": "OPENID", "session_key": "SESSIONKEY", } //滿足UnionID返回條件時,返回的JSON數(shù)據(jù)包 { "openid": "OPENID", "session_key": "SESSIONKEY", "unionid": "UNIONID" } //錯誤時返回JSON數(shù)據(jù)包(示例為Code無效) { "errcode": 40029, "errmsg": "invalid code" }發(fā)送模板消息 [POST]
https://api.weixin.qq.com/cgi...
參數(shù) | 是否必須 | 說明 |
---|---|---|
touser | 是 | 接收者(用戶)的 openid |
template_id | 是 | 所需下發(fā)的模板消息的id |
page | 否 | 點(diǎn)擊模板卡片后的跳轉(zhuǎn)頁面,僅限本小程序內(nèi)的頁面。支持帶參數(shù),(示例index?foo=bar)。該字段不填則模板無跳轉(zhuǎn)。 |
form_id | 是 | 表單提交場景下,為 submit 事件帶上的 formId;支付場景下,為本次支付的 prepay_id |
data | 是 | 模板內(nèi)容,不填則下發(fā)空模板 |
emphasis_keyword | 否 | 模板需要放大的關(guān)鍵詞,不填則默認(rèn)無放大 |
請求示例:
{ "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": "廣州市天河區(qū)天河路208號" } }, "emphasis_keyword": "keyword1.DATA" }代碼實現(xiàn)
注意:下面的代碼均為測試代碼,未考慮嚴(yán)謹(jǐn)性,僅為實現(xiàn)功能。小程序端
{{userInfo.nickName}} {{openId}} {{logMessage}}
需要注意的是,這里的表單需要加上report-submit="true"屬性,標(biāo)識該屬性表示可以獲得一次formId的機(jī)會,該formId可以用來推送模板消息,下面是控制器相關(guān)的代碼:
//index.js //獲取應(yīng)用實例 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; // 表單需設(shè)置report-submit="true" var formId = e.detail.formId; if (!formId || "the formId is a mock one" === formId) { _this.setData({ logMessage: "[fail]請使用真機(jī)調(diào)試,否則獲取不到formId" }); return; } // 發(fā)送隨機(jī)模板消息 wx.request({ url: requestHost + "/template_send", data: { openId: openId, formId: formId }, method: "POST", success: function(res) { if (res.data.status === 0) { _this.setData({ logMessage: "發(fā)送模板消息成功[" + 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 LoadingCachemAccessTokenCache = CacheBuilder.newBuilder() .expireAfterWrite(7200, TimeUnit.SECONDS) .build(new CacheLoader () { @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="http:// + (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; } }
對外開放相關(guān)的接口:
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最終效果 小程序界面 收到的模板消息 其他:突破發(fā)送模板消息的限制getOpenIdByJSCode(String js_code) { return WechatApi.getOpenIdByJSCode(js_code); } @PostMapping("/template_send") public Map templateSend() { String accessToken = WechatApi.getAccessToken(); JSONObject body = JSON.parseObject(WebUtil.getBody()); // 填充模板數(shù)據(jù) (測試代碼,寫死) WechatTemplate wechatTemplate = new WechatTemplate() .setTouser(body.getString("openId")) .setTemplate_id(WechatConf.templateId) // 表單提交場景下為formid,支付場景下為prepay_id .setForm_id(body.getString("formId")) // 跳轉(zhuǎn)頁面 .setPage("index") /** * 模板內(nèi)容填充:隨機(jī)字符 * 購買地點(diǎn) {{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); } }
如非必要,盡量不要這樣做,一旦發(fā)現(xiàn)小程序濫用模板消息,微信是有權(quán)進(jìn)行封禁的。
簡單來說,我們可以將小程序的表單組件進(jìn)行封裝,偽裝小程序中其他功能按鈕。當(dāng)用戶點(diǎn)擊按鈕時,表單組件就自動把formId上傳給服務(wù)器保存(7天后過期),當(dāng)收集到一定的用戶點(diǎn)擊事件后,就可以拿來使用了(主動消息推送群發(fā)),哈哈哈。
源碼地址包含用到的內(nèi)網(wǎng)穿透工具
https://gitee.com/wuwenze/wec...
https://github.com/wuwz/wecha...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/76897.html
摘要:在用戶喜愛的眾多功能中,使用率最高的是模版消息推送。模版消息推送數(shù)的量級也由早期每天幾百條,變?yōu)楹髞淼拿刻鞌?shù)百萬條。平臺支持少知曉云已經(jīng)支持包括微信小程序和支付寶小程序在內(nèi)的各大小程序平臺的消息推送,對平臺的支持也將在近期上線。 兩年多前,為了讓更多的人找到好玩、好用的小程序,我們成立了「知曉程序」。 再后來,我們推出了后端云服務(wù)平臺——知曉云,幫助大家降低創(chuàng)業(yè)成本,提升開發(fā)效率。 「...
摘要:前端,填寫填寫填寫模板模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞推送域名接口地址,我學(xué)習(xí)就用,建議用后端,參數(shù)此處開始處理數(shù)據(jù)發(fā)送一個常規(guī)的請求捕抓異常至于和怎么獲取,自己另外學(xué)習(xí)咯推送 前端,index.wxml 推送 index.js // pages/mubanxiaoxi/mubanx...
摘要:前端,填寫填寫填寫模板模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞模板的第個關(guān)鍵詞推送域名接口地址,我學(xué)習(xí)就用,建議用后端,參數(shù)此處開始處理數(shù)據(jù)發(fā)送一個常規(guī)的請求捕抓異常至于和怎么獲取,自己另外學(xué)習(xí)咯推送 前端,index.wxml 推送 index.js // pages/mubanxiaoxi/mubanx...
摘要:外鏈月最新新增提供組件可以用來承載網(wǎng)頁容器會自動鋪滿整個小程序頁面?zhèn)€人類型和海外類型暫不支持需將訪問域名后臺添加至白名單微信授權(quán)鏈接是否可訪問需要測試公眾號關(guān)聯(lián)公眾號關(guān)聯(lián)小程序后,將可在圖文消息自定義菜單模板消息等功能中使用小程序。 小程序入口 微信發(fā)現(xiàn),小程序 公眾號主體查看小程序 好友分享,群分享 公眾號自定義菜單跳轉(zhuǎn) APP頁面跳轉(zhuǎn) 第三方服務(wù) 附近的小程序 掃普通鏈接二維碼打...
閱讀 2028·2021-08-11 11:13
閱讀 1059·2021-07-25 21:37
閱讀 2601·2019-08-29 18:42
閱讀 2536·2019-08-26 12:18
閱讀 946·2019-08-26 11:29
閱讀 1711·2019-08-23 17:17
閱讀 2687·2019-08-23 15:55
閱讀 2634·2019-08-23 14:34