小程序、公众号准备
目前有一个已微信认证的订阅号类型公众号,一个微信认证小程序,小程序和公众号互相关联。尚不清楚是否必须微信认证或特定类型,因为目前没遇到类型不匹配或相关的问题,发送微信小程序一次性订阅消息的相关限制较少
什么是小程序订阅消息
1、功能介绍
-
订阅消息推送位置:服务通知
-
订阅消息下发条件:用户自主订阅
-
订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面
2、消息类型
-
一次性订阅消息(本文实现的消息类型)
一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 -
长期订阅消息
一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。
-
设备订阅消息
设备订阅消息是一种特殊类型的订阅消息,它属于长期订阅消息类型,且需要完成「设备接入」才能使用。设备订阅消息用于在设备触发某些需要人工介入的事件时(例如设备发生故障、设备耗材不足等),向用户发送消息通知。详见设备订阅消息文档。
详细介绍还是自己看官方文档吧:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html
实现过程
1、订阅消息模板选用
-
登录小程序微信公众平台,点击功能——订阅消息
-
点击公共模板库,搜索选用需要的模板
-
只能选用5个关键词,输入场景说明后点击提交
-
如果没有自己想要的关键词,点击页面上的
点击申请
去申请自己需要的关键词。注意,每个月有5次申请机会,申请的关键词是你要新增的关键词,原本模板中有的就不要再重复申请了,申请通过后点选用模板可以选用原本有的关键词和你申请通过的关键词
- 选好模板关键词确认提交后,在
我的模板
下就有了
- 点击详情可以查看使用该模板发送订阅消息需要传什么参数
2、对接拉起小程序登录弹窗,解析获取用户openId,将openId保存到数据库,关联到用户信息或用户表加个字段
- 小程序部分实现
官方文档:https://uniapp.dcloud.net.cn/api/plugins/login.html
uni.login({ success: (result) => { //保存获取到的code this.jsCode = result.code; request({ url: '后台接口地址', method: "POST", data: { code: result.code }, }) .then((res) => { this.openid = res.data.openid; this.session_key = res.data.session_key; }) .catch((err) => {}); }, fail: (error) => {},});
- 后端部分实现
官方文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
// 对传入code进行解密获取openidLinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("appid",appid);params.add("secret",secret);// 小程序调接口传的codeparams.add("js_code",code);params.add("grant_type","authorization_code");String url = "https://api.weixin.qq.com/sns/jscode2session?appid="+appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code";String result = restTemplate.getForObject(url, String.class);JSONObject jsonObject = JSON.parseObject(result);Map<String, String> map = new HashMap<>();try { map.put("openid", jsonObject.get("openid").toString()); map.put("session_key", jsonObject.get("session_key").toString()); log.info("code解密成功");} catch (Exception e){ log.error("code解析失败,"+jsonObject.getString("errmsg")); throw new RuntimeException("code解析失败,"+jsonObject.getString("errmsg"));}return map;
3、引导用户主动授权订阅消息发送
- 在小程序新建
appletAuthorize.js
文件
官方文档:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/subscribe-message/wx.requestSubscribeMessage.html
import Vue from 'vue'const templateIds = [ // 审批结果通知 '审批结果通知模板id', // 领用单领用通知 '领用单领用通知模板id', // 待审核通知 '待审核通知模板id', // 数据报表生成通知 '数据报表生成通知模板id', // 工单完成提醒 '工单完成提醒模板id']const appletAuthorize = () => { let tmplIds = [] uni.getStorage({ key: "currAuthorizeStep", success: ({ data }) => { console.log("currAuthorizeStep", data); let temp = data for (var i = 0; i < 3; i++) { if (temp === templateIds.length) { temp = 0 } tmplIds.push(templateIds[temp]) temp++ } uni.setStorage({ key: "currAuthorizeStep", data: temp, success: (result) => {}, fail: (error) => {}, }); if (tmplIds.length === 0) { return } wx.requestSubscribeMessage({ tmplIds: tmplIds, success(res) { console.log("订阅消息唤起成功 =====>", res) }, fail(err) { console.log("订阅消息唤起失败 =====>", err) } }) }, fail: (error) => {}, });};Vue.prototype.$appletAuthorize = appletAuthorizeexport default appletAuthorize;
注意:
(1)在登录时候setStorage
uni.setStorage({ key: "currAuthorizeStep", data: 0, success: (result) => {}, fail: (error) => {},});
(2)用templateIds把模板id都定义出来,每次取三个授权,是因为wx.requestSubscribeMessage订阅的消息模板的id每次最多只能传3个,不然会订阅失败
- 在小程序页面@click的方法里面加上
this.$appletAuthorize()
注意:
(1)用户对订阅消息授权了才能向用户发订阅消息
(2)用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。即不能一进入页面就调用订阅授权方法,需要和点击事件绑定一起使用
4、订阅消息模板有了,用户openId有了,用户订阅消息授权有了,接下来是发送订阅消息了
官方文档:
(1)https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/sendMessage.html
(2)https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
参数拼接工具类
import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.HashMap;import java.util.Map;/** * 发送订阅消息参数整合 * * @author Administrator */@Component@RequiredArgsConstructor(onConstructor_ = @Autowired)@Slf4jpublic class AppletParamUtil { /** * 待审核通知参数 * @param thing1Value * @param thing11Value * @param thing12Value * @param time4Value * @param thing10Value * @return */ public static Map<String, Object> getNeedConfirmParam (String thing1Value, String thing11Value, String thing12Value, String time4Value, String thing10Value) { Map<String, Object> objMap = new HashMap<>(5); Map<String, String> map = new HashMap<>(1); // 申请人 map.put("value", thing1Value); objMap.put("thing1", map); // 申请人部门 map = new HashMap<>(1); map.put("value", thing11Value); objMap.put("thing11", map); // 申请人工种 map = new HashMap<>(1); map.put("value", thing12Value); objMap.put("thing12", map); // 申请时间 map = new HashMap<>(1); map.put("value", time4Value); objMap.put("time4", map); // 申请物品 map = new HashMap<>(1); map.put("value", thing10Value); objMap.put("thing10", map); return objMap; } /** * 数据报表生成通知 * @param thing1Value * @param thing2Value * @return */ public static Map<String, Object> getDataReportParam (String thing1Value, String thing2Value) { Map<String, Object> objMap = new HashMap<>(2); Map<String, String> map = new HashMap<>(1); // 报表名称 map.put("value", thing1Value); objMap.put("thing1", map); // 数据统计周期 map = new HashMap<>(1); map.put("value", thing2Value); objMap.put("thing2", map); return objMap; }}
订阅消息发送工具类
import cn.hutool.json.JSONObject;import cn.hutool.json.JSONUtil;import com.alibaba.csp.sentinel.util.StringUtil;import com.alibaba.druid.support.json.JSONUtils;import com.ruoyi.RemoteSysConfigService;import com.ruoyi.common.core.web.domain.AjaxResult;import com.ruoyi.system.api.RemoteUserService;import com.ruoyi.system.api.domain.SysUser;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.http.HttpEntity;import org.springframework.stereotype.Component;import org.springframework.util.LinkedMultiValueMap;import org.springframework.util.MultiValueMap;import org.springframework.web.client.RestTemplate;import org.springframework.web.util.UriComponentsBuilder;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;/** * 发送小程序信息 * * @author Administrator */@Component@RequiredArgsConstructor(onConstructor_ = @Autowired)@Slf4jpublic class AppletMsgSendUtil { private final RestTemplate restTemplate; private final StringRedisTemplate redisTemplate; /** * 发送小程序订阅信息 * @param templateId 所需下发的订阅模板id * @param page 点击模板卡片后的跳转页面 * @param data 模板内容 * @return */ public void msgSend(String templateId, String page, Map<String, Object> data) { try { //region 获取access_token String accessToken = getAppletToken(); //endregion //region 推送小程序信息 String msgUrl = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken; // 参数处理 Map<String, Object> params = new HashMap<>(6); params.put("touser", openId); params.put("template_id", templateId); params.put("page", page); params.put("lang", "zh_CN"); params.put("miniprogram_state", "formal"); params.put("data", data); String jsonData = JSONUtils.toJSONString(params); HttpEntity<String> request = new HttpEntity<>(jsonData); String msgResult = restTemplate.postForObject(msgUrl, request, String.class); JSONObject msgResultObject = JSONUtil.parseObj(msgResult); if (!"0".equals(msgResultObject.get("errcode").toString())) { throw new RuntimeException("发送订阅消息失败," + msgResultObject.get("errmsg")); } //endregion } catch (Exception e) { throw new RuntimeException("发送订阅消息失败," + e); } //endregion } /** * 获取小程序token * @return */ public String getAppletToken () { // 先从缓存查看有没有 String appletToken = redisTemplate.opsForValue().get("AppletToken"); if (!StringUtil.isBlank(appletToken)) { return appletToken; } //设置查询参数与请求url MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); queryParams.add("appid", appid); queryParams.add("secret", secret); String tokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"; UriComponentsBuilder tokenBuilder = UriComponentsBuilder.fromHttpUrl(tokenUrl).queryParams(queryParams); //获取token String tokenResult = restTemplate.getForObject(tokenBuilder.toUriString(), String.class); JSONObject tokenObject = JSONUtil.parseObj(tokenResult); appletToken = tokenObject.getStr("access_token"); if (StringUtil.isBlank(appletToken)) { throw new RuntimeException("小程序token获取失败," + tokenObject.getStr("errmsg")); } //将token存到redis,有效期100分钟。官方接口返回的token有效期120分钟 redisTemplate.opsForValue().set("AppletToken", appletToken); redisTemplate.expire("AppletToken", 100, TimeUnit.MINUTES); return appletToken; }}
用法
// 发送消息通知领取人已领取物品Map<String, Object> objMap = AppletParamUtil.getDataReportParam(thing1Value,thing2Value);msgSendUtil.msgSend(模板id, 需要跳转的小程序页面, objMap);
注意:参数拼接工具类里面方法的各个参数是根据模板而定的,模板有多少个参数,工具类方法就有多少个参数
最后成功发送订阅消息的效果就是这样了
另外,附上apipost的订阅消息发送调用方式。参数含义看上面贴出的官方文档
url:https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=你的token
method:POST
body:
{ “touser”: “”,
“template_id”: “”,
“page”: “”,
“lang”:“zh_CN”,
“miniprogram_state”: “formal”,
“data”: {} }
有什么不懂的去微信开放社区问,去发帖。https://developers.weixin.qq.com/community/develop/mixflow
参考:
(1)https://developers.weixin.qq.com/community/develop/doc/0008aa1fd40ec80b4710e9b2260000
(2)https://developers.weixin.qq.com/community/develop/doc/000ce8a9298950338310bc75966800