做小程序开发的时候,消息推送是一个比较常用的功能,基本表涉及到一些重要提醒的功能时,都会使用到微信小程序的模板消息推送,随着用户和开发者的信息推送诉求日益增长,微信官方下架了之前的模板消息推送
功能,改为用户自助订阅消息推送,小程序开发者可自行接入,在用户主动订阅消息后可实现消息随时触达功能。
官方文档下架模板消息公告:
公告地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/template-message.html
公告社区: https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401?blockType=1
小程序模板消息能力在帮助小程序实现服务闭环的同时,也存在一些问题,如:
部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰;
模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求。
为提升小程序模板消息能力的使用体验,我们对模板消息的下发条件进行了调整,由用户自主订阅所需消息。
一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。
(一次性订阅示例)
一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。
目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。
调整计划
小程序订阅消息接口上线后,原先的模板消息接口将停止使用,详情如下:
开发者可登录小程序管理后台开启订阅消息功能,接口开发可参考文档:《小程序订阅消息》
开发者使用订阅消息能力时,需遵循运营规范,不可用奖励或其它形式强制用户订阅,不可下发与用户预期不符或违反国家法律法规的内容。具体可参考文档:《小程序订阅消息接口运营规范》
原有的小程序模板消息接口将于 2020 年 1 月 10 日下线,届时将无法使用此接口发送模板消息,请各位开发者注意及时调整接口。
订阅消息的模板库中提供了大量的模板,但是在企业项目开发过程中,这些模板一般都是不与我们现实业务相符合的,因此需要自行申请模板消息,待官方通过后才可以使用,这里为了方便我们直接从模板库添加一个模板来使用。
从模板库选用一个消费成功通知
模板
详细内容中的字段占位符就是需要我们来填充数据的,具体做法就是在Java后台,通过替换占位符的形式设置字段的。
微信小程序开发使用的是开源SDK
,WxJava
工具包下面的小程序SDK
依赖: weixin-java-miniapp
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>3.8.0</version>
</dependency>
由于这篇文章涉及到的代码比较多,这里只贴出一些核心的代码,像其他工具类、小程序相关接口类大家去GitHub下载源码即可
GitHub地址: https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-miniapp
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>
<groupId>com.thinkingcao</groupId>
<artifactId>springboot-miniapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-miniapp</name>
<description>微信小程序</description>
<properties>
<java.version>1.8</java.version>
<weixin-java-miniapp.version>3.8.0</weixin-java-miniapp.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.locales>zh_CN</project.build.locales>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>${weixin-java-miniapp.version}</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
#服务端口
server:
port: 80
#小程序SDK包日志级别配置
logging:
level:
org.springframework.web: info
com.github.binarywang.demo.wx.miniapp: debug
cn.binarywang.wx.miniapp: debug
#微信小程序参数配置
wx:
miniapp:
configs:
- appid: #微信小程序的appid
secret: #微信小程序的Secret
token: #微信小程序消息服务器配置的token
aesKey: #微信小程序消息服务器配置的EncodingAESKey
msgDataFormat: JSON
/**
* @desc: 小程序配置参数
* @author: cao_wencao
* @date: 2020-08-06 14:22
*/
@Data
@Component
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMiniProperties {
private List<Config> configs;
@Data
public static class Config {
/**
* 设置微信小程序的appid
*/
private String appid;
/**
* 设置微信小程序的Secret
*/
private String secret;
/**
* 设置微信小程序消息服务器配置的token
*/
private String token;
/**
* 设置微信小程序消息服务器配置的EncodingAESKey
*/
private String aesKey;
/**
* 消息格式,XML或者JSON
*/
private String msgDataFormat;
}
}
package com.thinkingcao.springboot.miniapp.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
import cn.binarywang.wx.miniapp.bean.WxMaTemplateData;
import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import cn.binarywang.wx.miniapp.message.WxMaMessageHandler;
import cn.binarywang.wx.miniapp.message.WxMaMessageRouter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @desc:
* @author: cao_wencao
* @date: 2020-08-06 14:22
*/
@Configuration
@EnableConfigurationProperties(WxMiniProperties.class)
public class WxMiniConfiguration {
private WxMiniProperties properties;
private static Map<String, WxMaMessageRouter> routers = Maps.newHashMap();
private static Map<String, WxMaService> maServices = Maps.newHashMap();
@Autowired
public WxMiniConfiguration(WxMiniProperties properties) {
this.properties = properties;
}
public static WxMaService getMaService(String appid) {
WxMaService wxService = maServices.get(appid);
if (wxService == null) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
return wxService;
}
public static WxMaMessageRouter getRouter(String appid) {
return routers.get(appid);
}
@PostConstruct
public void init() {
List<WxMiniProperties.Config> configs = this.properties.getConfigs();
if (configs == null) {
throw new RuntimeException("大哥,拜托先看下项目首页的说明(readme文件),添加下相关配置,注意别配错了!");
}
maServices = configs.stream()
.map(a -> {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(a.getAppid());
config.setSecret(a.getSecret());
config.setToken(a.getToken());
config.setAesKey(a.getAesKey());
config.setMsgDataFormat(a.getMsgDataFormat());
WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(config);
routers.put(a.getAppid(), this.newRouter(service));
return service;
}).collect(Collectors.toMap(s -> s.getWxMaConfig().getAppid(), a -> a));
}
private WxMaMessageRouter newRouter(WxMaService service) {
final WxMaMessageRouter router = new WxMaMessageRouter(service);
router
.rule().handler(logHandler).next()
.rule().async(false).content("模板").handler(templateMsgHandler).end()
.rule().async(false).content("文本").handler(textHandler).end()
.rule().async(false).content("图片").handler(picHandler).end()
.rule().async(false).content("二维码").handler(qrcodeHandler).end();
return router;
}
private final WxMaMessageHandler templateMsgHandler = (wxMessage, context, service, sessionManager) -> {
service.getMsgService().sendTemplateMsg(WxMaTemplateMessage.builder()
.templateId("此处更换为自己的模板id")
.formId("自己替换可用的formid")
.data(Lists.newArrayList(
new WxMaTemplateData("keyword1", "339208499", "#173177")))
.toUser(wxMessage.getFromUser())
.build());
return null;
};
private final WxMaMessageHandler logHandler = (wxMessage, context, service, sessionManager) -> {
System.out.println("收到消息:" + wxMessage.toString());
service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("收到信息为:" + wxMessage.toJson())
.toUser(wxMessage.getFromUser()).build());
return null;
};
private final WxMaMessageHandler textHandler = (wxMessage, context, service, sessionManager) -> {
service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("回复文本消息")
.toUser(wxMessage.getFromUser()).build());
return null;
};
private final WxMaMessageHandler picHandler = (wxMessage, context, service, sessionManager) -> {
try {
WxMediaUploadResult uploadResult = service.getMediaService()
.uploadMedia("image", "png",
ClassLoader.getSystemResourceAsStream("tmp.png"));
service.getMsgService().sendKefuMsg(
WxMaKefuMessage
.newImageBuilder()
.mediaId(uploadResult.getMediaId())
.toUser(wxMessage.getFromUser())
.build());
} catch (WxErrorException e) {
e.printStackTrace();
}
return null;
};
private final WxMaMessageHandler qrcodeHandler = (wxMessage, context, service, sessionManager) -> {
try {
final File file = service.getQrcodeService().createQrcode("123", 430);
WxMediaUploadResult uploadResult = service.getMediaService().uploadMedia("image", file);
service.getMsgService().sendKefuMsg(
WxMaKefuMessage
.newImageBuilder()
.mediaId(uploadResult.getMediaId())
.toUser(wxMessage.getFromUser())
.build());
} catch (WxErrorException e) {
e.printStackTrace();
}
return null;
};
}
package com.thinkingcao.springboot.miniapp.controller;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaMessage;
import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import com.thinkingcao.springboot.miniapp.config.WxMiniConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
/**
* @desc: 微信小程序门户入口接口
* @author: cao_wencao
* @date: 2020-08-06 14:23
*/
@RestController
@RequestMapping("/wx/portal/{appid}")
public class WxPortalController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(@PathVariable String appid,
@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
this.logger.info("\n接收到来自微信服务器的认证消息:signature = [{}], timestamp = [{}], nonce = [{}], echostr = [{}]",
signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
final WxMaService wxService = WxMiniConfiguration.getMaService(appid);
if (wxService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法请求";
}
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable String appid,
@RequestBody String requestBody,
@RequestParam(name = "msg_signature", required = false) String msgSignature,
@RequestParam(name = "encrypt_type", required = false) String encryptType,
@RequestParam(name = "signature", required = false) String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce) {
this.logger.info("\n接收微信请求:[msg_signature=[{}], encrypt_type=[{}], signature=[{}]," +
" timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
msgSignature, encryptType, signature, timestamp, nonce, requestBody);
final WxMaService wxService = WxMiniConfiguration.getMaService(appid);
final boolean isJson = Objects.equals(wxService.getWxMaConfig().getMsgDataFormat(),
WxMaConstants.MsgDataFormat.JSON);
if (StringUtils.isBlank(encryptType)) {
// 明文传输的消息
WxMaMessage inMessage;
if (isJson) {
inMessage = WxMaMessage.fromJson(requestBody);
} else {//xml
inMessage = WxMaMessage.fromXml(requestBody);
}
this.route(inMessage, appid);
return "success";
}
if ("aes".equals(encryptType)) {
// 是aes加密的消息
WxMaMessage inMessage;
if (isJson) {
inMessage = WxMaMessage.fromEncryptedJson(requestBody, wxService.getWxMaConfig());
} else {//xml
inMessage = WxMaMessage.fromEncryptedXml(requestBody, wxService.getWxMaConfig(),
timestamp, nonce, msgSignature);
}
this.route(inMessage, appid);
return "success";
}
throw new RuntimeException("不可识别的加密类型:" + encryptType);
}
private void route(WxMaMessage message, String appid) {
try {
WxMiniConfiguration.getRouter(appid).route(message);
} catch (Exception e) {
this.logger.error(e.getMessage(), e);
}
}
}
package com.thinkingcao.springboot.miniapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*/
@SpringBootApplication
public class MiniappApplication {
public static void main(String[] args) {
SpringApplication.run(MiniappApplication.class, args);
}
}
package com.thinkingcao.springboot.miniapp.controller;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import com.thinkingcao.springboot.miniapp.config.WxMiniConfiguration;
import com.thinkingcao.springboot.miniapp.result.ApiResult;
import com.thinkingcao.springboot.miniapp.utils.DateUtils;
import com.thinkingcao.springboot.miniapp.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
/**
* @desc: 微信小程序订阅消息推送接口
* @author: cao_wencao
* @date: 2020-08-06 14:22
*/
@Slf4j
@RestController
@RequestMapping("/api/msg")
public class WxMsgPushController {
public static final String POINT_COUNT = "10";
/**
* 微信小程序推送订阅消息
*
* @param openid
* @return
*/
@GetMapping("/push")
public ApiResult push(String accountCode, String appId, String openid) {
if (StringUtils.isAnyBlank(accountCode, appId, openid)) {
return ApiResult.error("参数缺失");
}
WxMaSubscribeMessage subscribeMessage = new WxMaSubscribeMessage();
//发送给哪个用户
subscribeMessage.setToUser(openid);
//模板消息id
subscribeMessage.setTemplateId("5zl1Qn_U10o1sGWRU06HqytTiZ8zi0QCJYA0D_l7_tA");
//跳转小程序页面路径
subscribeMessage.setPage("pages/index/index");
//进入小程序查看”的语言类型
subscribeMessage.setLang("zh_CN");
//=====================================创建一个参数集合======================
ArrayList<WxMaSubscribeMessage.Data> wxMaSubscribeData = new ArrayList<>();
// 第三个内容:绿账卡号
WxMaSubscribeMessage.Data data1 = new WxMaSubscribeMessage.Data();
data1.setName("character_string7");
data1.setValue(accountCode);
wxMaSubscribeData.add(data1);
//第一个内容: 变更分值
WxMaSubscribeMessage.Data data2 = new WxMaSubscribeMessage.Data();
data2.setName("thing4");
data2.setValue(POINT_COUNT);
//每个参数 存放到大集合中
wxMaSubscribeData.add(data2);
// 第二个内容:变更时间
WxMaSubscribeMessage.Data data3 = new WxMaSubscribeMessage.Data();
data3.setName("time1");
data3.setValue(DateUtils.getTime());
wxMaSubscribeData.add(data3);
// 第二个内容:备注
WxMaSubscribeMessage.Data data4 = new WxMaSubscribeMessage.Data();
data4.setName("thing3");
data4.setValue("扫描绿账卡号,账户积分增加变动");
wxMaSubscribeData.add(data4);
//把集合给大的data
subscribeMessage.setData(wxMaSubscribeData);
try {
//获取微信小程序配置:
//final WxMaService wxService = WxMaConfiguration.getMaService(appId);
//进行推送
final WxMaService wxService = WxMiniConfiguration.getMaService(appId);
wxService.getMsgService().sendSubscribeMsg(subscribeMessage);
log.info("【微信小程序推送订阅消息成功:】" + JsonUtils.toJson(subscribeMessage));
return ApiResult.succee("推送成功");
} catch (Exception e) {
log.error("【微信小程序推送订阅消息失败:】" + JsonUtils.toJson(subscribeMessage));
e.printStackTrace();
}
return ApiResult.error("推送失败");
}
}
在微信公众平台手动配置获取模板 ID:
登录 https://mp.weixin.qq.com 获取模板,如果没有合适的模板,可以申请添加新模板,审核通过后可使用。
下发权限的获取十分简单,只需要在小程序端调用 requestSubscribeMessage
API即可
wx.requestSubscribeMessage({
tmplIds: [''],
success (res) { }
})
详见小程序端获取下发权限接口文档: wx.requestSubscribeMessage
下发消息的核心在于发送POST请求:subscribeMessage.send
,通过此条请求我们将获得下发能力:
subscribeMessage.send
支持https
调用和云调用{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"page": "index",
"miniprogram_state":"developer",
"lang":"zh_CN",
"data": {
"number01": {
"value": "339208499"
},
"date01": {
"value": "2015年01月05日"
},
"site01": {
"value": "TIT创意园"
} ,
"site02": {
"value": "广州市新港中路397号"
}
}
}
详见小程序端调用接口下发订阅消接口文档: subscribeMessage.send
//发送订阅消息
pushMsg: function() {
let that = this;
wx.requestSubscribeMessage({
//订阅消息模板id
tmplIds: ['fqgotens_HRal3Ygiy7_WafYLsvDyMxvnNv9P8uJ_Ro'],
success(res) {
//'accept'表示用户接受;'reject'表示用户拒绝;'ban'表示已被后台封禁
if (res['fqgotens_HRal3Ygiy7_WafYLsvDyMxvnNv9P8uJ_Ro'] == "accept") {
wx.request({
//后端消息推送接口
url: 'https://thinkingcao.natapp1.cc/api/msg/push',
data: {
openid: that.openid
},
success: function(res) {
console.log("订阅成功");
console.log(res);
wx.showToast({
title: '订阅成功!',
duration: 1000,
success(data) {
//成功处理
}
})
},
fail: function(res) {
console.log("订阅失败");
wx.showToast({
title: '订阅失败!',
duration: 1000,
success(data) {
//失败处理
}
})
},
})
}
}
})
},
从上图可以看出,微信小程序订阅消息推送成功,后端和前端代码都很简单,主要是需要看懂微信小程序开发文档,这是最核心的。