说明
这里记录下自己学习SpringBoot对接微信公众平台的成长过程,以防止后面继续踩坑且方便以后直接使用。这里使用微信公众号的接口测试号来开发微信公众平台。这里承接自己的博客SpringBoot对接微信公众平台(1)— 配置微信公众平台测试号URL并校检这篇博客,在该博客项目的基础上增加接收普通消息的代码示例。
微信公众号-基础消息能力/接收普通消息:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
后端代码
当用户给微信公众号发送消息时,根据官网给的文档说明,它会去调用之前你在测试号里面配置的URL接口地址,并且是以POST请求方式调用。所以你会在下面的Controller层看到2个/check,一个get的/check接口是用来测试微信公众号能否调用你的接口,一个Post的/check接口是用户给微信公众号发送消息时,会去调用该接口。这个接口才是实际环境中你要的功能接口。
SpringBoot项目目录如下:
pom.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>wechat-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>wechat-service</name>
<description>wechat-service</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web依赖,内嵌入tomcat,RestTempLate使用该依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--用来将string的json格式字符串转换成json对象-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--lombok依赖,用来对象省略写set、get方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!--解析xml-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--用来将string的json格式字符串转换成json对象-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.wechatservice.WechatServiceApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml代码:
server:
port: 8080
wxChat:
appID:你的测试公众号appID
appsecret:你的测试公众号appsecret
config下的RestTemplateConfig配置代码如下:
package com.example.wechatservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());
return restTemplate;
}
// 设置超时时间
public ClientHttpRequestFactory clientHttpRequestFactory(){
//创建一个httpClient简单工厂
SimpleClientHttpRequestFactory factory=new SimpleClientHttpRequestFactory();
//设置连接超时时间,单位ms
factory.setConnectTimeout(15000);
//设置读取超时时间,单位ms
factory.setReadTimeout(10000);
return factory;
}
}
button文件夹下的AbstractButton抽象类代码:
package com.example.wechatservice.button;
public abstract class AbstractButton {
private String name;
public AbstractButton(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
button文件夹下的Button类代码:
package com.example.wechatservice.button;
import java.util.List;
public class Button {
private List<AbstractButton> button;
public List<AbstractButton> getButton() {
return button;
}
public void setButton(List<AbstractButton> button) {
this.button = button;
}
}
button文件夹下的ClickButton类代码:
package com.example.wechatservice.button;
public class ClickButton extends AbstractButton{
public ClickButton(String name) {
super(name);
this.type = "click";
}
private String type;
private String key;
public String getType() {
return type;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
button文件夹下的PhotoOrAlbumButton类代码:
package com.example.wechatservice.button;
public class PhotoOrAlbumButton extends AbstractButton{
public PhotoOrAlbumButton(String name) {
super(name);
this.type = "pic_photo_or_album";
}
private String type;
private String key;
public String getType() {
return type;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
button文件夹下的SubButton类代码:
package com.example.wechatservice.button;
import java.util.List;
public class SubButton extends AbstractButton{
public SubButton(String name) {
super(name);
}
private List<AbstractButton> sub_button;
public List<AbstractButton> getSub_button() {
return sub_button;
}
public void setSub_button(List<AbstractButton> sub_button) {
this.sub_button = sub_button;
}
}
button文件夹下的ViewButton类代码:
package com.example.wechatservice.button;
public class ViewButton extends AbstractButton{
public ViewButton(String name,String url) {
super(name);
this.type = "view";
this.url = url;
}
private String type;
private String url;
public String getUrl() {
return url;
}
public String getType() {
return type;
}
}
WeChatController代码如下:
package com.example.wechatservice.controller;
import com.alibaba.fastjson.JSONObject;
import com.example.wechatservice.button.*;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.security.MessageDigest;
import java.util.*;
@RestController
@RequestMapping(value = "/weChat")
public class WeChatController {
@Resource
private RestTemplate restTemplate;
//测试内网穿透后在外网访问接口
@GetMapping(value = "/hello")
public String hello(){
return "hello";
}
//测试微信公众平台里面的接口配置信息里面的URL能否调用成功,这种只是测试微信公众平台能否调用接口,但是不能识别它是否真的来自于微信公众平台来调用你的,所以才需要做验证
@GetMapping(value = "/test")
public String test(@RequestParam(value = "signature", required = false) String signature,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "nonce", required = false) String nonce,
@RequestParam(value = "echostr", required = false) String echostr){
System.out.println("微信测试公众平台调用我了!!!!");
System.out.println("signature="+signature);
System.out.println("timestamp="+timestamp);
System.out.println("nonce="+nonce);
System.out.println("echostr="+echostr);
//必须原封不动将echostr返回给微信公众号,微信公众测试号才能配置成功那个URL,返回其他值都会导致配置失败
return echostr;
}
//测试微信公众平台里面的接口配置信息,并验证它是否真的来自微信公众平台
@GetMapping(value = "/check")
public String check(@RequestParam(value = "signature", required = false) String signature,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "nonce", required = false) String nonce,
@RequestParam(value = "echostr", required = false) String echostr){
System.out.println("微信公众平台调用我了!!!!");
//微信公众平台配置的token值
String token="testToken";
//1.将token、timestamp、nonce三个参数进行字典序排序
List<String> list= Arrays.asList(token,timestamp,nonce);
//排序
Collections.sort(list);
//2.将三个参数字符串拼接成一个字符串进行sha1加密
StringBuilder stringBuilder=new StringBuilder();
for(String s:list){
stringBuilder.append(s);
}
//加密
try{
MessageDigest instance = MessageDigest.getInstance("SHA-1");
//使用sha1进行加密获得byte数组
byte[] digest=instance.digest(stringBuilder.toString().getBytes());
StringBuilder sum=new StringBuilder();
for(byte b:digest){
sum.append(Integer.toHexString((b>>4)&15));
sum.append(Integer.toHexString(b&15));
}
//3.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
if(!StringUtils.isEmpty(signature)&&signature.equals(sum.toString())){
System.out.println("与微信公众平台一致,确认来自微信公众平台。");
//必须原封不动将echostr返回给微信公众号,微信公众测试号才能配置成功那个URL,返回其他值都会导致配置失败
return echostr;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
//给微信公众号发送普通消息时(重中之重,与微信公众号进行交互全靠这个接口),微信公众号会调用配置好的URL接口,并且是以POST形式,所以这里有个POST的check方法,这里需要把get请求中的/check里面的验证是否真的来自微信公众平台的逻辑给加上,因为是demo所以没加验证
//MsgType为text时,表示用户给微信公众号发的是文本消息
//MsgType为image时,表示用户给微信公众号发的是图片消息
//MsgType为event时,表示用户点击微信公众号里面的菜单
@PostMapping(value = "/check")
public String receiveMessage(@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid){
System.out.println("用户给我发消息了!!");
System.out.println("requestBody="+requestBody);
System.out.println("signature="+signature);
System.out.println("timestamp="+timestamp);
System.out.println("nonce="+nonce);
System.out.println("openid="+openid);
Map<String,String> map=new HashMap<>();
try{
SAXReader reader=new SAXReader();
Document document = reader.read(new ByteArrayInputStream(requestBody.getBytes("utf-8")));//读取微信传的xml字符串,注意这里要转成输入流
Element root = document.getRootElement();//获取根元素
List<Element> elementList = root.elements();//获取当前元素下的全部子元素
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
System.out.println(map);
}catch (Exception e){
e.printStackTrace();
}
//这里需要对不同的回复消息xml模板做不同封装,因为是demo所以就直接拿来用先看一看效果。
String message="";
//如果用户给公众号发送的是文本消息
if(map.get("MsgType").equals("text")){
//如果用户发送的内容是111(相当于条件匹配),则返回主菜单文本消息
if(map.get("Content").equals("111")){
message="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715665213</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[您好,欢迎你访问该微信公众号!]]></Content></xml>";
}
}else if(map.get("MsgType").equals("event")){//用户点击菜单栏菜单按钮时
//这里我只写了一个样例,这里肯定是要写所有点击菜单的判断的,因为是demo所以就不写多了
//如果用户点击了菜单2子级1,通过key来匹配,回复图文消息
if(map.get("EventKey").equals("菜单2子级1")){
//如果用户点击了菜单2子级1,回复图文消息
message="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715656909</CreateTime><MsgType><![CDATA[news]]></MsgType><ArticleCount>1</ArticleCount><Articles><item><Title><![CDATA[菜单2子级1]]></Title><Description><![CDATA[点击进入页面,即可进入查询>>]]></Description><PicUrl><![CDATA[http://mmbiz.qpic.cn/sz_mmbiz_jpg/76BCY09cHYSZ60MBaHyHOJPibesjP6ibeRziabuSHZ4jYrITTlmVqFazCzlm8DdQ8QRUrfCulTNrEUyhUcydUiayuQ/0]]></PicUrl><Url><![CDATA[http://www.soso.com/]]></Url></item></Articles></xml>";
}
}else if(map.get("MsgType").equals("image")){//用户发送的是图片消息
//回复图片消息示例,可以先给微信公众平台发送图片消息,可以在后端控制台获取到图片的MediaId值和PicUrl值
message="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715665213</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[A3UqbFuQxw2gVlIk1_JGw57YlBrmPA4-stcjyk8yA1MBB7pgGSgD_UxbmfwswSN4]]></MediaId></Image></xml>";
}
//回复文本消息示例
//String messageText="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715665213</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[你好]]></Content></xml>";
//回复图片消息示例,可以先给微信公众平台发送图片消息,可以在后端控制台获取到图片的MediaId值和PicUrl值
//String messageImage="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715665213</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[A3UqbFuQxw2gVlIk1_JGw57YlBrmPA4-stcjyk8yA1MBB7pgGSgD_UxbmfwswSN4]]></MediaId></Image></xml>";
//回复语音消息示例,可以先给微信公众平台发送语音消息,可以在后端控制台获取到它的MediaId值
//String messageVoice="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715656909</CreateTime><MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[d15tL6-BeVLR_v1sijEi8K5nv17RDdz1jDN1DhHVnstKFGbqB8n3riY_d3e2OJlL]]></MediaId></Voice></xml>";
//回复视频消息示例,可以先给微信公众平台发送语音消息,可以在后端控制台获取到它的MediaId值,有点问题,回复不了,需要查找原因
//String messageVideo="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715656909</CreateTime><MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[d15tL6-BeVLR_v1sijEi8HHaaJYRI5baAHJNz-Rsy9nhyAocMVwbdz-uFwPIzpGs2uwQC6BbBpcoK-dpNJ4QEA]]></MediaId><Title><![CDATA[test title]]></Title><Description><![CDATA[test description]]></Description></Video></xml>";
//回复图文消息示例
//String messageNews="<xml><ToUserName><![CDATA[ooiyP6HjMn7v42TtTkeZtj89OLKc]]></ToUserName><FromUserName><![CDATA[gh_7fe728a71cd6]]></FromUserName><CreateTime>1715656909</CreateTime><MsgType><![CDATA[news]]></MsgType><ArticleCount>1</ArticleCount><Articles><item><Title><![CDATA[我是标题]]></Title><Description><![CDATA[测试描述]]></Description><PicUrl><![CDATA[http://mmbiz.qpic.cn/sz_mmbiz_jpg/76BCY09cHYSZ60MBaHyHOJPibesjP6ibeRziabuSHZ4jYrITTlmVqFazCzlm8DdQ8QRUrfCulTNrEUyhUcydUiayuQ/0]]></PicUrl><Url><![CDATA[http://www.soso.com/]]></Url></item></Articles></xml>";
return message;
}
//获取公众号的全局唯一接口调用凭据access_token,失效时间120分钟
@GetMapping(value = "/getAccessToken")
public String getAccessToken(){
// 请求地址
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}";
//提交参数设置,这里map里面的key要与请求地址里面占位符名一样,{username}对应map的key:username
Map<String, Object> map = new HashMap<>();
map.put("appid", "你的测试公众号appID");
map.put("secret", "你的测试公众号appsecret");
//请求接口
JSONObject result = restTemplate.getForObject(url, JSONObject.class,map);
//80_Zd7nT4s07RA6gAu4F5kyM8xlpZvPijpnT4CJQr687DXdUcuVRR9SvrE4xGBtilov5sBC_2hnYQlNs34qINDSgcTLL4tdGEi1usXMEJMY-pxYS1wbLZQm7KK1s-0KFDfAFABWU
System.out.println(result.get("access_token"));
return result.get("access_token").toString();
}
//创建菜单
@GetMapping(value = "/createMenu")
public String createMenu(){
//创建一级菜单
Button button=new Button();
List<AbstractButton> buttons=new ArrayList<>();
//一级菜单中的第一个菜单按钮(二级菜单)
SubButton subButton1=new SubButton("菜单1");
List<AbstractButton> subButtons1=new ArrayList<>();
//二级菜单菜单1的第一个按钮
ClickButton clickButton1=new ClickButton("菜单1子级1");
clickButton1.setKey("菜单1子级1");
subButtons1.add(clickButton1);
//二级菜单菜单1的第二个按钮
ClickButton clickButton2=new ClickButton("菜单1子级2");
clickButton2.setKey("菜单1子级2");
subButtons1.add(clickButton2);
subButton1.setSub_button(subButtons1);
//一级菜单中的第二个菜单按钮(二级菜单)
SubButton subButton2=new SubButton("菜单2");
List<AbstractButton> subButtons2=new ArrayList<>();
//二级菜单菜单2的第一个按钮
ClickButton clickButton3=new ClickButton("菜单2子级1");
clickButton3.setKey("菜单2子级1");
subButtons2.add(clickButton3);
//二级菜单菜单2的第二个按钮
ClickButton clickButton4=new ClickButton("菜单2子级2");
clickButton4.setKey("菜单2子级2");
subButtons2.add(clickButton4);
//二级菜单菜单2的第三个按钮
ClickButton clickButton5=new ClickButton("菜单2子级3");
clickButton5.setKey("菜单2子级3");
subButtons2.add(clickButton5);
//二级菜单菜单2的第四个按钮
ClickButton clickButton6=new ClickButton("菜单2子级4");
clickButton6.setKey("菜单2子级4");
subButtons2.add(clickButton6);
//二级菜单菜单2的第五个按钮
ClickButton clickButton7=new ClickButton("菜单2子级5");
clickButton7.setKey("菜单2子级5");
subButtons2.add(clickButton7);
subButton2.setSub_button(subButtons2);
//一级菜单中的第三个菜单按钮(二级菜单)
SubButton subButton3=new SubButton("菜单3");
List<AbstractButton> subButtons3=new ArrayList<>();
//二级菜单菜单3的第一个按钮
ClickButton clickButton8=new ClickButton("菜单3子级1");
clickButton8.setKey("菜单3子级1");
subButtons3.add(clickButton8);
//二级菜单菜单3的第二个按钮
ClickButton clickButton9=new ClickButton("菜单3子级2");
clickButton9.setKey("菜单3子级2");
subButtons3.add(clickButton9);
//二级菜单菜单3的第三个按钮
ClickButton clickButton10=new ClickButton("菜单3子级3");
clickButton10.setKey("菜单3子级3");
subButtons3.add(clickButton10);
subButton3.setSub_button(subButtons3);
//把一级菜单中的三个按钮添加进集合
buttons.add(subButton1);
buttons.add(subButton2);
buttons.add(subButton3);
//把集合添加到一级菜单中
button.setButton(buttons);
System.out.println("菜单数据="+JSONObject.toJSONString(button));
// 请求地址,这里我是先用Postman调用了getAccessToken接口获得了access_token值,在这里直接写死了供测试使用。
String url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=80_Zd7nT4s07RA6gAu4F5kyM8xlpZvPijpnT4CJQr687DXdUcuVRR9SvrE4xGBtilov5sBC_2hnYQlNs34qINDSgcTLL4tdGEi1usXMEJMY-pxYS1wbLZQm7KK1s-0KFDfAFABWU";
// 请求头设置,指定数据以application/json格式的数据格式的数据传递参数
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8"));
headers.add("Accept",MediaType.APPLICATION_JSON.toString());
headers.add("Accept-Charset","UTF-8");
// 组装请求体
HttpEntity<Button> request = new HttpEntity<>(button, headers);
// 发送post请求,并打印结果,以String类型接收响应结果JSON字符串
//第1个参数:请求的url地址
//第2个参数:请求的字段参数加数据格式
//第3个参数:返回的结果类型,这里String.class表示返回结果是一个字符串。
JSONObject result = restTemplate.postForObject(url, request,JSONObject.class);
System.out.println(result);
return result.toString();
}
}
如果微信公众号测试号你本地配置好了,外网也可以访问了,在该项目代码里面,操作如下:
- 修改getAccessToken接口里面的appid值和secret值,改为你自己的。
- 使用Postman调用getAccessToken接口获得access_token值
- 将获取到的access_token写到创建菜单接口/createMenu里面的那个调用微信公众号菜单的接口里面
- 使用Postman调用菜单接口,然后在微信公众号接口测试号里面扫描那个测试号二维码,就能看到你创建的菜单
- 给你自己刚创建的测试公众号发送文本消息或者点击菜单,它都会去调用你在测试号里面配置的那个接口地址,并且是以Post形式,这里我的是Post方法/check这个方法,里面有和微信公众号交互的逻辑,根据用户发送的消息类型来进行匹配并回复相应消息。
这里没有实际效果展示,所以对于第一次学习微信公众号的人来说不好理解,不过你把上面代码全部复制到你的项目里面并进行测试,看控制台打印消息就能理解了,然后就是多看官网文档了,以上全部代码就是这些了。希望对各位小伙伴有帮助吧!