本文是 微信公众号开发者模式介绍及接入 的后续,如没看过前文的话,可能看本文会有些懵逼。本文主要介绍微信公众平台的素材、消息管理接口的开发。由于个人的订阅号是没有大多数接口的权限的,所以我们需要使用微信官方提供的测试号来进行开发。测试号的申请可参考下文:
本小节我们来开发回复图文消息的功能,官方文档地址如下:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543
回复图文消息所需传递的参数如下:
注:多图文消息不会显示Description参数的信息
官方的图文消息示例数据结构如下:
12345678
2
-
-
图文消息都在Articles标签内,而每个item标签都包含一条图文消息,有多少个item标签就代表有多少条图文消息。
在开发回复图文消息的时候,我们需要使用到一张图片来作为图文消息的封面,找一个图片文件放在工程的resources/static目录下即可,并确保能够在外网上访问:
看完了官方的示例数据及文档,那么我们就来开发一下图文消息的回复吧。首先是创建一个基类,封装通用的字段,代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Getter;
import lombok.Setter;
/**
* @program: mq-demo
* @description: 图文消息基类
* @author: 01
* @create: 2018-07-02 20:24
**/
@Getter
@Setter
public class BaseMassage {
/**
* 接收方账号
*/
@XStreamAlias("ToUserName")
private String toUserName;
/**
* 发送方账号
*/
@XStreamAlias("FromUserName")
private String fromUserName;
/**
* 消息创建时间 (整型)
*/
@XStreamAlias("CreateTime")
private long createTime;
/**
* 消息类型
*/
@XStreamAlias("MsgType")
private String msgType;
}
然后是具体的封装每条图文消息字段的对象,代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Getter;
import lombok.Setter;
/**
* @program: mq-demo
* @description: 图文消息对象
* @author: 01
* @create: 2018-07-02 20:19
**/
@Getter
@Setter
public class NewsItem{
@XStreamAlias("Title")
private String title;
@XStreamAlias("Description")
private String description;
@XStreamAlias("PicUrl")
private String picUrl;
@XStreamAlias("Url")
private String url;
}
接着是包含每条图文消息的容器对象,代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @program: mq-demo
* @description: 图文消息容器对象
* @author: 01
* @create: 2018-07-02 20:29
**/
@Getter
@Setter
public class NewsMessage extends BaseMassage{
@XStreamAlias("ArticleCount")
private int articleCount;
@XStreamAlias("Articles")
private List articles;
}
将图文消息结构都封装成一个个的类后,就是需要组装图文消息以及将组装好的图文消息转换成xml格式的数据,发送给微信服务器了。所以我们需要在MessageUtil类中,新增如下两个方法:
/**
* 图文消息转换为xml
*
* @param newsMessage
* @return
*/
public static String newsMessageToXml(NewsMessage newsMessage) {
XStream xStream = new XStream();
xStream.processAnnotations(new Class[]{NewsItem.class, NewsMessage.class});
xStream.alias("xml", newsMessage.getClass());
xStream.alias("item", NewsItem.class);
return xStream.toXML(newsMessage);
}
/**
* 组装图文消息
*
* @param toUserName
* @param fromUserName
* @return
*/
public static String initNewsMessage(String toUserName, String fromUserName) {
List newsItemList = new ArrayList<>();
NewsMessage newsMessage = new NewsMessage();
NewsItem newsItem = new NewsItem();
newsItem.setTitle("图文消息");
newsItem.setDescription("这是一个图文消息");
newsItem.setPicUrl("http://zero.mynatapp.cc/code.jpg");
newsItem.setUrl("www.baidu.com");
newsItemList.add(newsItem);
newsMessage.setToUserName(fromUserName);
newsMessage.setFromUserName(toUserName);
newsMessage.setCreateTime(System.currentTimeMillis());
newsMessage.setMsgType(MessageTypeEnum.MSG_NEWS.getMsgType());
newsMessage.setArticles(newsItemList);
newsMessage.setArticleCount(newsItemList.size());
return newsMessageToXml(newsMessage);
}
最后修改WeChatMqController中的text方法,增加一条判断,判断当用户输入数字1时,则回复图文消息。代码如下:
@PostMapping("/common")
public String text(@RequestBody String xmlStr) {
// 将xml格式的数据,转换为 AllMessage 对象
AllMessage allMessage = MessageUtil.xmlToAllMessage(xmlStr);
// 是否是文本消息类型
if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_TEXT.getMsgType())) {
// 用户输入数字1时,回复图文消息
if ("1".equals(allMessage.getContent())) {
return MessageUtil.initNewsMessage(allMessage.getToUserName(), allMessage.getFromUserName());
}
// 自动回复用户所发送的文本消息
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_PREFIX.getContent() + allMessage.getContent());
}
// 是否是事件推送类型
else if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_EVENT.getMsgType())) {
// 是否为订阅事件
if (EventType.EVENT_SUBSCRIBE.getEventType().equals(allMessage.getEvent())) {
// 自动回复欢迎语
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_SUBSCRIBE.getContent());
}
} else {
// 暂不支持文本以外的消息回复
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
}
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
}
完成以上代码的编写后,启动SpringBoot,打开微信公众号,测试结果如下:
本小节我们来看看如何获取access_token,官方文档地址如下:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
access_token是什么?官方的定义如下:
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
调用接口获取access_token需要传递的参数说明如下:
获取access_token成功后,接口所返回的参数说明如下:
从文档中我们可以看到,调用接口获取access_token时需要传递appid和secret,appid和secret可以在公众号的基本配置页面中获取,如下:
然后我们还需要安装提示,设置一下白名单的ip,即你机器的ip,不然是无法调用接口获取access_token的,如下:
将appid、secret以及获取access_token的接口url,配置到SpringBoot的配置文件中,如下:
wechat:
mpAppid: wx8ed1xxxxxx9513dd
mpAppSecret: 0c1b5b7ea5xxxxxxxxx14cb5b61258
accessTokenUrl: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
在工程中新建一个config包,在该包下新建一个 WeXinConfig 配置类,用于加载配置文件中所配置的appid和secret:
package org.zero01.weixin.mqdemo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @program: mq-demo
* @description: 微信公众号配置类
* @author: 01
* @create: 2018-07-03 20:50
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "wechat")
public class WeXinConfig {
private String mpAppid;
private String mpAppSecret;
private String accessTokenUrl;
}
因为我们需要序列化json数据以及发送http请求给微信服务器,所以需要使用到一些工具包,在maven的pom.xml文件中,加入如下依赖:
org.json
json
20160810
org.apache.httpcomponents
httpclient
4.5.5
在util包下新建一个 WeiXinUtil 工具类,在该类中封装get、post请求方法,以及获取access_token的方法。代码如下:
package org.zero01.weixin.mqdemo.util;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.zero01.weixin.mqdemo.config.WeXinConfig;
import org.zero01.weixin.mqdemo.vo.AccessToken;
import java.io.IOException;
/**
* @program: mq-demo
* @description:
* @author: 01
* @create: 2018-07-03 21:04
**/
@Component
public class WeiXinUtil {
private static WeXinConfig wxConfig;
public WeXinConfig getWeXinConfig() {
return wxConfig;
}
@Autowired
public void setWeXinConfig(WeXinConfig wxConfig) {
WeiXinUtil.wxConfig = wxConfig;
}
/**
* get请求
*
* @param url
* @return
*/
public static JSONObject doGet(String url) throws IOException {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet(url);
HttpResponse response = client.execute(httpGet);
String result = EntityUtils.toString(response.getEntity(), "utf-8");
return new JSONObject(result);
}
/**
* post请求
*
* @param url
* @param outStr
* @return
*/
public static JSONObject doPost(String url, String outStr) throws IOException {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(outStr, "utf-8"));
HttpResponse response = client.execute(httpPost);
String result = EntityUtils.toString(response.getEntity(), "utf-8");
return new JSONObject(result);
}
/**
* 获取access_token
*
* @return
* @throws IOException
*/
public static AccessToken getAccessToken() throws IOException {
AccessToken token = new AccessToken();
// 替换appid和secret
String url = wxConfig.getAccessTokenUrl()
.replace("APPID", wxConfig.getMpAppid())
.replace("APPSECRET", wxConfig.getMpAppSecret());
JSONObject jsonObject = doGet(url);
token.setToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
return token;
}
}
完成以上代码的编写后,新建一个测试类,测试一下是否能正常获取到access_token。测试代码如下:
package org.zero01.weixin.mqdemo.util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.zero01.weixin.mqdemo.vo.AccessToken;
import java.io.IOException;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class WeiXinUtilTest {
@Test
public void getAccessToken() throws IOException {
AccessToken accessToken = WeiXinUtil.getAccessToken();
System.out.println("access_token: " + accessToken.getToken());
System.out.println("有效时间: " + accessToken.getExpiresIn());
}
}
运行以上测试用例后,控制台输出如下:
access_token: 11_AMxhxO9soXndEc6XI-0hG0CWQ_oVQjaiPol6P2eMDLrSYpIrbiNMjHEDFwoOiKwG-ckgwPTHCiWypzK_reZohT7H5UdEYUmdlU_qq-oGQefv9q9A4mEkFV5WyiEFK5q5SsvsLR5QIKcjf1BhLDEfAIAAST
有效时间: 7200
从测试结果中,可以看到,成功获取到了access_token,并且这个access_token的有效期是7200秒,也就是两个小时,和官方文档描述的一致。
一般在实际的项目开发中,我们都会把这个access_token缓存起来,缓存到本地或者nosql数据库中,然后每隔1.5个小时或2个小时的时候,就重新获取一次access_token,刷新缓存。这样做是为了避免在每个逻辑点都去重新获取access_token,因为这样会导致服务的不稳定,而且微信也规定了获取access_token的接口每天只能调用2000次,如果每个逻辑点都去重新获取access_token的话,不仅会导致服务不稳定,还容易把调用次数给花完。如下:
本小节我们来看看如何进行图片消息的回复,官方文档地址如下:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543
回复图片消息所需传递的参数如下:
官方的图文消息示例数据结构如下:
12345678
从所需传递的参数列表中可以看到,回复图片消息时需要传递一个MediaId,这是通过素材管理中的接口上传多媒体文件,得到的id。所以在开发回复图片消息的接口前,我们还需要开发一个上传多媒体文件的接口,以此来获得MediaId。关于素材管理接口的官方文档地址如下:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738726
新增临时素材接口调用说明如下:
上传素材成功后,返回的参数如下:
有一点要说明的是,个人的订阅号是没有素材管理接口的权限的,所以我们需要将之前配置的appid和AppSecret配置为测试号的,不然接口会调用失败,如果是已认证的服务号就可以直接使用。
由于需要上传图片素材才能发送图片消息,所以首先需要在 WexinUtil 中,新增一个 upload 方法,用于上传临时图片素材并返回素材的media_id。
但是在写代码前,需要先将上传临时素材的接口url地址配置到SpringBoot的配置文件中,如下:
wechat:
...
uploadUrl: https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
然后在配置类里加上这个配置的字段,如下:
...
public class WeXinConfig {
...
private String uploadUrl;
}
upload 方法代码如下:
/**
* 上传临时素材
*
* @param filePath 需要上传的文件所在路径
* @param accessToken access_token
* @param type 素材类型
* @return media_id
* @throws IOException
*/
public static String upload(String filePath, String accessToken, String type, String key) throws IOException {
File file = new File(filePath);
if (!(file.exists() || file.isFile())) {
throw new IOException("文件不存在");
}
String url = wxConfig.getUploadUrl()
.replace("ACCESS_TOKEN", accessToken)
.replace("TYPE", type);
URL urlObj = new URL(url);
// 打开连接
HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
// 设置属性
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setUseCaches(false);
// 设置头信息
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Charset", "UTF-8");
// 设置边界
String boundary = "----------" + System.currentTimeMillis();
connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
StringBuilder sb = new StringBuilder();
sb.append("--").append(boundary).append("\r\n")
.append("Content-Disposition;form-data;name=\"file\";filename=\"")
.append(file.getName())
.append("\"\r\n")
.append("Content-Type:application/octet-stream\r\n\r\n");
byte[] head = sb.toString().getBytes("UTF-8");
// 获得输出流
OutputStream output = new DataOutputStream(connection.getOutputStream());
// 输出表头
output.write(head);
// 文件正文部分
// 把文件以流文件的方式,推入到url中
DataInputStream input = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] bufferOutput = new byte[1024];
while ((bytes = input.read(bufferOutput)) != -1) {
output.write(bufferOutput, 0, bytes);
}
input.close();
// 结尾部分,定义最后数据分割线
byte[] foot = ("\r\n--" + boundary + "--\r\n").getBytes("utf-8");
output.write(foot);
output.flush();
output.close();
StringBuilder buffer = new StringBuilder();
String result = null;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
result = buffer.toString();
} catch (IOException e) {
e.printStackTrace();
}
JSONObject jsonObject = new JSONObject(result);
log.info("response data: {}", jsonObject);
return jsonObject.getString(key);
}
在测试类中新增一个测试方法,测试代码如下:
@Test
public void upload() throws IOException {
String filePath = "Z:/v2-9b17df91629f842edd472d7cfcaa9c4b_hd.jpg";
AccessToken accessToken = WeiXinUtil.getAccessToken();
String mediaId = WeiXinUtil.upload(filePath, accessToken.getToken(), "image");
System.out.println(mediaId);
}
控制台输出结果如下:
mediaId: 5_PCrofX1_KIpSfWzJE-tu7AxQjxw6zlQ44oBuUkM_PZ6FiPeDY0a7vcWU2zdap9
获取到media_id后,就可以开始开发回复图片消息功能了,首先根据官方给出的数据结构,封装好各个实体类。Image类代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
@Data
public class Image {
@XStreamAlias("MediaId")
private String mediaId;
}
ImageMessage类代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
@Data
public class ImageMessage extends BaseMassage {
@XStreamAlias("Image")
private Image image;
}
然后在MessageUtil中新增如下两个方法:
/**
* 将图片消息转换为xml
*
* @param imageMessage
* @return
*/
public static String imageMessageToXml(ImageMessage imageMessage) {
XStream xStream = new XStream();
xStream.processAnnotations(new Class[]{ImageMessage.class, Image.class});
xStream.alias("xml", imageMessage.getClass());
return xStream.toXML(imageMessage);
}
/**
* 组装图片消息对象
*
* @param toUserName
* @param fromUserName
* @return
*/
public static String initImageMessage(String toUserName, String fromUserName) {
Image image = new Image();
image.setMediaId("5_PCrofX1_KIpSfWzJE-tu7AxQjxw6zlQ44oBuUkM_PZ6FiPeDY0a7vcWU2zdap9");
ImageMessage imageMessage = new ImageMessage();
imageMessage.setFromUserName(toUserName);
imageMessage.setToUserName(fromUserName);
imageMessage.setMsgType(MessageTypeEnum.MSG_IMAGE.getMsgType());
imageMessage.setCreateTime(System.currentTimeMillis());
imageMessage.setImage(image);
return imageMessageToXml(imageMessage);
}
最后修改WeChatMqController中的text方法,增加一条判断,判断当用户输入数字2时,则回复图片消息。代码如下:
...
if ("1".equals(allMessage.getContent())) {
return MessageUtil.initNewsMessage(allMessage.getToUserName(), allMessage.getFromUserName());
} else if ("2".equals(allMessage.getContent())) {
return MessageUtil.initImageMessage(allMessage.getToUserName(), allMessage.getFromUserName());
}
...
完成以上代码的编写后,重启SpringBoot,打开微信公众号,测试结果如下:
在上一小节中,我们介绍了如何开发回复图片消息的功能,而其他类似的消息回复都是差不多的,这里就不一一去赘述了。本小节我们来看看如何进行音乐消息回复的开发,官方文档地址如下:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543
回复音乐消息所需传递的参数如下:
官方的图文消息示例数据结构如下:
12345678
开发音乐消息回复,我们需要一个音乐文件,找一个mp3文件放在工程的resources/static目录下即可,并确保能够在外网上访问:
根据官方给出的数据结构,封装好各个实体类。Music 实体类代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
@Data
public class Music {
@XStreamAlias("Title")
private String title;
@XStreamAlias("Description")
private String description;
@XStreamAlias("MusicUrl")
private String musicUrl;
@XStreamAlias("HQMusicUrl")
private String hQMusicUrl;
@XStreamAlias("ThumbMediaId")
private String thumbMediaId;
}
MusicMessage 实体类代码如下:
package org.zero01.weixin.mqdemo.vo;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
@Data
public class MusicMessage extends BaseMassage {
@XStreamAlias("Music")
private Music music;
}
由于音乐消息需要传递一个ThumbMediaId,也就是缩略图的媒体id。所以我们需要修改之前的测试代码,以此获取thumb_media_id,如下:
@Test
public void upload() throws IOException {
String filePath = "Z:/v2-9b17df91629f842edd472d7cfcaa9c4b_hd.jpg";
AccessToken accessToken = WeiXinUtil.getAccessToken();
String thumbMediaId = WeiXinUtil.upload(filePath, accessToken.getToken(), "thumb","thumb_media_id");
System.out.println("thumb_media_id: " + thumbMediaId);
}
执行以上测试方法,控制台输出的结果如下:
thumb_media_id: Iu9puUGeFcS8HWyBGepJfeGoLDV_sWg8vJTeG-akMhcSGrqFjvoimMhCfjWw8F53
复制好thumb_media_id,然后在MessageUtil中新增如下两个方法:
/**
* 将音乐消息转换为xml
*
* @param musicMessage
* @return
*/
public static String musicMessageToXml(MusicMessage musicMessage) {
XStream xStream = new XStream();
xStream.processAnnotations(new Class[]{MusicMessage.class, Music.class});
xStream.alias("xml", musicMessage.getClass());
return xStream.toXML(musicMessage);
}
/**
* 组装音乐消息对象
*
* @param toUserName
* @param fromUserName
* @return
*/
public static String initMusicMessage(String toUserName, String fromUserName) {
Music music = new Music();
music.setTitle("音乐消息");
music.setDescription("这是一个音乐消息");
music.setThumbMediaId("Iu9puUGeFcS8HWyBGepJfeGoLDV_sWg8vJTeG-akMhcSGrqFjvoimMhCfjWw8F53");
music.setMusicUrl("http://zero.mynatapp.cc/Unravel.mp3");
music.setHQMusicUrl("http://zero.mynatapp.cc/Unravel.mp3");
MusicMessage musicMessage = new MusicMessage();
musicMessage.setFromUserName(toUserName);
musicMessage.setToUserName(fromUserName);
musicMessage.setMsgType(MessageTypeEnum.MSG_MUSIC.getMsgType());
musicMessage.setCreateTime(System.currentTimeMillis());
musicMessage.setMusic(music);
return musicMessageToXml(musicMessage);
}
最后修改WeChatMqController中的text方法,增加一条判断,判断当用户输入数字3时,则回复音乐消息。代码如下:
...
if ("1".equals(allMessage.getContent())) {
return MessageUtil.initNewsMessage(allMessage.getToUserName(), allMessage.getFromUserName());
} else if ("2".equals(allMessage.getContent())) {
return MessageUtil.initImageMessage(allMessage.getToUserName(), allMessage.getFromUserName());
} else if ("3".equals(allMessage.getContent())) {
return MessageUtil.initMusicMessage(allMessage.getToUserName(), allMessage.getFromUserName());
}
...
完成以上代码的编写后,重启SpringBoot,打开微信测试公众号进行测试,测试结果如下:
点击音乐消息,打开后效果如下:
注:我这用的是pc端的微信,是可以正常播放的,但实际手机端很有可能无法播放,这也是微信的一个小坑
转载于:https://blog.51cto.com/zero01/2136341