文章目录
参考
微信开发专栏 - 跟着这个搞一下,里面的代码不错 - 对应的代码
SpringBoot整合调用微信模板方法实现微信公众号消息通知推送,Java实现微信公众号给关注用户推送自定义消息通知(手把手从0到1)
本篇文章的主题是 如何通过springboot来实现微信的模版消息推送
实现效果:
在当今的信息化时代,微信作为国人最为常用的通讯工具之一,已经不仅仅是一个简单的社交应用,更是连接人与服务、人与信息的桥梁。企业微信模板消息作为一种高效、便捷的信息传递方式,被广泛应用于各类业务场景中,如订单通知、会议提醒、活动推送等。
通过本教程的学习,您将掌握如何在Spring Boot项目中集成微信SDK,如何编写代码发送微信模板消息,并了解整个推送的过程。
简要说明: 由于发送模版消息需要微信的服务号,申请服务号的话需要营业执照,个人是没有办法申请的,但是微信为了给开发者们提供测试特意开放了公众平台测试账号号,大家可以申请测试号来进行模版推送的开发和测试
开发步骤:
- 访问微信公众平台测试账号页面
- 获取appID和appsecret
- 扫码二维码添加测试号
- 添加模版消息
- 集成微信SDK
- 调用相关API
1、微信公众平台测试号管理
1.1 访问微信公众平台测试账号页面
大家首先访问微信公众平台地址:https://mp.weixin.qq.com/ 如果还没有注册账号的可以申请一个个人订阅号,这个教程大家网上自行查阅,很简单~
登录成功之后选择 开发者工具 --> 公众平台测试账号
测试号管理页面如下:
1.2 获取appID和appsecret
获取你的测试号信息
1.3 扫码二维码添加测试号
使用你的微信扫描这个测试公众号的二维码并关注 然后会得到你的**微信号(openId)**这个后面会用到
1.4 添加模版消息
点击新增测试模版
添加模板信息 一定要按照注意事项填写 参数需以{
{开头,以.DATA}}结尾 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4e13f07460ad4d9e8901a76b9eec3471.png)我这边创建了两个模版,这个模版id后面也会用到
2、集成微信SDK
2.1 引入微信工具包
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.0.0</version>
</dependency>
2.2 添加配置文件
appId、appSecret和orderTemplateId就是上面微信公众平台测试号管理中我们获取到的几个参数,现在把这三个参数配置到我们的项目中。 callBackUrl暂时先不用管
创建配置类
@ConfigurationProperties(prefix = "wechat.public")
@Component
@Data
@RefreshScope
public class WeChatProperties {
private String appId;
private String appSecret;
private String callBackUrl;
private String orderTemplateId;
}
3、API调用
3.1 发送消息模版的实现
package com.mdx.user.manager;
import com.mdx.user.config.WeChatProperties;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpInMemoryConfigStorage;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* @author : jiagang
* @date : Created in 2022/7/26 10:42
*/
@Component
@Slf4j
public class WxMessagesManager {
@Autowired
private WeChatProperties weChatProperties;
public void sendOrderMsg(String openId, String orderId, String serviceName){
String templateId = weChatProperties.getOrderTemplateId();
// 订单时间
SimpleDateFormat sdf = new SimpleDateFormat();
sdf.applyPattern("yyyy-MM-dd HH:mm");
Date date = new Date();
String timeNow = sdf.format(date);
WxMpInMemoryConfigStorage wxStorage = new WxMpInMemoryConfigStorage();
wxStorage.setAppId(weChatProperties.getAppId());
wxStorage.setSecret(weChatProperties.getAppSecret());
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxStorage);
// 此处的 key/value 需和模板消息对应
List<WxMpTemplateData> wxMpTemplateDataList = Arrays.asList(
new WxMpTemplateData("first", "您有一个新的订货单", "#FF0000"),
new WxMpTemplateData("keyword1", orderId),
new WxMpTemplateData("keyword2", serviceName),
new WxMpTemplateData("keyword3", timeNow),
new WxMpTemplateData("remark", "请登录系统查看订单详情并及时配货")
);
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId) // openId为1.3步骤中得到的微信号
.templateId(templateId)
.data(wxMpTemplateDataList)
.url("https://blog.csdn.net/qq_38374397?type=blog") // 跳转详情地址
.build();
try {
wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
log.info("消息模版发送成功~");
} catch (Exception e) {
log.error("推送失败:" + e.getMessage());
}
}
}
3.2 测试类调用
openId为1.3步骤中得到的微信号,其余参数可自定义
3.3 效果展示
移动端:
点击详情:
PC端:
4、回调
配置回调URL和token
启动natapp,开启内网穿透
@GetMapping("/master")
@ResponseBody
public String init(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
log.info("/myWeXin/master: 收到请求");
if (CheckUtil.checkSignature("myToken", signature, timestamp, nonce)) {
return echostr; // 将这个返回就代表配置成功
}
return null;
}
public class CheckUtil {
/**
* 验证微信get请求
* @param token
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String token,String signature,String timestamp,String nonce){
String[] arr = new String[]{token,timestamp,nonce};
Arrays.sort(arr);
StringBuffer content = new StringBuffer();
for(int i = 0 ; i < arr.length ; i++){
content.append(arr[i]);
}
String temp = Sha1Util.getSha1(content.toString());
return temp.equals(signature);
}
}
import java.security.MessageDigest;
public class Sha1Util {
public static String getSha1(String str){
if(str==null||str.length()==0){
return null;
}
char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();
int j = md.length;
char buf[] = new char[j*2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
// TODO: handle exception
return null;
}
}
}
填写回调URL和Token,点击提交,我们本地的服务器将收到1个GET请求,然后显示配置成功
处理用户消息和事件
上面使用的是GET请求,这里使用的是POST请求。
当用户发生的是消息时,
/master 收到消息: <xml><ToUserName><![CDATA[gh_e86239ea7bd5]]></ToUserName>
<FromUserName><![CDATA[oSbIc6sxvTBJh-QEgMIGv3K9SosE]]></FromUserName>
<CreateTime>1724679387</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[测试消息]]></Content>
<MsgId>24691472136157010</MsgId>
</xml>
收到用户消息:{Content=测试消息, CreateTime=1724679387, ToUserName=gh_e86239ea7bd5, FromUserName=oSbIc6sxvTBJh-QEgMIGv3K9SosE, MsgType=text, MsgId=24691472136157010}
当用取消关注时,
/master 收到消息: <xml><ToUserName><![CDATA[gh_e86239ea7bd5]]></ToUserName>
<FromUserName><![CDATA[oSbIc6sxvTBJh-QEgMIGv3K9SosE]]></FromUserName>
<CreateTime>1724679471</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[unsubscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
收到用户消息:{CreateTime=1724679471, EventKey=, Event=unsubscribe, ToUserName=gh_e86239ea7bd5, FromUserName=oSbIc6sxvTBJh-QEgMIGv3K9SosE, MsgType=event}
当用户再次关注时,
/master 收到消息: <xml><ToUserName><![CDATA[gh_e86239ea7bd5]]></ToUserName>
<FromUserName><![CDATA[oSbIc6sxvTBJh-QEgMIGv3K9SosE]]></FromUserName>
<CreateTime>1724679529</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
收到用户消息:{CreateTime=1724679529, EventKey=, Event=subscribe, ToUserName=gh_e86239ea7bd5, FromUserName=oSbIc6sxvTBJh-QEgMIGv3K9SosE, MsgType=event}
@PostMapping(value = "/master")
public void receiver(@RequestBody String xml, HttpServletResponse resp, HttpServletRequest request) {
try {
log.info("/master 收到消息: {}", xml);
Map<String, String> msgMap = MessageUtil.string2Map(xml);
log.info("收到用户消息:{}", msgMap);
String res = PassiveMsgUtil.getInstance().textMessage(msgMap.get("FromUserName"), msgMap.get("ToUserName"), "你好");
resp.setContentType("text/xml;charset=UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(res);
} catch (Exception e) {
e.printStackTrace();
}
}
public class PassiveMsgUtil {
//私有构造方法
private PassiveMsgUtil() {
}
// 静态内部类实现单例模式
private static class PassiveMsgUtilInstance {
private static final PassiveMsgUtil INSTANCE = new PassiveMsgUtil();
}
public static PassiveMsgUtil getInstance() {
return PassiveMsgUtilInstance.INSTANCE;
}
public String textMessage(String toUserName, String fromUserName, String content) {
return "<xml>\n" +
"<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>\n" +
"<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>\n" +
"<CreateTime>" + new Date().getTime() + "</CreateTime>\n" +
"<MsgType><![CDATA[text]]></MsgType>\n" +
"<Content><![CDATA[" + content + "]]></Content>\n" +
"</xml>";
}
}
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MessageUtil {
/**
* 解析reqString中xml格式消息
* @param reqString HttpServletRequest
* @return Map<节点名,值>
*/
public static Map<String,String> string2Map(String reqString) {
try {
String xml = reqString;
Map<String,String> maps = new HashMap<>();
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
List<Element> eles = root.elements();
for (Element e:eles){
maps.put(e.getName(),e.getTextTrim());
}
return maps;
}catch (DocumentException e){
e.printStackTrace();
}
return null;
}
}
5、获取AccessToken
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
@Test
void test02() {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={APPID}&secret={APPSECRET}";
AccessTokenDTO tokenDTO = restTemplate.getForObject(url, AccessTokenDTO.class, new HashMap<String, Object>() {{
put("APPID", "~~~");
put("APPSECRET", "~~~");
}});
// WxMessageTest.AccessTokenDTO(accessToken=83_5M9JRCgu94nmbTfsD4eoDXw2nWzmoLnHVOYWpdMqT-NsfyBfFFB6gUt5711m4ETlQZZxFejSA6-X5SgELSv7hNXz37fUjamS3zC6M3uagXBriwoMbXgxWNSpvVgRBLgAFAUe1, expiresIn=7200)
System.out.println(tokenDTO);
}
@Data
static class AccessTokenDTO {
@JsonSetter("access_token")
private String accessToken;
@JsonSetter("expires_in")
private Integer expiresIn;
}