1、概述
微信公众号模板消息是腾讯提供的一种采用公众号发送消息方式,微信公众号官方提供了基础模板消息接口供业务调用。但是,如何在业务里面更加合理地发送模板消息是个难题。将消息以异步队列的方式和主业务功能解耦,实现不会因为消息发送影响主业务的正确性和性能;同时,解耦出来的消息队列可以用多种策略提高消息的送达率。为此,实现一种开源基于Redis的公众号模板消息队列开源框架奇辰Open-API。
2、开源框架架构
基于奇辰Open-API实现的前后端分离云原生应用前端请求后端业务如步骤1所示;需要通过微信公众号模板消息通知用户的场景直接调用奇辰Open-API封装好的消息中心往消息队列里面添加待发送的模板消息内容如步骤2所示;由奇辰Open-API消息中心自动完成微信基础API、SDK调用如步骤3所示;最终实现通过微信公众号给用户发送消息通知如步骤4所示。
3、开源实现
3.1原理技术
消息队列基于Redis的List数据结构管理消息队列,生产者(主业务流程)往List队列添加消息,消费者(消息中心)端能获取待发送消息,然后调用封装好的消息功能进行处理,实现主业务流程和消息中心的解耦。消息中心采用Java Springboot框架实现,调用Jedis接口实现Redis应用功能。
3.2消息触发
@SpringBootTest
class MessageApplicationTests {
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Test
void contextLoads() {
JSONObject message = new JSONObject();
message.put("openid", "omOR006*******2qevI");
redisTemplate.opsForList().leftPush("message:queue", JSON.toJSONString(message));
}
}
采用Springboot的test机制模拟往openid为omOR006*******2qeVI的用户发送消息标识为message:queue的公众号模板消息,在第10、12行实现。
消息添加接口:
由于消息队列基于Redis的List数据结构实现采用Java Springboot框架实现,在Springboot框架调用Jedis连接Redis并进行操作。Jedis配置如下:
- 添加redis依赖
<!-- redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--<version>2.1.4.RELEASE</version>-->
</dependency>
- 配置redis参数
@Configuration
public class RedisConfig {
/**
* 设置Redis序列化方式,默认使用的JDKSerializer的序列化方式,效率低,这里我们使用 FastJsonRedisSerializer
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value序列化
redisTemplate.setValueSerializer(new StringRedisSerializer());
// Hash key序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Hash value序列化
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
- 消息管理
开源框架已经对消息队列的管理进行了低代码封装,只需要在在后台可视化添加消息模板即可自动完成消息队列管理,如下图所示:
3.3队列消息发送
发送机制:开启独立线程(根据消息规模选取合适线程数量),循环判断消息队列是否有新消息。
@PostConstruct
public void brPop() {
new Thread(() -> {
List<Template> template_list = templateDao.queryAll();
for (Template message_template : template_list) {
if(message_template.getIdentity() != null && !message_template.getIdentity().equals("")) {
while (true) {
try {
String message = (String) redisTemplate.opsForList().rightPop(message_template.getIdentity(), 10,
TimeUnit.SECONDS);
if (message != null) {
RestTemplate template = new RestTemplate();
String access_token = (String) redisTemplate.opsForValue().get("mp_access_token");
if (access_token == null) {
ConfMp conf = confMpDao.query();
if (conf != null) {
try {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type={grant_type}&appid={appid}&secret={secret}";
Map<String, Object> requestMap = new HashMap<>();
// JSONObject requestMap = new JSONObject();
requestMap.put("grant_type", "client_credential");
requestMap.put("appid", conf.getAppid());
requestMap.put("secret", conf.getSecret());
ResponseEntity<JSONObject> responseEntity = template
.getForEntity(url, JSONObject.class, requestMap);
HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
if (statusCode == HttpStatus.OK) {
JSONObject body = responseEntity.getBody();
if (body.get("access_token") != null) {
access_token = body.getString("access_token");
Integer expires_in = body.getInteger("expires_in") - 5 * 60;
redisTemplate.opsForValue().set("mp_access_token", access_token,
expires_in,
TimeUnit.SECONDS);
}
} else {
log.error("saas 服务访问失败!");
// throw new RuntimeException("saas 服务访问失败!");
}
} catch (Exception e) {
log.error(e.toString());
// throw new RuntimeException(e.toString());
}
}
}
if (access_token != null) {
// Template template = templateDao.queryAll()
String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + access_token;
Map<String, Object> requestMap = new HashMap<>();
// JSONObject requestMap = new JSONObject();
JSONObject message_json = JSON.parseObject(message);
requestMap.put("touser", message_json.getString("openid"));
requestMap.put("template_id", message_template.getMp_template_id());
Map<String, Object> data = new HashMap<>();
Map<String, Object> first = new HashMap<>();
first.put("value", "first");
first.put("color", "#173177");
data.put("first", first);
Map<String, Object> keyword1 = new HashMap<>();
keyword1.put("value", "keyword1");
keyword1.put("color", "#173177");
data.put("keyword1", first);
Map<String, Object> keyword2 = new HashMap<>();
keyword2.put("value", "keyword2");
keyword2.put("color", "#173177");
data.put("keyword2", first);
Map<String, Object> keyword3 = new HashMap<>();
keyword3.put("value", "keyword3");
keyword3.put("color", "#173177");
data.put("keyword3", first);
Map<String, Object> keyword4 = new HashMap<>();
keyword4.put("value", "keyword4");
keyword4.put("color", "#173177");
data.put("keyword4", first);
Map<String, Object> remark = new HashMap<>();
remark.put("value", "remark");
remark.put("color", "#173177");
data.put("remark", remark);
requestMap.put("data", data);
template.postForEntity(url, requestMap, JSONObject.class);
}
}
} catch (Exception e) {
log.info(e.toString());
}
}
}
}
}).start();
}
在第9行检测Redis的消息队列是否有新的消息:如果有新消息,根据对应消息的唯一标识查看对应公众号模板消息ID,调用微信公众号模板消息接口发送模板消息给用户,如下图所示:
微信模板消息:为了发送微信公众号模板消息,首先要获取公众号access token,见代码第25行;然后再以access token为参数加上微信公众号官网获取的模板消息ID,调用微信公众号接口实现模板消息发送,见代码第82行,这个过程开源框架已经封装实现,不需要单独开发。
消息队列在开源框架的message模块里面实现,该模块可以单独运行,保证主业务流程的运行和消息中心的运行独立,减轻主业务流程运行、维护负担,同时使得消息中心的研发也相对独立。
4、更多
开源项目:Open-Api
更多信息:www.lokei.cn
5、后续完善
后续可以继续完善的地方主要有两方面:1)处理微信公众号模板消息外,可以支持短信、邮件等更多消息模式;2)针对更大规模消息队列,可以考虑集成Kafaka等更加复杂消息队列方式。