开始

本文是 微信公众号开发者模式介绍及接入 的后续,如没看过前文的话,可能看本文会有些懵逼。本文主要介绍微信公众平台的素材、消息管理接口的开发。由于个人的订阅号是没有大多数接口的权限的,所以我们需要使用微信官方提供的测试号来进行开发。测试号的申请可参考下文:


图文消息

本小节我们来开发回复图文消息的功能,官方文档地址如下:

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543

回复图文消息所需传递的参数如下:
微信公众号开发-素材/消息管理接口

注:多图文消息不会显示Description参数的信息

官方的图文消息示例数据结构如下:

<xml>
    <ToUserName>
        <![CDATA[toUser]]>
    </ToUserName>
    <FromUserName>
        <![CDATA[fromUser]]>
    </FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType>
        <![CDATA[news]]>
    </MsgType>
    <ArticleCount>2</ArticleCount>
    <Articles>
        <item>
            <Title>
                <![CDATA[title1]]>
            </Title>
            <Description>
                <![CDATA[description1]]>
            </Description>
            <PicUrl>
                <![CDATA[picurl]]>
            </PicUrl>
            <Url>
                <![CDATA[url]]>
            </Url>
        </item>
        <item>
            <Title>
                <![CDATA[title]]>
            </Title>
            <Description>
                <![CDATA[description]]>
            </Description>
            <PicUrl>
                <![CDATA[picurl]]>
            </PicUrl>
            <Url>
                <![CDATA[url]]>
            </Url>
        </item>
    </Articles>
</xml>

图文消息都在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<NewsItem> 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<NewsItem> 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的获取

本小节我们来看看如何获取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文件中,加入如下依赖:

<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20160810</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.5</version>
</dependency>

在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

回复图片消息所需传递的参数如下:
微信公众号开发-素材/消息管理接口

官方的图文消息示例数据结构如下:

<xml>
    <ToUserName>
        <![CDATA[toUser]]>
    </ToUserName>
    <FromUserName>
        <![CDATA[fromUser]]>
    </FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType>
        <![CDATA[image]]>
    </MsgType>
    <Image>
        <MediaId>
            <![CDATA[media_id]]>
        </MediaId>
    </Image>
</xml>

从所需传递的参数列表中可以看到,回复图片消息时需要传递一个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

回复音乐消息所需传递的参数如下:
微信公众号开发-素材/消息管理接口

官方的图文消息示例数据结构如下:

<xml>
    <ToUserName>
        <![CDATA[toUser]]>
    </ToUserName>
    <FromUserName>
        <![CDATA[fromUser]]>
    </FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType>
        <![CDATA[music]]>
    </MsgType>
    <Music>
        <Title>
            <![CDATA[TITLE]]>
        </Title>
        <Description>
            <![CDATA[DESCRIPTION]]>
        </Description>
        <MusicUrl>
            <![CDATA[MUSIC_Url]]>
        </MusicUrl>
        <HQMusicUrl>
            <![CDATA[HQ_MUSIC_Url]]>
        </HQMusicUrl>
        <ThumbMediaId>
            <![CDATA[media_id]]>
        </ThumbMediaId>
    </Music>
</xml>

开发音乐消息回复,我们需要一个音乐文件,找一个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端的微信,是可以正常播放的,但实际手机端很有可能无法播放,这也是微信的一个小坑