前言:
- 本文只是自己平时的学习记录,所以有点不规范,请见谅
- 本文通过简单的demo练习,带你了解公众号的对接流程
- 简单的功能主要有:(1)个性化菜单:对接聚合数据,完成笑话大全、谜语大全、心灵鸡汤、成语接龙等有趣功能(2)测试模板消息相关(3)对接百度AI开放平台实现网络图片的文字识别功能。
一、关于内网穿透
微信公众号服务器与自己的服务器连接,一般有俩种方式:
(1)将自己的服务器部署到云端上,微信公众号服务器直接与自己的云服务器交互。
(2)微信公众号服务器与本地服务器怎么交互?
可以通过内网穿透的方式,将本地服务器映射到云端(内网穿透就相当于一个代理),微信公众号服务器通过内网穿透映射的地址,访问到本地。
二、关于验证消息是否来自微信服务器
注:下方的这个url取决于你的内网穿透地址 +接口访问地址
- 点击提交:微信会将(signature、timestamp、nonce、echostr)四个参数发送到接口配置中的URL接口中
- 开发者通过检验 signature 对请求进⾏校验(下⾯有校验⽅式)。若确认此次 GET请求来⾃微信服务器,请原样返回 echostr 参数内容,则接⼊⽣效,成为开发者成功,否则接⼊失败。加密/校验流程如下:
-
- (1)将token、timestamp、nonce三个参数进⾏字典序排序
-
- (2)将三个参数字符串拼接成⼀个字符串进⾏sha1加密
-
- (3)开发者获得加密后的字符串可与 signature 对⽐,
-
- (4)如果加密后的字符串=signature ,标识该请求来源于微信,返回echostr参数内容即可。
- 有个问题:进行sha1加密后,得到的是一个byte数组,那么怎么将该数组转化成一个字符串呢?
-
- byte[] digest; 使用new String(digest)转化?不行,这种方式会按照string的内置的编码器对byte数组解码,处理过后的数据肯定不会等于signature 字符串,可能还会乱码。
-
- 使用digest.toString()?也不行,原因如上。
-
-
-
-
- 解决问题?
- 由数据可知signature字符串,每一个元素都是一个16进制的元素(16进制:0~f)
- 而byte数组中,每一个元素都是一个字节,而一个字节是8位二进制的数。(byte[]数组是二进制的)
- 注:在byte[]数组中,每个元素都占据一个字节的空间,并用8个二进制位来表示其值。
- 那么假设byte[]数组中的第一个元素为 1011 0001 (8位二进制数表示)。那么byte数组转化成16进制的字符串就相当于,二进制—》16进制的转化。即4位一体,1011转化为16进制的值为:11—》B ,0001转化为16进制为:1 ,则(二进制)10110001—》(16进制)b1。
- 由上述可知,byte[]数组中一个元素,就是一个字节,就是8位2进制数,就相当于16进制字符串signature中的2个元素。
- 关于byte[]数组转16进制字符串还有好多方法:Java – 如何将字节数组转换为十六进制数组转16进制字符串allway2的博客-CSDN博客
三、接收普通消息
基础消息能力:接收普通消息:文本消息 | 微信开放文档
1、练习一:接受消息,在控制台输出,查看消息结构
- 由官方文档可知,发送的是Post请求,格式是xml的格式
- 代码:
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
byte[] bytes = new byte[1024];
System.out.println("inputStream:"+inputStream);
int len = 0;
while ((len=inputStream.read(bytes))!=-1){
System.out.println(new String(bytes, 0, len));
}
return "hello";
}
- 使用微信扫描测试公众号,进行测试
-
-
-
-
- 向测试号发送消息(后端服务已经启动)
-
- 在后端idea控制台中查看结果
-
- 发送的文本
-
-
-
-
-
-
- 发送的图片
-
-
-
-
-
2、练习二,解析用户消息,封装成map结构
- 代码
<!-- DOM4J是 dom4j.org 出品的一个开源XML解析包-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
return "";
}
- 结果:封装成map结构
-
-
-
四、接收消息后,做出回复文本消息
- 由官方文档可知,消息回复的格式必须是xml结构。格式如下
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>
1、练习一:简单的回复
- 代码:
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
String message ="<xml> " +
"<ToUserName><![CDATA[okuVs5iLGbRWwjjotu8EsKVURyDM]]></ToUserName>" +
" <FromUserName><![CDATA[gh_54b74a688c3b]]></FromUserName>" +
" <CreateTime>1692685746</CreateTime> <MsgType><![CDATA[text]]>" +
"</MsgType> <Content><![CDATA[你好]]></Content> " +
"</xml>";
return message;
}
- 结果
-
-
-
- 需要注意的地方:接收消息与发送消息的ToUserName和FromUserName的值是相反的
-
- 这是接收的消息
{Content=哈哈哈哈哈, CreateTime=1692686018, ToUserName=gh_54b74a688c3b, FromUserName=okuVs5iLGbRWwjjotu8EsKVURyDM, MsgType=text, MsgId=24233435728488437}
-
- 这是发送的消息
"<xml>
<ToUserName><![CDATA[okuVs5iLGbRWwjjotu8EsKVURyDM]]></ToUserName>
<FromUserName><![CDATA[gh_54b74a688c3b]]></FromUserName>
<CreateTime>1692685746</CreateTime> <MsgType><![CDATA[text]]>
</MsgType> <Content><![CDATA[你好]]></Content>
</xml>"
2、练习二:封装消息
将回复的消息XML内容封装为Java对象进⾏操作,更有利于项⽬开发
- 代码:创建text实体
-
-
-
package com.zsh.wx_account1.entity;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 封装text结构的数据
* @Author ZhaoShuHao
* @Date 2023/8/22 14:50
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@XStreamAlias("xml")//修改类,字段,属性别名
public class TextMessage {
@XStreamAlias("ToUserName")
private String toUserName;
@XStreamAlias("FromUserName")
private String fromUserName;
@XStreamAlias("CreateTime")
private long createTime;
@XStreamAlias("MsgType")
private String msgType;
@XStreamAlias("Content")
private String content;
}
- 创建方法,用来封装结构
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
//回复消息的格式必须为xml的格式
//封装消息回复
String message = getReplyMessageByWord(map);
return message;
}
//封装消息格式
private String getReplyMessageByWord(Map<String, String> map) {
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
textMessage.setContent("哈哈哈哈哈哈哈哈哈");
textMessage.setCreateTime(System.currentTimeMillis()/1000);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);
String xml = xStream.toXML(textMessage);
return xml;
}
- 结果:
-
-
-
3、练习三:消息处理案例-接⼊第三⽅服务接⼝
我们以同义词为例
- Sdk的网址(这里有聚合数据的接口示例代码):最新 - sdk社区 | 技术至上
- 在聚合数据网址里,找到同义词与反义词
-
-
-
-
- 这个没有示例代码,我们可以从其他api进行跳转,例如笑话大全
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 开始代码编写
-
- 引入依赖
<!-- 和fastjson作用一样,对json进行解析与构建-->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.3</version>
<classifier>jdk15</classifier>
</dependency>
-
- 编写util工具类
package com.zsh.wx_account1.util;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* @Author ZhaoShuHao
* @Date 2023/8/22 16:36
*/
public class WordUtil {
//聚合数据网站的api接口(同义词/反义词查询)
public static final String WORD_URL = "http://apis.juhe.cn/tyfy/query?key=%s";
//申请接⼝的请求key
// TODO: 您需要改为⾃⼰的请求key
public static final String KEY = "fe4c67727c5cc2cbc12094a563c44305";
public static String getWords(String word) {
//发送http请求的url
String url = String.format(WORD_URL, KEY);
final String response = doPost(url,"word="+word);
System.out.println("接⼝返回:" + response);
try {
JSONObject jsonObject =
JSONObject.fromObject(response);
int error_code = jsonObject.getInt("error_code");
if (error_code == 0) {
System.out.println("调⽤接⼝成功");
JSONObject result =
jsonObject.getJSONObject("result");
JSONArray words = result.getJSONArray("words");
StringBuilder stringBuilder = new
StringBuilder();
words.stream().forEach(w->stringBuilder.append(w+" "));
System.out.println(stringBuilder);
return stringBuilder.toString();
} else {
System.out.println("调⽤接⼝失败:" +
jsonObject.getString("reason"));
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* get⽅式的http请求
*
* @param httpUrl 请求地址
* @return 返回结果
*/
public static String doGet(String httpUrl) {
HttpURLConnection connection = null;
InputStream inputStream = null;
BufferedReader bufferedReader = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpUrl);
// 通过远程url连接对象打开⼀个连接,强转成httpURLConnection类
connection = (HttpURLConnection)
url.openConnection();
// 设置连接⽅式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输⼊流
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 封装输⼊流,并指定字符集
bufferedReader = new BufferedReader(new
InputStreamReader(inputStream, StandardCharsets.UTF_8));
// 存放数据
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = bufferedReader.readLine()) !=
null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();// 关闭远程连接
}
}
return result;
}
/**
* post⽅式的http请求
*
* @param httpUrl 请求地址
* @param param 请求参数
* @return 返回结果
*/
public static String doPost(String httpUrl, String param) {
HttpURLConnection connection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
String result = null;
try {
java.net.URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection)
url.openConnection();
// 设置连接请求⽅式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 设置传⼊参数的格式:请求参数应该是name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
// 通过连接对象获取⼀个输出流
outputStream = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
outputStream.write(param.getBytes());
// 通过连接对象获取⼀个输⼊流,向远程读取
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 对输⼊流对象进⾏包装:charset根据⼯作项⽬组的要求来设置
bufferedReader = new BufferedReader(new
InputStreamReader(inputStream, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
// 循环遍历⼀⾏⼀⾏读取数据
while ((temp = bufferedReader.readLine()) !=
null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();
}
}
return result;
}
}
-
- 编写同义词实现
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
//回复消息的格式必须为xml的格式
//3、消息处理案例-接⼊第三⽅服务接⼝
String message = getReplyMessageByWord(map);
return message;
}
//3.消息处理案例-接⼊第三⽅服务接⼝
private String getReplyMessageByWord(Map<String, String> map)
{
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
String content = WordUtil.getWords(map.get("Content"));
textMessage.setContent(content);
textMessage.setCreateTime(System.currentTimeMillis()/1000);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);
String xml = xStream.toXML(textMessage);
return xml;
}
- 结果展示
-
-
-
-
-
-
五、接收消息后,回复图文消息
关于图文消息:回复文本消息 | 微信开放文档
根据官方文档,创建对应的实体,用来组装结构
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>1</ArticleCount>
<Articles>
<item>
<Title><![CDATA[title1]]></Title>
<Description><![CDATA[description1]]></Description>
<PicUrl><![CDATA[picurl]]></PicUrl>
<Url><![CDATA[url]]></Url>
</item>
</Articles>
</xml>
1、新建图文消息实体
@Data
@AllArgsConstructor
@NoArgsConstructor
@XStreamAlias("xml")
public class NewsMessage {
@XStreamAlias("ToUserName")
private String toUserName;
@XStreamAlias("FromUserName")
private String fromUserName;
@XStreamAlias("CreateTime")
private long createTime;
@XStreamAlias("MsgType")
private String msgType;
@XStreamAlias("ArticleCount")
private int articleCount;
@XStreamAlias("Articles")
private List<Article> articles;
}
/**图文信息的Article
* @Author ZhaoShuHao
* @Date 2023/8/22 17:27
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@XStreamAlias("item")
public class Article {
@XStreamAlias("Title")
private String title;
@XStreamAlias("Description")
private String description;
@XStreamAlias("PicUrl")
private String picUrl;
@XStreamAlias("Url")
private String url;
}
2、创建组装图文消息的方法
//获取图文消息
private String getReplyNewsMessage(Map<String, String> map) {
NewsMessage newsMessage = new NewsMessage();
newsMessage.setToUserName(map.get("FromUserName"));
newsMessage.setFromUserName(map.get("ToUserName"));
newsMessage.setMsgType("news");
newsMessage.setCreateTime(System.currentTimeMillis()/1000);
newsMessage.setArticleCount(1);
List<Article> articles = new ArrayList<>();
Article article = new Article();
article.setTitle("要开心呀的csdn个人中心");
article.setDescription("要开心呀的csdn个人中心,个人笔记、学习记录");
article.setUrl("https://blog.csdn.net/ZhShH0413?spm=1000.2115.3001.5343");
article.setPicUrl("http://mmbiz.qpic.cn/sz_mmbiz_jpg/IPwvTbhAHQL1rPJgNdCuD2udSmC7icF7bFtqdOejlAIFQo9HibTFLggqtbRfObia9XP90f9gSQ2mYWApBkSjcl1lA/0");
articles.add(article);
newsMessage.setArticles(articles);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(NewsMessage.class);
String xml = xStream.toXML(newsMessage);
return xml;
}
3、返回消息
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
//回复消息的格式必须为xml的格式
//2、封装消息回复
String message = null;
if(map.get("Content").equals("图文")){
message = getReplyNewsMessage(map);
}else {
//3、消息处理案例-接⼊第三⽅服务接⼝(获取同义词)
message = getReplyMessageByWord(map);
}
return message;
}
六、获得Access_token
官方文档:微信开放文档
1、创建HttpUtil工具类,将wordUtil中的doGet()和doPost()请求拿出来,封装到HttpUtil工具类中
package com.zsh.wx_account1.util;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* @Author ZhaoShuHao
* @Date 2023/8/23 9:35
*/
public class HttpUtil {
/**
* get⽅式的http请求
*
* @param httpUrl 请求地址
* @return 返回结果
*/
public static String doGet(String httpUrl) {
HttpURLConnection connection = null;
InputStream inputStream = null;
BufferedReader bufferedReader = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpUrl);
// 通过远程url连接对象打开⼀个连接,强转成httpURLConnection类
connection = (HttpURLConnection)
url.openConnection();
// 设置连接⽅式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输⼊流
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 封装输⼊流,并指定字符集
bufferedReader = new BufferedReader(new
InputStreamReader(inputStream, StandardCharsets.UTF_8));
// 存放数据
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = bufferedReader.readLine()) !=
null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();// 关闭远程连接
}
}
return result;
}
/**
* post⽅式的http请求
*
* @param httpUrl 请求地址
* @param param 请求参数
* @return 返回结果
*/
public static String doPost(String httpUrl, String param) {
HttpURLConnection connection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
String result = null;
try {
java.net.URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection)
url.openConnection();
// 设置连接请求⽅式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 设置传⼊参数的格式:请求参数应该是name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
// 通过连接对象获取⼀个输出流
outputStream = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
outputStream.write(param.getBytes());
// 通过连接对象获取⼀个输⼊流,向远程读取
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 对输⼊流对象进⾏包装:charset根据⼯作项⽬组的要求来设置
bufferedReader = new BufferedReader(new
InputStreamReader(inputStream, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
// 循环遍历⼀⾏⼀⾏读取数据
while ((temp = bufferedReader.readLine()) !=
null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();
}
}
return result;
}
}
2、创建AccessToken实体,用来接accessToken,并判断是否超时
package com.zsh.wx_account1.entity;
/**
* Access token实体
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
public class AccessToken {
//token的值
private String token;
//超时时间
private long expireTime;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public long getExpireTime() {
return expireTime;
}
/**
* 官方文档规定access_token的有效期目前为2个小时
* System.currentTimeMillis()单位是毫秒,当前系统时间
* expireIn单位是秒,*1000转化为毫秒
* 当前系统时间往后推2小时就是过期时间
* @param expireIn
*/
public void setExpireTime(long expireIn) {
this.expireTime = System.currentTimeMillis() + expireIn * 1000;
}
/**
* 判断是否超时
* 当前系统时间>过期时间,就代表超时
* @return
*/
public boolean isExpired(){
return System.currentTimeMillis()>this.expireTime;
}
}
3、创建TokenUtil工具类,获取accessToken
package com.zsh.wx_account1.util;
import com.zsh.wx_account1.entity.AccessToken;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.ObjectUtils;
/**
* 获取 Access token的工具类
* @Author ZhaoShuHao
* @Date 2023/8/23 9:44
*/
public class TokenUtil {
//自己的公众号信息
private static final String APP_ID = "wx43f47bd394a23feb";
private static final String APP_SECRET = "1a29d7b96b7403c99b20d563057a10c8";
private static AccessToken accessToken = new AccessToken();
public static void main(String[] args) {
//{"access_token":"63_8R2EcPuM3dz_D81Q2FBiSfgrlwokafQloAU33iFhHIbjabRFtC_thRqk7VOkMbarQ8lA9yyq2pgwh4pc6P-5qQutc6WWMLwFafIR6ZaLkB299OJU78npFt--I0ACXCiACAHCH","expires_in":7200}
System.out.println(getAccessToken());
}
private static void getToken(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
APP_ID,
APP_SECRET);
String result = HttpUtil.doGet(url);
if(StringUtils.isNotBlank(result)){
JSONObject jsonObject = JSONObject.fromObject(result);
String token = jsonObject.getString("access_token");
long expiresIn = jsonObject.getLong("expires_in");
accessToken.setToken(token);
accessToken.setExpireTime(expiresIn);
}
}
/**
* 获取AccessToken
* @return
*/
public static String getAccessToken(){
//如果accessToken为空或者超时,都重新获取token
if(ObjectUtils.isEmpty(accessToken) || accessToken.isExpired()){
getToken();
}
return accessToken.getToken();
}
}
七、自定义菜单
{
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"miniprogram",
"name":"wxa",
"url":"http://mp.weixin.qq.com",
"appid":"wx286b93c14bbf93aa",
"pagepath":"pages/lunar/index"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}]
}]
}
1、生成简单的菜单
1.1 根据官方文档以及按钮的结构,组装按钮实体
/**
* button按钮的抽象类,抽出共同的属性
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public abstract class AbstractButton {
private String name;
}
/**
* 一级菜单
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
public class Button {
private List<AbstractButton> button;
public List<AbstractButton> getButton() {
return button;
}
public void setButton(List<AbstractButton> button) {
this.button = button;
}
}
/**
* 二级菜单
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
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;
}
}
/**
* 点击按钮
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
public class ClickButton extends AbstractButton{
public ClickButton(String name) {
super(name);
this.type = "click";
this.key = String.valueOf(UUID.randomUUID());
}
private String type;
private String key;
public String getType() {
return type;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
/**
* 弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
public class PhotoOrAlbumButton extends AbstractButton{
public PhotoOrAlbumButton(String name) {
super(name);
this.type = "pic_photo_or_album";
this.key = String.valueOf(UUID.randomUUID());
}
private String type;
private String key;
public String getType() {
return type;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
/**
* 跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,
* 可与网页授权获取用户基本信息接口结合,获得用户基本信息。
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
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 getType() {
return type;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
1.2 编写测试类,生成简单的菜单
/**
* 二级菜单
* @Author ZhaoShuHao
* @Date 2023/8/23 9:37
*/
public class TestButton {
public static void main(String[] args) {
//创建一级菜单
Button button = new Button();
List<AbstractButton> buttons = new ArrayList<>();
//一级菜单中的第一个按钮
ClickButton clickButton = new ClickButton("csdn");
//一级菜单中的第二个按钮
ViewButton viewButton = new ViewButton("百度","http://www.baidu.com");
//一级菜单中的第三个按钮(二级菜单)
SubButton subButton = new SubButton("更多");
List<AbstractButton> subButtons = new ArrayList<>();
//二级菜单的第一个按钮
subButtons.add(new ViewButton("csdn个人中心","https://blog.csdn.net/ZhShH0413?spm=1010.2135.3001.5343"));
//二级菜单的第二个按钮
subButtons.add(new PhotoOrAlbumButton("上传图片"));
subButton.setSub_button(subButtons);
//把一级菜单中的三个按钮添加进集合
buttons.add(clickButton);
buttons.add(viewButton);
buttons.add(subButton);
//把集合添加到一级菜单中
button.setButton(buttons);
//转换成json字符串
JSONObject jsonObject = JSONObject.fromObject(button);
System.out.println(jsonObject);
String json = jsonObject.toString();
String url = String.format("https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s", TokenUtil.getAccessToken());
//发送请求
String result = HttpUtil.doPost(url, json,"application/json;charset=utf-8");
System.out.println(result);
}
}
1.3 结果(需取消关注公众号,再次关注即可看到最新)
2、处理自定义菜单事件
2.1 添加处理事件方法
/**
* 处理事件推送
* @param map
* @return
*/
private String handleEvent(Map<String, String> map) {
String message = null;
String event = map.get("Event");
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
switch (event){
case "CLICK":
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
textMessage.setContent("你点击了event key是"+map.get("EventKey")+"的按钮");
textMessage.setCreateTime(System.currentTimeMillis()/1000);
xStream.processAnnotations(TextMessage.class);
message = xStream.toXML(textMessage);
break;
case "VIEW":
System.out.println("view");
break;
case "pic_photo_or_album":
System.out.println("pic_photo_or_album");
default:
break;
}
return message;
}
2.2 测试事件处理
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
//回复消息的格式必须为xml的格式
String message = null;
String msgType = map.get("MsgType");
switch (msgType){
case "text":
if(map.get("Content").equals("图文")){
//回复图文信息
message = getReplyNewsMessage(map);
}else {
//3、消息处理案例-接⼊第三⽅服务接⼝(获取同义词)
message = getReplyMessageByWord(map);
}
break;
case "event":
//处理事件
message = handleEvent(map);
break;
case "image":
System.out.println("您发了一张图片");
break;
default:
break;
}
return message;
}
八、文字识别功能
借用百度AI开放平台实现网络图片的文字识别功能。
操作流程如下:
1、先在百度AI控制台中,添加应用
可以在应用列表中查看创建的应用服务:
2、查看api教程,进入文档中心,选择文字识别,网络文字识别
3、引入依赖
<!-- 引入百度ai的依赖 -->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.12</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
4、实现图片识别
//百度AI的个人相关信息
public static final String APP_ID = "38135852";
public static final String API_KEY = "GimuujuwA63R1xqYtonSC3Fw";
public static final String SECRET_KEY = "0K2bfiGowe4cEG9yvRoAr35GUG9dC4YV";
private String handleImage(Map<String, String> map) {
// 初始化⼀个AipOcr
AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
// ⽹络图⽚⽂字识别, 图⽚参数为远程url图⽚
String url = map.get("PicUrl");
JSONObject res = client.webImageUrl(url, new HashMap<String,String>());
System.out.println(res.toString(2));
JSONArray wordsResult = res.getJSONArray("words_result");
StringBuilder stringBuilder = new StringBuilder();
Iterator<Object> iterator = wordsResult.iterator();
while(iterator.hasNext()){
JSONObject next = (JSONObject) iterator.next();
stringBuilder.append(next.getString("words"));
}
return assembleTextXMLFormat(stringBuilder.toString(),map);
}
//获取用户向测试号发送的消息,并解析,封装成map结构
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException{
ServletInputStream inputStream = request.getInputStream();
Map<String, String> map = new HashMap<>();
//获取xml解析工具类
SAXReader saxReader = new SAXReader();
try {
//读取request输⼊流,获得Document对象
Document document = saxReader.read(inputStream);
//获得root节点
Element rootElement = document.getRootElement();
//获取所有子节点
List<Element> elements = rootElement.elements();
elements.stream().forEach(new Consumer<Element>() {
@Override
public void accept(Element element) {
map.put(element.getName(),element.getStringValue());
}
});
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println("消息为:"+map);
//回复消息的格式必须为xml的格式
String message = null;
String msgType = map.get("MsgType");
switch (msgType){
case "text":
if(map.get("Content").equals("图文")){
//回复图文信息
message = getReplyNewsMessage(map);
}else {
//3、消息处理案例-接⼊第三⽅服务接⼝(获取同义词)
message = getReplyMessageByWord(map);
}
break;
case "event":
//处理事件
message = handleEvent(map);
break;
case "image":
message=handleImage(map);
break;
default:
break;
}
System.out.println(message);
return message;
}
5、结果展示
九、模版消息
注:在设置模版消息之前,要先设置所属行业信息
1、设置所属行业
- 编码:
//测试设置所属行业(模版消息的前置条件)
@Test
public void testSetTrade(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=%s", TokenUtil.getAccessToken());
String data = "{\n" +
" \"industry_id1\":\"1\",\n" +
" \"industry_id2\":\"4\"\n" +
"}";
String s = HttpUtil.doPost(url, data, "application/x-www-form-urlencoded");
System.out.println(s);
}
2、查询所属行业
- 编码
//测试查询所属行业
@Test
public void testGetTrade(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/template/get_industry?access_token=%s",TokenUtil.getAccessToken());
System.out.println(HttpUtil.doGet(url));
}
- 结果
3、获取模版列表
//获取模版列表
@Test
public void testGetModelMessage(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=%s", TokenUtil.getAccessToken());
String s = HttpUtil.doGet(url);
System.out.println(s);
}
4、测试发送模版消息
- 注:最新的模版已经取消了颜色、标题、描述信息
-
- 旧模版:
{
{first.DATA}}
参赛队伍:{
{keyword1.DATA}}
比赛时间:{
{keyword2.DATA}}
比赛地点:{
{keyword3.DATA}}
已报名人数:{
{keyword4.DATA}}
{
{remark.DATA}}
-
- 新模版:
参赛队伍:{
{keyword1.DATA}}
比赛时间:{
{keyword2.DATA}}
比赛地点:{
{keyword3.DATA}}
已报名人数:{
{keyword4.DATA}}
- 编码:
//测试发送模版消息
@Test
public void testModelmessage(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", TokenUtil.getAccessToken());
String data = "{\n" +
" \"touser\":\"okuVs5iLGbRWwjjotu8EsKVURyDM\",\n" +
" \"template_id\":\"EOlNyURgdR1rnZx3_x_k9iHb4j5CZ3dshCZxZbCsU1w\",\n" +
" \"url\":\"https://blog.csdn.net/ZhShH0413?spm=1010.2135.3001.5343\",\n" +
" \"data\":{\n" +
" \"keyword1\": {\n" +
" \"value\":\"张三,恭喜您报名比赛成功\"\n" +
" },\n" +
" \"keyword2\":{\n" +
" \"value\":\"A队\"\n" +
" },\n" +
" \"keyword3\": {\n" +
" \"value\":\"2023年8月26日 6:00\"\n" +
" },\n" +
" \"keyword4\": {\n" +
" \"value\":\"XX集团东大门\"\n" +
" },\n" +
" \"keyword5\": {\n" +
" \"value\":\"A队1人,B队66人\"\n" +
" },\n" +
" \"keyword6\":{\n" +
" \"value\":\"点击链接有惊喜,双击关注666\"\n" +
" }\n" +
" }\n" +
" }";
System.out.println(HttpUtil.doPost(url, data,"application/x-www-form-urlencoded"));
}
- 结果:
-
-
-
十、临时素材管理
1、新增临时素材
- 准备素材
-
-
-
-
- 开始上传素材
//上传临时素材
@Test
public void testImage(){
//这里上传了一个图片
String url = String.format("https://api.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s",
TokenUtil.getAccessToken(),
"image");
String result = HttpUtil.doPostByFile(url, null, "src/main/resources/static/image/image1.jpg", "");
System.out.println(result);
//{"type":"image","media_id":"gw-BxwWm3mP6T2M_OBR1IzPwl6YcFFncr4fx3CrS-wtJOd78l7evdmUm5e3b2BZP","created_at":1692935476,"item":[]}
}
2、获取临时素材
- 获取临时素材
//获得临时素材(mediaId就是上传图片,返回的图片id)
@Test
public void testGetImage(){
String mediaId = "gw-BxwWm3mP6T2M_OBR1IzPwl6YcFFncr4fx3CrS-wtJOd78l7evdmUm5e3b2BZP";
String url = String.format("https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s",
TokenUtil.getAccessToken(),
mediaId);
String result = HttpUtil.doGet(url);
System.out.println(result);
}
- 图片在idea中不能看,拼接个地址,在浏览器查看
/*
图片地址:https://api.weixin.qq.com/cgi-bin/media/get?access_token=token的值&media_id=图片id
https://api.weixin.qq.com/cgi-bin/media/get?access_token=71_3PiSMSASfd1s9s5CFK30XX3CaG3IamKRjT25tmfShuk4i2I3y3Ze0ebR0SSES-hro5T7zO5YVEo0pWiaedG3Yv5y4qRcoETPNRbqJJKOYxpH5ac_rD3CPtfCuZQTGGdAIAYLL&media_id=gw-BxwWm3mP6T2M_OBR1IzPwl6YcFFncr4fx3CrS-wtJOd78l7evdmUm5e3b2BZP
*/
十一、二维码的生成与获取信息
为了满足用户渠道推广分析和用户账号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。使用接口过程中有任何问题,可以前往微信开放社区 #公众号 专区发帖交流。
目前有2种类型的二维码:
1、临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于账号绑定等不要求二维码永久保存的业务场景 2、永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。
2、永久二维码主要用于适用于账号绑定、用户来源统计等场景。
用户扫描带场景值二维码时,可能推送以下两种事件:
如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。
获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借ticket到指定URL换取二维码。
1、生成带参数的二维码
- 编码
//生成带参数的二维码
@Test
public void testQRcode(){
String url = String.format(" https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s",
TokenUtil.getAccessToken());
String data = "{\"expire_seconds\": 604800, \"action_name\": \"QR_SCENE\", \"action_info\": {\"scene\": {\"scene_id\": 123}}}";
String s = HttpUtil.doPost(url, data, "application/x-www-form-urlencoded");
System.out.println(s);
}
- 结果:
{"ticket":"gQE18DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyOHREMVZraUJkSUMxVFRUTk5BMXkAAgR3PehkAwSAOgkA","expire_seconds":604800,"url":"http:\/\/weixin.qq.com\/q\/028tD1VkiBdIC1TTTNNA1y"}
2、通过ticket换取二维码
- 编码
/*
https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET
* https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQEA8DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyeW1TVFVPaUJkSUMxVFhTTk5BMVkAAgR7POhkAwSAOgkA
* */
//通过ticket换取二维码
@Test
public void testGetQRcode(){
String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQEA8DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyeW1TVFVPaUJkSUMxVFhTTk5BMVkAAgR7POhkAwSAOgkA";
String s = HttpUtil.doGet(url);
System.out.println(s);
}
- 通过该url,在浏览器查看二维码
-
-
-
3、扫描二维码,取消关注和关注会有一定的事件,可以配合使用