微信公众号自动回复-底部菜单栏-关键字回复-回复2条消息(1文字,1图片)
主要实现:
1.关注后自动回复文字内容
2.关键字回复图文消息
3.实现公众号自定义底部菜单栏 - 点击菜单栏进入链接, 点击菜单栏回复图文消息, 点击菜单栏回复2条消息(一条文字, 一条图片)
主要参考: https://segmentfault.com/a/1190000015715950
1. 前提准备
需要可以接收微信发来的XML
可以参考上一篇博文:
https://www.cnblogs.com/Annago/p/15411077.html
2. 关注后自动回复文字
/**
* 接收微信的事件
*/
@PostMapping(value = "/weixinVerify", produces = "application/xml")
public void weixinVerify(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType("text/html; charset=utf-8");
response.setContentType("application/xml; charset=utf-8");
PageData pageData = this.getPageData();
String method = request.getMethod();
if ("POST".equals(method)) {
String xmlStr = "";
String msgrsp = "";
PrintWriter out = response.getWriter();
try {
//解析微信发来的XML
xmlStr = XmlUtil.inputStream2StringNew(request.getInputStream());
Map<String, String> requestMap = XmlUtil.parseXml(xmlStr);
//消息类型 -- 事件(event)
if (MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(requestMap.get("msgType"))) {
//关注(subscribe)
if (MessageUtil.EVENT_TYPE_SUBSCRIBE.equals(requestMap.get("event"))) {
String openid = requestMap.get("fromUserName"); //用户openid
String mpid = requestMap.get("toUserName"); //公众号原始ID
//普通文本消息
BaseMessage txtmsg = new BaseMessage();
txtmsg.setToUserName(openid);
txtmsg.setFromUserName(mpid);
txtmsg.setCreateTime(new Date().getTime());
txtmsg.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
txtmsg.setContent("您好,欢迎关注!");
//文本转xml
String msgrsp = MessageUtil.textMessageToXml(txtmsg);
}
}
out.print(msgrsp);
out.close();
} catch (Exception e) {
logger.error("消息事件接收失败");
e.printStackTrace();
}
}
}
需要的jar
<!-- hutool工具集 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.9</version>
</dependency>
BaseMessage
@Data
public class BaseMessage {
//接收方账号(收到的OpenID)
private String ToUserName;
//开发者微信号(公众号)
private String FromUserName;
//消息创建时间(整形)
private long CreateTime;
//消息类型(text,image,location,link,voice)
private String MsgType;
//返回内容
private String Content;
//返回的图片id
private String MediaId;
}
BaseButton
@Data
public class BasicButton {
private String name;
private String type;
}
ViewButton
/**
* @Description: 直接进入链接的按钮
*/
@Data
public class ViewButton extends BasicButton {
private String url;
}
CommonButton
/**
* @Description: 点击的按钮
*/
@Data
public class CommonButton extends BasicButton {
private String key;
}
父按钮
/**
* @Description: 父按钮
*/
@Data
public class ComplexButton extends BasicButton {
private BasicButton[] sub_button;
}
菜单
/**
* @Description: 菜单
*/
@Data
public class Menu {
private BasicButton[] button;
}
MessageUtil
import java.io.*;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.servlet.http.HttpServletRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.utils.weixin.*;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
public class MessageUtil {
/**
* 返回消息类型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/**
* 返回消息类型:音乐
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/**
* 返回消息类型:图文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
/**
* 请求消息类型:文本
*/
public static final String REQ_MESSAGE_TYPE_TEXT = "text";
/**
* 请求消息类型:图片
*/
public static final String REQ_MESSAGE_TYPE_IMAGE = "image";
/**
* 请求消息类型:链接
*/
public static final String REQ_MESSAGE_TYPE_LINK = "link";
/**
* 请求消息类型:地理位置
*/
public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
/**
* 请求消息类型:音频
*/
public static final String REQ_MESSAGE_TYPE_VOICE = "voice";
/**
* 请求消息类型:推送
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/**
* 事件类型:subscribe(订阅)
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/**
* 事件类型:unsubscribe(取消订阅)
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
/**
* 事件类型:CLICK(自定义菜单点击事件)
*/
public static final String EVENT_TYPE_CLICK = "CLICK";
/**
* 事件类型:自己设置的KEY
*/
public static final String KEY = "设置什么KEY, 监听到什么值";
// 菜单创建(POST) 限100(次/天)
public static String MENU_CREATE_RUL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
/**
* 解析微信发来的请求(XML)
*
* @param request
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
/**
* 扩展xstream,使其支持CDATA块
*
* @date 2016-01-15
*/
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true;
@SuppressWarnings("unchecked")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});
/**
* 文本转xml
*/
public static String textMessageToXml(BaseMessage baseMessage) {
xstream.alias("xml", baseMessage.getClass());
return xstream.toXML(baseMessage);
}
/**
* 图文转xml
*/
public static String newsMessageToXml(BaseMessage baseMessage) {
xstream.alias("xml", baseMessage.getClass());
xstream.alias("item", new Article().getClass());
return xstream.toXML(baseMessage);
}
/**
* 图片转xml
*/
public static String imageMessageToXml(ImageMessage imageMessage) {
xstream.alias("xml", imageMessage.getClass());
return xstream.toXML(imageMessage);
}
/**
* 发送https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
try {
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
jsonObject = JSONObject.parseObject(buffer.toString());
} catch (Exception e) {
e.printStackTrace();
}
return jsonObject;
}
/**
* 组装菜单数据(父子菜单栏), 直接拉到底下看父菜单
* 1.如果菜单类型是:view, 点击菜单则会进入到url中(进入链接)
* 2.类型是:click, 点击, 微信会传入XML, 里面有"eventKey"字段, 对应你设置的MessageUtil.KEY(监听到"eventKey"= 你设置的key)
*/
public static Menu getMenu() {
//左边的菜单栏------------------
ViewButton btn11 = new ViewButton();
btn11.setName("资讯月刊");
btn11.setType("view");
btn11.setUrl("https://blog.csdn.net/yuanpeij/article/details/120776759");
CommonButton btn12 = new CommonButton();
btn12.setName("行业动态");
btn12.setType("click");
btn12.setKey(MessageUtil.KEY);
CommonButton btn13 = new CommonButton();
btn13.setName("XX动态");
btn13.setType("click");
btn13.setKey(MessageUtil.KEY);
//中间的菜单栏-------------
ViewButton btn21 = new ViewButton();
btn21.setName("专业研究");
btn21.setType("view");
btn21.setUrl("https://blog.csdn.net/yuanpeij/article/details/120776759");
ViewButton btn22 = new ViewButton();
btn22.setName("XX观点");
btn22.setType("view");
btn22.setUrl("https://blog.csdn.net/yuanpeij/article/details/120776759");
ViewButton btn23 = new ViewButton();
btn23.setName("主题演讲");
btn23.setType("view");
btn23.setUrl("https://blog.csdn.net/yuanpeij/article/details/120776759");
//右边的菜单栏-------------
CommonButton btn31 = new CommonButton();
btn31.setName("XX简介");
btn31.setType("click");
btn31.setKey(MessageUtil.KEY);
CommonButton btn32 = new CommonButton();
btn32.setName("业务类型");
btn32.setType("click");
btn32.setKey(MessageUtil.KEY);
CommonButton btn33 = new CommonButton();
btn33.setName("招贤纳士");
btn33.setType("click");
btn33.setKey(MessageUtil.KEY);
CommonButton btn34 = new CommonButton();
btn34.setName("XX路线");
btn34.setType("click");
btn34.setKey(MessageUtil.KEY);
CommonButton btn35 = new CommonButton();
btn35.setName("XXXX");
btn35.setType("click");
btn35.setKey(MessageUtil.KEY);
//三个主菜单栏, 底下包含各自的子菜单
ComplexButton mainBtn1 = new ComplexButton();
mainBtn1.setName("XX资讯");
mainBtn1.setSub_button(new BasicButton[] { btn11, btn12, btn13});
ComplexButton mainBtn2 = new ComplexButton();
mainBtn2.setName("专业分享");
mainBtn2.setSub_button(new BasicButton[] { btn21, btn22, btn23});
ComplexButton mainBtn3 = new ComplexButton();
mainBtn3.setName("关于XX");
mainBtn3.setSub_button(new BasicButton[] { btn31, btn32, btn33, btn34, btn35});
Menu menu = new Menu();
menu.setButton(new BasicButton[] { mainBtn1, mainBtn2, mainBtn3});
return menu;
}
/**
* 创建菜单
*
* @param menu 菜单实例
* @param accessToken 有效的access_token
* @return 0表示成功,其他值表示失败
*/
public static int createMenu(Menu menu, String accessToken) {
int result = 0;
// 拼装创建菜单的url
String url = MENU_CREATE_RUL.replace("ACCESS_TOKEN", accessToken);
// 将菜单对象转换成json字符串
String jsonMenu = JSON.toJSON(menu).toString();
// 调用接口创建菜单
JSONObject jsonObject = httpsRequest(url, "POST", jsonMenu);
if (null != jsonObject) {
if (0 != jsonObject.getInteger("errcode")) {
result = jsonObject.getInteger("errcode");
System.out.println("创建菜单失败 errcode:{} errmsg:{}" + jsonObject.getInteger("errcode") + jsonObject.getString("errmsg"));
}
}
return result;
}
}
3.自定义菜单栏
@Test
public void t() throws Exception {
JSONObject accessToken = GetReqUtil.getAccess_token();
MessageUtil.createMenu(MessageUtil.getMenu(), accessToken.getString("access_token"));
}
public static JSONObject getAccess_token() {
String url = "https://sz.api.weixin.qq.com/cgi-bin/token";
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("appid", "微信公众号的appid");
paramMap.put("secret", "微信公众号的secret");
paramMap.put("grant_type", "client_credential");
String result2 = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
.header(Header.USER_AGENT, "Hutool http")//头信息,多个头信息多次调用此方法即可
.form(paramMap)//表单内容
.timeout(20000)//超时,毫秒
.execute().body();
JSONObject jsonObject = JSONObject.parseObject(result2); //字符串转Map
return jsonObject;
}
1.自定义菜单栏示例图:
2.点击自定义菜单栏回复图文
//消息类型 -- 事件(event)
if (MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(requestMap.get("msgType"))) {
if (MessageUtil.KEY.equals(requestMap.get("eventKey"))) {
String openid = requestMap.get("fromUserName"); //用户openid
String mpid = requestMap.get("toUserName"); //公众号原始ID
//对图文消息
NewsMessage newmsg = new NewsMessage();
newmsg.setToUserName(openid);
newmsg.setFromUserName(mpid);
newmsg.setCreateTime(new Date().getTime());
newmsg.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);
List<Article> articleList = new ArrayList<>();
Article article = new Article();
if (MessageUtil.INDUSTRY_TRENDS.equals(requestMap.get("eventKey"))) {
//行业动态
article.setTitle("标题");
article.setDescription("描述");
article.setPicUrl("图片URL, 可以把图片保存到微信公众号里的素材库, 然后使用图片的路径, 或者使用绝对路径");
article.setUrl("链接URL");
}
articleList.add(article);
// 设置图文消息个数
newmsg.setArticleCount(articleList.size());
// 设置图文消息包含的图文集合
newmsg.setArticles(articleList);
String msgrsp = MessageUtil.newsMessageToXml(newmsg);
out.print(msgrsp);
out.close();
}
}
NewMessage
public class NewsMessage extends BaseMessage {
/**
* 图文消息个数,限制为10条以内
*/
private Integer ArticleCount;
/**
* 多条图文消息信息,默认第一个item为大图
*/
private List<Article> Articles;
public Integer getArticleCount() {
return ArticleCount;
}
public NewsMessage setArticleCount(Integer articleCount) {
ArticleCount = articleCount;
return this;
}
public List<Article> getArticles() {
return Articles;
}
public NewsMessage setArticles(List<Article> articles) {
Articles = articles;
return this;
}
Article
@Data
public class Article {
/**
* 图文消息描述
*/
private String Description;
/**
* 图片链接,支持JPG、PNG格式,<br>
* 较好的效果为大图640*320,小图80*80
*/
private String PicUrl;
/**
* 图文消息名称
*/
private String Title;
/**
* 点击图文消息跳转链接
*/
private String Url;
}
回复图文示例图:
微信现在不支持默认回复大图了, 如果需要大图得展示2条数据-> 一条文字, 一张图片
4.关键字回复
//消息类型 -- 文本(text)
if (MessageUtil.REQ_MESSAGE_TYPE_TEXT.equals(requestMap.get("msgType"))) {
//自定义文本
if ("资讯".equals(requestMap.get("content"))) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("XXXX年9月)" + "\n");
stringBuilder.append("链接:XXXXX" + "\n");
//可以让文字变蓝, 用户点击一下蓝色,会自己打出提取码, 然后容易拷贝
stringBuilder.append("提取码:<a href=\"weixin://bizmsgmenu?msgmenuid=1&msgmenucontent=9tdy\">9tdy</a>" + "\n");
String openid = requestMap.get("fromUserName"); //用户openid
String mpid = requestMap.get("toUserName"); //公众号原始ID
//普通文本消息
BaseMessage txtmsg = new BaseMessage();
txtmsg.setToUserName(openid);
txtmsg.setFromUserName(mpid);
txtmsg.setCreateTime(new Date().getTime());
txtmsg.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
txtmsg.setContent(stringBuilder.toString());
String msgrsp = MessageUtil.textMessageToXml(txtmsg);
out.print(msgrsp);
out.close();
}
}
示例图:
5.回复2条消息(1文字, 1图片)
示例图:
微信限制我们每次只能给用户被动回复一条消息, 所以回复2条只能使用客服推送这个功能
1.客服推送文字
public static void processAppletTextMessage(Map<String, String> requestMap) {
String openid = requestMap.get("fromUserName"); //用户openid
JSONObject accessToken = GetReqUtil.getAccess_token();
JSONObject jsonObject = new JSONObject();
JSONObject jsonObject2 = new JSONObject();
jsonObject2.put("content", "请联系工作人员进入小程序体验");
jsonObject.put("touser", openid);
jsonObject.put("msgtype", "text");
jsonObject.put("text", jsonObject2);
HttpUtil.post("https://sz.api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + accessToken.getString("access_token"), jsonObject.toString());
}
2.回复图片信息
public static String processAppletPictureMessage(Map<String, String> requestMap) {
String openid = requestMap.get("fromUserName"); //用户openid
String mpid = requestMap.get("toUserName"); //公众号原始ID
ImageMessage imageMsg = new ImageMessage();
Image image = new Image();
image.setMediaId("上传图片到微信服务器, 获取MediaId, 下面有");
//普通文本消息
imageMsg.setToUserName(openid);
imageMsg.setFromUserName(mpid);
imageMsg.setCreateTime(new Date().getTime());
imageMsg.setMsgType(MessageUtil.REQ_MESSAGE_TYPE_IMAGE);
imageMsg.setImage(image);
return MessageUtil.imageMessageToXml(imageMsg);
}
ImageMessage
@Data
public class ImageMessage extends BaseMessage{
private Image Image;
}
把图片上传到微信素材库(永久上传)
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Test
public void t2() throws Exception {
JSONObject accessToken = GetReqUtil.getAccess_token();
System.out.println("accessToken = " + accessToken.getString("access_token"));
JSONObject jsonObject = GetReqUtil.addMaterialEver("C:\\Users\\Jaime\\Desktop\\工作人员.jpg", "image", accessToken.getString("access_token"));
boolean b=jsonObject.containsKey("media_id");
if (b==true) {
String media_id=jsonObject.getString("media_id");
System.out.println("jsonObject = " + jsonObject);
System.out.println("media_id:"+media_id);
}
}
addMaterialEver
/**
* 上传其他永久素材(图片素材的上限为5000,其他类型为1000)
*
* @return
* @throws Exception
*/
public static JSONObject addMaterialEver(String fileurl, String type, String token) {
try {
File file = new File(fileurl);
//上传素材
String path = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=" + token + "&type=" + type;
String result = connectHttpsByPost(path, null, file);
result = result.replaceAll("[\\\\]", "");
JSONObject resultJSON = JSONObject.parseObject(result);
if (resultJSON != null) {
if (resultJSON.get("media_id") != null) {
System.out.println("true");
return resultJSON;
} else {
System.out.println("false");
}
}
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String connectHttpsByPost(String path, String KK, File file) throws IOException, NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
URL urlObj = new URL(path);
//连接
HttpURLConnection con = (HttpURLConnection) urlObj.openConnection();
String result = null;
con.setDoInput(true);
con.setDoOutput(true);
con.setUseCaches(false); // post方式不能使用缓存
// 设置请求头信息
con.setRequestProperty("Connection", "Keep-Alive");
con.setRequestProperty("Charset", "UTF-8");
// 设置边界
String BOUNDARY = "----------" + System.currentTimeMillis();
con.setRequestProperty("Content-Type",
"multipart/form-data; boundary="
+ BOUNDARY);
// 请求正文信息
// 第一部分:
StringBuilder sb = new StringBuilder();
sb.append("--"); // 必须多两道线
sb.append(BOUNDARY);
sb.append("\r\n");
sb.append("Content-Disposition: form-data;name=\"media\";filelength=\"" + file.length() + "\";filename=\""
+ file.getName() + "\"\r\n");
sb.append("Content-Type:application/octet-stream\r\n\r\n");
byte[] head = sb.toString().getBytes("utf-8");
// 获得输出流
OutputStream out = new DataOutputStream(con.getOutputStream());
// 输出表头
out.write(head);
// 文件正文部分
// 把文件已流文件的方式 推入到url中
DataInputStream in = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] bufferOut = new byte[1024];
while ((bytes = in.read(bufferOut)) != -1) {
out.write(bufferOut, 0, bytes);
}
in.close();
// 结尾部分
byte[] foot = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("utf-8");// 定义最后数据分隔线
out.write(foot);
out.flush();
out.close();
StringBuffer buffer = new StringBuffer();
BufferedReader reader = null;
try {
// 定义BufferedReader输入流来读取URL的响应
reader = new BufferedReader(new InputStreamReader(con.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
if (result == null) {
result = buffer.toString();
}
} catch (IOException e) {
} finally {
if (reader != null) {
reader.close();
}
}
return result;
}