本文目录
  1. 1 微信公众号介绍
  2. 2 注册订阅号
  3. 3 注册测试号
  4. 4 程序运行流程
  5. 5 搭建开发环境
  6. 6 内外网穿透
  7. 7 开发接入
  8. 8 接收用户消息
  9. 9 回复用户消息封装
    1. 9.1 回复消息入门demo
    2. 9.2 基础消息类的封装
    3. 9.3 文本消息类封装
    4. 9.4 图文消息封装
    5. 9.5 测试
    6. 9.6 自动回复机器人
  10. 10 ★access token的获取
    1. 10.1 ★封装请求工具类
    2. 10.2 创建AccessToken类
    3. 10.3 WxService中添加获取AccessToken的方法
  11. 11 自定义菜单
    1. 11.1 封装菜单类
    2. 11.2 测试
  12. 12 设置和获取行业信息
    1. 12.1 设置行业信息
    2. 12.2 获取行业信息
  13. 13 发送模板消息
  14. 14 新增和获取临时素材
    1. 14.1 新增临时素材
    2. 14.2 获取临时素材
  15. 15 二维码生成和扫描
    1. 15.1 生成带参数的临时二维码
    2. 15.2 扫描二维码
  16. 16 获取用户信息
    1. 16.1 获取已关注的用户信息
    2. 16.2 网页授权
  17. 17 微信公众号开发框架

分类: 公众号和小程序 | 标签: java 微信公众号

微信公众号开发-Java版学习笔记

发表于: 2021-08-14 22:26:28 | 字数统计: 13k | 阅读时长预计: 65分钟

本文基于罗召勇老师的教程加上自己的理解整理

本文源码已上传至我的码云: https://gitee.com/heliufang/wx

微信公众号开发整体不难,主要是熟悉微信公众号常用的一些接口文档,然后会一门后端语言(比如java)即可。

罗召勇老师教程:微信公众号开发-Java版(蓝桥罗召勇)

微信公众号文档:微信公众号官方文档

1 微信公众号介绍

账号分为服务号订阅号、小程序

image-20210808110213141

服务号和订阅号开发类似,但是申请服务号必须是企业,所以学习的话申请一个订阅号+测试账号即可。为啥要申请测试账号呢?因为订阅号的接口功能有限,为了学习开发以及熟悉更多的接口,所以还需要申请一个测试号。

2 注册订阅号

第一步:访问:https://mp.weixin.qq.com/ 点击立即注册按钮

image-20210808110839008

第二步:注册类型页面选择订阅号

image-20210808110933591

第三步:填写相关信息,点击注册即可

image-20210808111031011

3 注册测试号

因为订阅号的接口权限是有限的,为了熟悉更多的微信公众号接口,所以需要申请一个测试号。

第一步:用注册的订阅号登录

第二步:在目录中【设置与开发】—>【开发者工具】下选择公众平台测试账号,点击进入后申请即可。

image-20210808112917414

申请成功之后,就可以配置相关信息进行开发了,具体怎么配置后面再解释

image-20210808113103181

4 程序运行流程

用户在公众号发送请求到微信服务器

微信服务器将请求转发到我们自己的服务器

我们自己的服务器处理完之后再把结果发送到微信服务器

最后微信服务器再把结果响应给客户

image-20210808114624893

5 搭建开发环境

罗老师用的是eclipse并且没有用maven环境,我用的是eclipse+maven+jdk7+tomcat8.0。maven的话可以兼容idea,而且下载依赖方便。

新建一个名为wx的maven项目(这个项目名字任意都行),pom.xml的依赖如下:

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- 阿里云小蜜-自动回复机器人 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-chatbot</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.2</version>
        </dependency>
        <!-- xml操作相关依赖 -->
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.0.0</version>
        </dependency>
        <!-- 阿里json解析 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>
        <!-- 这个是编码解码的 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>
    </dependencies>

编写一个测试的servlet

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/test") 
public class TestServlet extends HttpServlet{

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("请求到达了");
        resp.getWriter().write("hello weixin");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
    }

}

启动项目访问:http://localhost:8080/wx/test

浏览器看到如下效果说明搭建成功

image-20210808120644851

6 内外网穿透

外网默认是访问不到自己电脑上的项目的,为了让外网能够访问,所以需要做内外网穿透.这个不需要自己实现,可以借助一些工具,如花生壳、ngrok.这里用的是ngrok.

ngrok文档

第一步:访问ngrok官网,注册ngrok账号。

第二步:使用注册的账号登录

第三步:【隧道管理—>开通隧道】立即购买,可以购买最后那个免费的,也可以花10块钱买一个。免费的有时候不稳定,可以买一个10块。

image-20210808195028874

image-20210808195438170

开通之后在隧道管理下就可以看到刚刚开通的隧道

image-20210808195618721

第四步:下载客户端工具,我电脑是windows的所以下载windows版

各版本工具下载地址:https://www.ngrok.cc/download.html

第五步:启动ngrok客户端工具,运行bat,输入隧道id,回车

image-20210808195948516

image-20210808200101402

看到下面这个状态为【online】表示启动成功
image-20210808200116096

然后就可以通过http://heliufang.vipgz4.idcfengye.com这个域名访问本地8080端口上的项目了,比如访问之前搭建的wx项目

image-20210808200322739

7 开发接入

接入之后微信服务器和我们自己的项目就接通了。那么如何接入呢?

接入的官方文档

image-20210808213637686

上图中的url就是自己电脑的项目

点击上图的提交按钮之后,微信会向上图中的url发送一个get请求,请求参数如下:

参数描述
signature微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp时间戳
nonce随机数
echostr随机字符串
  • 第二步:编写代码校验,用代码实现下面的逻辑

1)将token、timestamp、nonce三个参数进行字典序排序

2)将三个参数字符串拼接成一个字符串进行sha1加密

3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信,如果比对成功,请原样返回echostr参数内容

在之前搭建的名为wx的项目中新建一个【WxServlet.java】

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.qy.service.WxService;

@WebServlet("/api")
public class WxServlet extends HttpServlet{

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("请求到达了");
        //取出微信服务器传过来的参数
        String signature = req.getParameter("signature");
        String timestamp = req.getParameter("timestamp");
        String nonce = req.getParameter("nonce");
        String echostr = req.getParameter("echostr");
        //自定义一个check方法用来校验接入
        boolean success = WxService.check(timestamp, nonce, signature);
        if(success){
            System.out.println("接入成功");
            PrintWriter writer = resp.getWriter();
            writer.write(echostr);//接入成功需要原样返回echostr
        }else{
            System.out.println("接入失败");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
    }
}

新建一个【WxService.java】并添加一个check工具方法

import java.util.Arrays;
import org.apache.commons.codec.digest.DigestUtils;

public class WxService {
    
    public static final String TOKEN = "hlf";//在微信配置界面自定义的token
    
    /**
     * 接入校验
     * @param timestamp
     * @param nonce
     * @param signature
     * @return
     */
    public static boolean check(String timestamp, String nonce, String signature) {
        //1.将token、timestamp、nonce三个参数进行字典序排序
        String[] arr = new String[]{TOKEN,timestamp,nonce};
        Arrays.sort(arr);
        //2.将三个参数字符串拼接成一个字符串进行sha1加密  https://www.cnblogs.com/2333/p/6405386.html
        String str = arr[0]+arr[1]+arr[2];
        str = DigestUtils.sha1Hex(str);//sha1加密,这里没有像罗老师那样手写,直接用的commons-codec包的工具类
        System.out.println("str:"+str);
        //3.将加密后的字符串和signature比较
        System.out.println(signature);
        return str.equalsIgnoreCase(signature);
    }
}

启动项目,点击提交按钮,出现下面这个代表接入成功。

image-20210808220055857

8 接收用户消息

官方文档:接受普通消息

当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。

也就是说用户发消息给微信服务器,微信服务器会发送post请求到我们自己的服务器,并且传送一个xml的数据给我们自己的服务器。

例如文本消息是这样的

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
</xml>
参数描述
ToUserName开发者微信号
FromUserName发送方帐号(一个OpenID)
CreateTime消息创建时间 (整型)
MsgType消息类型,文本为text
Content文本消息内容
MsgId消息id,64位整型

java中这样的数据读取并不方便。可以转换一下,先通过dom4j这个包转成dom对象,再把标签名和对应的标签的值保存到HashMap集合中,这样后面处理数据就很方便了,具体代码实现如下:

在【WxServlet】中编写doPost方法,在测试号管理界面,扫码关注测试公众号

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Map<String,String> map = WxService.parseRequest(req.getInputStream());
        System.out.println(map);//关注测试号,给测试公众号发消息,就可以看到打印结果了
    }

在【WxService】中添加parseRequest方法

/**
     * 将接受到的消息转化成map
     * @param req
     * @return
     */
    public static  Map<String, String> parseRequest(InputStream is) {
        Map<String,String> map = new HashMap<String,String>();
        //1.通过io流得到文档对象
        SAXReader saxReader = new SAXReader();
        Document document = null;
        try {
            document = saxReader.read(is);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        //2.通过文档对象得到根节点对象
        Element root = document.getRootElement();
        //3.通过根节点对象获取所有子节点对象
        List<Element> elements = root.elements();
        //4.将所有节点放入map
        for (Element element : elements) {
            map.put(element.getName(), element.getStringValue());
        }
        return map;
    }

9 回复用户消息封装

官方文档:被动回复用户消息

当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。

一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:

1、开发者在5秒内未回复任何内容 2、开发者回复了异常数据,比如JSON数据等

上面这段文字来自官方,可以看出

  • 回复必须是xml的类型

  • 可以回复多种类型的xml(文本、图片、图文、语音、视频、音乐)

  • 接收到消息没有做出响应就会抛出:该公众号暂时无法提供服务,请稍后再试

9.1 回复消息入门demo

这个demo就是给用户回复一个文本消息

回复的xml格式如下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[你好]]></Content>
</xml>
参数是否必须描述
ToUserName接收方帐号(收到的OpenID)
FromUserName开发者微信号
CreateTime消息创建时间 (整型)
MsgType消息类型,文本为text
Content回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)

在wxservlet中doPost编写如下代码

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置编码格式,不然中文会乱码
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        //将请求中的xml参数转成map
        Map<String,String> map = WxService.parseRequest(req.getInputStream());
        System.out.println(map);
        //回复消息
        String textMsg = "<xml><ToUserName><![CDATA["+map.get("FromUserName")+"]]></ToUserName><FromUserName><![CDATA["+map.get("ToUserName")+"]]></FromUserName><CreateTime>12345678</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[你好]]></Content></xml>";
        resp.getWriter().print(textMsg);
    }

然后用测试号发消息,公众号都会回复一个 【你好】

image-20210810221633028

这样写代码功能是可以实现,但是这样拼接字符串,再回复消息很不方便.然后自然就想到可以用java类来封装消息,响应的时候将java类转成xml(通过xstream这个工具包实现)。下面就以文本消息和图文消息为例进行封装,其它消息类似。

9.2 基础消息类的封装

把公共的属性放到基础消息类中,然后其它消息类继承即可。

@XStreamAlias 这个注解配置的就是转成xml时对应的节点名字

public class BaseMsg {
    @XStreamAlias("ToUserName")
    private String toUserName;//接收方的账号(收到的openid)
    @XStreamAlias("FromUserName")
    private String fromUserName;//开发者的微信号
    @XStreamAlias("CreateTime")
    private String createTime;//消息创建时间
    @XStreamAlias("MsgType")
    private String msgType;//消息类型

    public BaseMsg(Map<String,String> requestMap) {
        super();
        this.toUserName = requestMap.get("FromUserName");
        this.fromUserName = requestMap.get("ToUserName");
        this.createTime = requestMap.get("CreateTime");
    }
    
    //get and set ...
}

9.3 文本消息类封装

回复的xml的格式说明可以参考9.1入门demo.回复文本的封装类如下:

@XStreamAlias("xml") //xml指的就是xml这个根节点名称
public class TextMsg extends BaseMsg {
    @XStreamAlias("Content")
    private String content;//回复的文本内容
    
    public TextMsg(Map<String,String> requestMap,String content) {
        super(requestMap);
        this.setMsgType("text");
        this.content = content;
    }
    
    //get and set ...
}

9.4 图文消息封装

图文消息格式说明

<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>
参数是否必须说明
ToUserName接收方帐号(收到的OpenID)
FromUserName开发者微信号
CreateTime消息创建时间 (整型)
MsgType消息类型,图文为news
ArticleCount图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
Articles图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数
Title图文消息标题
Description图文消息描述
PicUrl图片链接,支持JPG、PNG格式,较好的效果为大图360200,小图200200
Url点击图文消息跳转链接

首先封装一个article类,对应就是xml中的item这个节点

@XStreamAlias("item")//映射到xml中的item这个节点
public class Article {
    @XStreamAlias("Title")
    private String title;//图文消息标题
    @XStreamAlias("Description")
    private String description;//图文消息描述
    @XStreamAlias("PicUrl")
    private String picUrl;//图片链接
    @XStreamAlias("Url")
    private String url;//点击图文消息跳转链接
    
    //get and set ...
}

然后再封装一个图文消息类

@XStreamAlias("xml")
public class NewsMsg extends BaseMsg {
    
    @XStreamAlias("ArticleCount")
    private String articleCount;//图文消息个数
    @XStreamAlias("Articles")
    private List<Article> articles;

    public NewsMsg(Map<String, String> requestMap,List<Article> articles) {
        super(requestMap);
        this.setMsgType("news");
        this.articles = articles;
        this.setArticleCount(this.articles.size()+"");
    }
    //get and set ...
}

9.5 测试

前面已经将基础消息和图文消息封装好了,现在用封装好的消息类来回复

第一步:将wxservletdoPost方法改成如下

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置编码格式,不然中文会乱码
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        //将请求中的xml参数转成map
        Map<String,String> map = WxService.parseRequest(req.getInputStream());
        System.out.println(map);
        //处理完将响应一个xml给微信
        String respXml = WxService.getRespose(map);
        System.out.println(respXml);
        resp.getWriter().print(respXml);
    }

第二步:WxService添加如下方法:

/**
     * 事件消息回复
     */
    public static String getRespose(Map<String, String> requestMap) {
        BaseMsg msg = null;
        // 根据用户发送消息的类型,做不同的处理
        String msgType = requestMap.get("MsgType");
        switch (msgType) {
        case "text":
            msg = dealTextMsg(requestMap);
            break;
        case "news":
            break;
        default:
            break;
        }
        // System.out.println(msg);
        // 将处理结果转化成xml的字符串返回
        if (null != msg) {
            return beanToXml(msg);
        }
        return null;
    }

    /**
     * 将回复的消息类转成xml字符串
     * 
     * @param msg
     * @return
     */
    public static String beanToXml(BaseMsg msg) {
        XStream stream = new XStream();
        stream.processAnnotations(TextMsg.class);
        stream.processAnnotations(NewsMsg.class);
        String xml = stream.toXML(msg);
        return xml;
    }

    /**
     * 当用户发送是文本消息的处理逻辑
     * 
     * @param map
     * @return
     */
    private static BaseMsg dealTextMsg(Map<String, String> requestMap) {
        // 获取用户发送的消息内容
        String msg = requestMap.get("Content");
        // 如果是图文回复一个图文消息
        if (msg.equals("图文")) {
            List<Article> articles = new ArrayList<Article>();
            articles.add(new Article("码云博客", "这个是我个人的码云博客,基于hexo搭建,里面的文章都是使用markdown编写",
                    "https://heliufang.gitee.io/uploads/banner.jpg", "https://heliufang.gitee.io/"));
            return new NewsMsg(requestMap, articles);
        }
        //否则回复一个文本消息,文本内容为'当前时间+你好'
        //当然这个内容可以自定义,在这里也可以接入自动回复机器人
        TextMsg textMsg = new TextMsg(requestMap, new Date(System.currentTimeMillis()).toLocaleString() + "你好");
        return textMsg;
    }

然后分别给公众号发一个1和图文

image-20210810231459572

9.6 自动回复机器人

罗老师教程中的图灵机器人已经要收费.我使用的是阿里云的阿里云小蜜这个机器人来做的回复.

阿里云小蜜机器人可以免费体验三个月。

具体代码可以查看阿里云小蜜的文档:阿里云产品服务协议(云小蜜)

10 ★access token的获取

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存.access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效

access token文档

目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token

总结:调用很多接口需要access_token,获取access_token之后需要保存起来,过期了再重新获取,而不是每次都重新获取。

接口调用请求说明

https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明

参数是否必须说明
grant_type获取access_token填写client_credential
appid第三方用户唯一凭证
secret第三方用户唯一凭证密钥,即appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数说明

参数说明
access_token获取到的凭证
expires_in凭证有效时间,单位:秒

10.1 ★封装请求工具类

因为需要发送请求给微信服务器,所以需要有请求的工具类。罗老师用的是java自带的请求类,相对来说比较繁琐。所以我这里采用的是Apache HttpClient,这个用起来更加的简单。

第一步:pom.xml中导入依赖

<!--httpClient需要的依赖-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>
<!--//httpclient缓存-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient-cache</artifactId>
    <version>4.5</version>
</dependency>
<!--//http的mime类型都在这里面-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpmime</artifactId>
    <version>4.3.2</version>
</dependency>

第二步:基于Apache HttpClient封装HttpUtils工具类,我封装了4个方法,可以支持get请求和post请求。后面很多需要用的地方直接调用即可。

可以参考这个博客:HttpClient发送get/post请求

public class HttpUtils {

    public static void main(String[] args) {
        // 1.测试get请求
        /*
         String getUrl = "http://localhost:8080/user/searchPage?pageNum=1&pageSize=2";
         System.out.println(sendGet(getUrl));
         */
        
        // 2.测试post请求 携带x-www-form-urlencoded数据格式
        /*String postUrlForm = "http://localhost:8080/user";
        Map paramMap = new HashMap();
        paramMap.put("name", "杰克");
        paramMap.put("age", "20");
        paramMap.put("gender", "1");
        System.out.println(sendPost(postUrlForm, paramMap));*/
        
        //3.测试post请求 携带json数据格式
        /*String postUrlJson = "http://localhost:8080/user";
        String jsonParam = "{\"name\":\"jack\",\"age\":\"18\",\"gender\":\"2\"}";
        System.out.println(sendPost(postUrlJson,jsonParam));*/
        
        //4 测试post 携带文件
        String postUrlFile = "http://localhost:8080/user/upload";
        Map paramMap = new HashMap();
        paramMap.put("name", "tom");
        String localFile = "d:\\logo.png";
        String fileParamName = "file";
        System.out.println(sendPost(postUrlFile, paramMap,localFile,fileParamName));
    }

    // 1.httpClient发送get请求
    public static String sendGet(String url) {
        String result = "";
        CloseableHttpResponse response = null;
        try {
            // 根据地址获取请求
            HttpGet request = new HttpGet(url);// 这里发送get请求
            // 获取当前客户端对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
            // 通过请求对象获取响应对象
            response = httpClient.execute(request);
            // 判断网络连接状态码是否正常(0--200都数正常)
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                result = EntityUtils.toString(response.getEntity(), "utf-8");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != response) {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }

    // 2.httpClient发送post请求 携带x-www-form-urlencoded数据格式
    public static String sendPost(String url, Map<String, String> map) {
        CloseableHttpResponse httpResponse = null;
        String result = "";
        try {
            // 1、创建一个httpClient客户端对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
            // 2、创建一个HttpPost请求
            HttpPost httpPost = new HttpPost(url);
            // 设置请求头
            httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded"); // 设置传输的数据格式
            // 携带普通的参数params的方式
            List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
            Set<String> keys = map.keySet();
            for (String key : keys) {
                params.add(new BasicNameValuePair(key, map.get(key)));
            }
            String str = EntityUtils.toString(new UrlEncodedFormEntity(params, Consts.UTF_8));
            // 这里就是:username=kylin&password=123456
            System.out.println(str);

            // 放参数进post请求里面 从名字可以知道 这个类是专门处理x-www-form-urlencoded 添加参数的
            httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));

            // 7、执行post请求操作,并拿到结果
            httpResponse = httpClient.execute(httpPost);
            // 获取结果实体
            HttpEntity entity = httpResponse.getEntity();
            if (entity != null) {
                result = EntityUtils.toString(entity, "UTF-8");
            } else {
                EntityUtils.consume(entity);//// 如果entity为空,那么直接消化掉即可
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != httpResponse) {
                try {
                    httpResponse.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }

    // 3.httpClient发送post请求 携带json数据格式
    public static String sendPost(String url, String jsonStr) {
        CloseableHttpResponse httpResponse = null;
        String result = "";
        try {
            // 1.创建httpClient
            CloseableHttpClient httpClient = HttpClients.createDefault();
            // 2.创建post请求方式实例
            HttpPost httpPost = new HttpPost(url);

            // 2.1设置请求头 发送的是json数据格式
            httpPost.setHeader("Content-type", "application/json;charset=utf-8");
            httpPost.setHeader("Connection", "Close");

            // 3.设置参数---设置消息实体 也就是携带的数据
            /*
             * 比如传递: { "username": "aries", "password": "666666" }
             */
            //String jsonStr = " {\"username\":\"aries\",\"password\":\"666666\"}";
            StringEntity entity = new StringEntity(jsonStr.toString(), Charset.forName("UTF-8"));
            entity.setContentEncoding("UTF-8"); // 设置编码格式
            // 发送Json格式的数据请求
            entity.setContentType("application/json");
            // 把请求消息实体塞进去
            httpPost.setEntity(entity);

            // 4.执行http的post请求
            // 4.执行post请求操作,并拿到结果
            httpResponse = httpClient.execute(httpPost);
            // 获取结果实体
            HttpEntity httpEntity = httpResponse.getEntity();
            if (httpEntity != null) {
                result = EntityUtils.toString(httpEntity, "UTF-8");
            } else {
                EntityUtils.consume(httpEntity);//// 如果httpEntity为空,那么直接消化掉即可
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != httpResponse) {
                try {
                    httpResponse.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }

    // 4.httpClient发送post请求 携带文件
    public static String sendPost(String url, Map<String, String> map,String localFile, String fileParamName) {
        HttpPost httpPost = new HttpPost(url);
        CloseableHttpClient httpClient = HttpClients.createDefault();
        String resultString = "";
        CloseableHttpResponse response = null;
        try {
            // 把文件转换成流对象FileBody
            FileBody bin = new FileBody(new File(localFile));

            MultipartEntityBuilder builder = MultipartEntityBuilder.create();

            // 相当于<input type="file" name="fileParamName"/> 其中fileParamName以传进来的为准
            builder.addPart(fileParamName, bin);
            // 相当于<input type="text" name="userName" value=userName>
            /*builder.addPart("filesFileName",
                    new StringBody(fileParamName, ContentType.create("text/plain", Consts.UTF_8)));*/
            if (map != null) {
                for (String key : map.keySet()) {
                    builder.addPart(key,
                            new StringBody(map.get(key), ContentType.create("text/plain", Consts.UTF_8)));
                }
            }
            HttpEntity reqEntity = builder.build();
            httpPost.setEntity(reqEntity);
            // 发起请求 并返回请求的响应
            response = httpClient.execute(httpPost, HttpClientContext.create());
            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        }  catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (response != null)
                    response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return resultString;
    }
}

10.2 创建AccessToken类

public class AccessToken {
    private String token;
    private long expiresTime;//过期时间
    
    public AccessToken(String token, String expiresIn) {
        super();
        this.token = token;
        //当前时间+有效期 = 过期时间
        this.expiresTime = System.currentTimeMillis()+Integer.parseInt(expiresIn);
    }
    
    /**
     * 判断token是否过期
     * @return
     */
    public boolean isExpire() {
        return System.currentTimeMillis() > expiresTime;
    }
    //get and set ...
}

10.3 WxService中添加获取AccessToken的方法

private static AccessToken at;//token获取的次数有限,有效期也有限,所以需要保存起来
private static String GET_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
    
    //登录测试号管理界面-测试号信息下面可以得到你的APPID和APPSECRET
    private static String APPID = "wx7bf783afc5150a5a";
    private static String APPSECRET = "8d9930d60717c7aaa0620ad993d984d8";
/**
     * 发送get请求获取AccessToken
     */
    private static void getToken() {
        String url = GET_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
        String tokenStr = HttpUtils.sendGet(url);//调用工具类发get请求
        System.out.println(tokenStr);
        JSONObject jsonObject = JSONObject.parseObject(tokenStr);
        String token = jsonObject.getString("access_token");
        String expiresIn = jsonObject.getString("expires_in");
        at = new AccessToken(token, expiresIn);
    }
    
    /**
     * 获取AccessToken  向外提供
     */
    public static String getAccessToken() {
        //过期了或者没有值再去发送请求获取
        if(at == null || at.isExpire()) {
            getToken();
        }
        return at.getToken();
    }

编写一个测试类获取AccessToken

import org.junit.Test;

import com.qy.service.WxService;

public class TestToken {

    @Test
    public void getAccessToken() {
        //可以看到下面两次获取的值一致
        System.out.println(WxService.getAccessToken());
        System.out.println(WxService.getAccessToken());
    }
}

11 自定义菜单

自定义菜单文档

请注意:

  1. 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
  2. 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。
  3. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

自定义菜单接口可实现多种类型按钮,如下:

  1. click:点击推事件用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;
  2. view:跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
  3. scancode_push:扫码推事件用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。
  4. scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
  5. pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
  6. pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
  7. pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
  8. location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
  9. media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频、图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
  10. view_limited:跳转图文消息URL用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。

接口调用请求说明

http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

url中的ACCESS_TOKEN就是之前获取的,调用这个接口需要带上

请求需携带json参数

{
 "button":[
     {
           "type":"click",
          "name":"一级点击",
          "key":"1"
     },
     {
           "type":"view",
          "name":"个人博客",
          "url":"https://heliufang.gitee.io/"
     },
     {
          "name":"有子菜单",
          "sub_button":[
              {
                  "type":"click",
                  "name":"三一点击",
                  "key":"31"
            },
            {
                  "type":"view",
                  "name":"码云博客",
                  "url":"https://heliufang.gitee.io/"
            },
            {
                "type":"pic_photo_or_album",
                "name":"拍照或发图",
                "key":"33"
            }
          ]
     }
 ]
}

参数说明

参数是否必须说明
button一级菜单数组,个数应为1~3个
sub_button二级菜单数组,个数应为1~5个
type菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型
name菜单标题,不超过16个字节,子菜单不超过60个字节
keyclick等点击类型必须菜单KEY值,用于消息接口推送,不超过128字节
urlview、miniprogram类型必须网页 链接,用户点击菜单可打开链接,不超过1024字节。 type为miniprogram时,不支持小程序的老版本客户端将打开本url。
media_idmedia_id类型和view_limited类型必须调用新增永久素材接口返回的合法media_id
appidminiprogram类型必须小程序的appid(仅认证公众号可配置)
pagepathminiprogram类型必须小程序的页面路径

返回结果

正确时的返回JSON数据包如下:

{"errcode":0,"errmsg":"ok"}

错误时的返回JSON数据包如下(示例为无效菜单名长度):

{"errcode":40018,"errmsg":"invalid button name size"}

和前面xml的类似,我们需要对着请求的json数据封装按钮类,这样后面操作起来就比较方便,而且也方便维护。

11.1 封装菜单类

<1>AbstractButton类

//所有菜单(按钮)的父类
public abstract class AbstractButton {
    private String name;//按钮标题

    public String getName() {
        return this.name;
    }

    public void setName(final String name) {
        this.name = name;
    }

    public AbstractButton(final String name) {
        this.name = name;
    }
}

<2>Button类

//一级菜单对象
public class Button {
    private List<AbstractButton> button;

    public Button() {
        this.button = new ArrayList<AbstractButton>();
    }

    public List<AbstractButton> getButton() {
        return this.button;
    }

    public void setButton(final List<AbstractButton> button) {
        this.button = button;
    }
}

<3>ClickButton类

//点击类型的菜单
public class ClickButton extends AbstractButton {
    private String type;
    private String key;

    public String getType() {
        return this.type;
    }

    public void setType(final String type) {
        this.type = type;
    }

    public String getKey() {
        return this.key;
    }

    public void setKey(final String key) {
        this.key = key;
    }

    public ClickButton(final String name, final String key) {
        super(name);
        this.type = "click";//点击类型
        this.key = key;
    }
}

<4>ViewButton类

//网页类型的菜单
public class ViewButton extends AbstractButton {
    private String type;
    private String url;

    public String getType() {
        return this.type;
    }

    public void setType(final String type) {
        this.type = type;
    }

    public String getUrl() {
        return this.url;
    }

    public void setUrl(final String url) {
        this.url = url;
    }

    public ViewButton(final String name, final String url) {
        super(name);
        this.type = "view";//网页类型
        this.url = url;
    }
}

<5> PhotoOrAlbumButton

//拍照或传图菜单
public class PhotoOrAlbumButton extends AbstractButton{
    private String type;
    private String key;

    public PhotoOrAlbumButton(String name,String key) {
        super(name);
        this.type = "pic_photo_or_album";//拍照获取传图
        this.key = key;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}

<6>SubButton

import java.util.ArrayList;
import java.util.List;

//二级菜单对象
public class SubButton extends AbstractButton {
    private List<AbstractButton> sub_button;

    public List<AbstractButton> getSub_button() {
        return this.sub_button;
    }

    public void setSub_button(final List<AbstractButton> sub_button) {
        this.sub_button = sub_button;
    }

    public SubButton(final String name) {
        super(name);
        this.sub_button = new ArrayList<AbstractButton>();
    }
}

11.2 测试

新增一个Test方法

package com.qy.test;

import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import com.alibaba.fastjson.JSONObject;
import com.qy.entity.button.AbstractButton;
import com.qy.entity.button.Button;
import com.qy.entity.button.ClickButton;
import com.qy.entity.button.PhotoOrAlbumButton;
import com.qy.entity.button.SubButton;
import com.qy.entity.button.ViewButton;
import com.qy.service.WxService;
import com.qy.utils.HttpUtils;

public class TestButton {

    @Test
    public void setButton() {
        //创建一级菜单
        Button button = new Button();
        //在第三个菜单中创建二级菜单
        SubButton subButton = new SubButton("有子菜单");
        List<AbstractButton> list2 = new ArrayList();
        list2.add(new ClickButton("三一点击", "31"));
        list2.add(new ViewButton("码云博客", "https://heliufang.gitee.io/"));
        list2.add(new PhotoOrAlbumButton("拍照或发图","33"));
        subButton.setSub_button(list2);
        //在一级菜单中添加三个按钮,
        List<AbstractButton> list = new ArrayList();
        list.add(new ClickButton("一级点击", "1"));
        list.add(new ViewButton("个人博客", "https://heliufang.gitee.io/"));
        list.add(subButton);
        button.setButton(list);
        //转成json格式字符串
        String jsonString = JSONObject.toJSONString(button);
        //System.out.println(jsonString);
        //发送请求
        String url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
        url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());//把token带上
        String result = HttpUtils.sendPost(url, jsonString);
        System.out.println(result);
    }
}

运行效果如下:

image-20210813000312449

12 设置和获取行业信息

12.1 设置行业信息

如果要发送模板消息,那么首先就得设置行业信息,如何设置和获取可以看下面接口。

模板消息文档

设置行业可在微信公众平台后台完成,每月可修改行业1次,帐号仅可使用所属行业中相关的模板,为方便第三方开发者,提供通过接口调用的方式来修改账号所属行业,具体如下:

接口调用请求说明

http请求方式: POST https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=ACCESS_TOKEN

POST数据说明

POST数据示例如下:

{
    "industry_id1":"1",
    "industry_id2":"4"
}

参数说明

参数是否必须说明
access_token接口调用凭证
industry_id1公众号模板消息所属行业编号-主行业
industry_id2公众号模板消息所属行业编号-副行业

行业代码查询,更多代码可以查询文档

主行业副行业代码
IT科技互联网/电子商务1
IT科技IT软件与服务2
IT科技IT硬件与设备3

编写测试代码

@Test
    public void setIndustry() {
        String url = "https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=ACCESS_TOKEN";
        url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
        String jsonStr = "{\"industry_id1\":\"1\",\"industry_id2\":\"4\"}";
        String rString = HttpUtils.sendPost(url, jsonStr);
        System.out.println(rString);
    }

12.2 获取行业信息

获取帐号设置的行业信息。可登录微信公众平台,在公众号后台中查看行业信息。为方便第三方开发者,提供通过接口调用的方式来获取帐号所设置的行业信息,具体如下:

接口调用请求说明

http请求方式:GET https://api.weixin.qq.com/cgi-bin/template/get_industry?access_token=ACCESS_TOKEN

参数说明

参数是否必须说明
access_token接口调用凭证

返回说明

正确调用后的返回示例:

{
    "primary_industry":{"first_class":"运输与仓储","second_class":"快递"},
    "secondary_industry":{"first_class":"IT科技","second_class":"互联网|电子商务"}
}

返回参数说明

参数是否必填说明
access_token接口调用凭证
primary_industry帐号设置的主营行业
secondary_industry帐号设置的副营行业

编写测试代码

@Test
    public void getIndustry() {
        String url = "https://api.weixin.qq.com/cgi-bin/template/get_industry?access_token=ACCESS_TOKEN";
        url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
        String string = HttpUtils.sendGet(url);
        System.out.println(string);
    }

13 发送模板消息

模板消息接口

就是微信主动给用户推送消息,不需要像之前那样被动(用户发送之后再回复).

接口调用请求说明

http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

POST数据如下:

{
    "touser": "oQxvI51GI5t9wBaBjmBXgJZZVM3A",
    "template_id": "tQ0G9Pmd_n_ylmplYsEnexgabkJXH1S3J7BXahK454g",
    "url": "https://heliufang.gitee.io/",
    "data": {
        "first": {
            "value": "您好!您投递的简历有新的反馈",
            "color": "#173177"
        },
        "company": {
            "value": "广州壹新网络科技有限公司",
            "color": "#173177"
        },
        "time": {
            "value": "2021-8-5 23:31:23",
            "color": "#173177"
        },
        "result": {
            "value": "已通过",
            "color": "#ff0000"
        },
        "remark": {
            "value": "带身份证",
            "color": "#173177"
        }
    }
}

参数说明

参数是否必填说明
touser接收者openid
template_id模板ID,这个需要在管理界面配置
url模板跳转链接(海外帐号没有跳转能力)
data模板数据
color模板内容字体颜色,不填默认为黑色

返回码说明

在调用模板消息接口后,会返回JSON数据包。正常时的返回JSON数据包示例:

{"errcode":0,"errmsg":"ok","msgid":200228332}

★第一步:在微信测试号管理后台配置模板:

  • 模板标题: 简历反馈提醒

  • 模板内容:

{{first.DATA}} 
公司名:{{company.DATA}} 
投递时间:{{time.DATA}} 
反馈结果:{{result.DATA}} {{remark.DATA}}

创建好之后是下面这个样子

image-20210814170124702

第二步:编写代码

@Test
    public void sendTemplateMsg() {
        String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";
        url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
        //实际开发中应封装成java类,再把java对象转成类似下面的jsonstr
        String jsonStr = "{\r\n" + 
                "    \"touser\": \"oQxvI51GI5t9wBaBjmBXgJZZVM3A\",\r\n" + 
                "    \"template_id\": \"tQ0G9Pmd_n_ylmplYsEnexgabkJXH1S3J7BXahK454g\",\r\n" + 
                "    \"url\": \"https://heliufang.gitee.io/\",\r\n" + 
                "    \"data\": {\r\n" + 
                "        \"first\": {\r\n" + 
                "            \"value\": \"您好!您投递的简历有新的反馈\",\r\n" + 
                "            \"color\": \"#173177\"\r\n" + 
                "        },\r\n" + 
                "        \"company\": {\r\n" + 
                "            \"value\": \"广州壹新网络科技有限公司\",\r\n" + 
                "            \"color\": \"#173177\"\r\n" + 
                "        },\r\n" + 
                "        \"time\": {\r\n" + 
                "            \"value\": \"2021-8-5 23:31:23\",\r\n" + 
                "            \"color\": \"#173177\"\r\n" + 
                "        },\r\n" + 
                "        \"result\": {\r\n" + 
                "            \"value\": \"已通过\",\r\n" + 
                "            \"color\": \"#ff0000\"\r\n" + 
                "        },\r\n" + 
                "        \"remark\": {\r\n" + 
                "            \"value\": \"带身份证\",\r\n" + 
                "            \"color\": \"#173177\"\r\n" + 
                "        }\r\n" + 
                "    }\r\n" + 
                "}";
        String rString = HttpUtils.sendPost(url, jsonStr);
        System.out.println(rString);
    }

测试结果如下

image-20210814171316722

14 新增和获取临时素材

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放。

注意点:

1、临时素材media_id是可复用的。

2、媒体文件在微信后台保存时间为3天,即3天后media_id失效。

3、上传临时素材的格式、大小限制与公众平台官网一致。

图片(image): 10M,支持PNG\JPEG\JPG\GIF格式

语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式

视频(video):10MB,支持MP4格式

缩略图(thumb):64KB,支持JPG格式

14.1 新增临时素材

新增临时素材文档

罗老师用的是java自带的文件类上传,代码比较繁琐。而我使用HttpClient封装的HttpUtils上传就很简单了。

接口调用请求说明

http请求方式: POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE

参数说明

参数是否必须说明
access_token调用接口凭证
type媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
mediaform-data中媒体文件标识,有filename、filelength、content-type等信息

返回说明

正确情况下的返回JSON数据包结果如下:

{"type":"image","media_id":"atL80WWRNpMWhivoIGf9KTUUUO5pm6RxML8OPEUd7cbfb1Rs0kl2Yv0319KMQI-0","created_at":1628933345,"item":[]}
参数描述
type媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb,主要用于视频与音乐格式的缩略图)
media_id媒体文件上传后,获取标识
created_at媒体文件上传时间戳

编写测试代码

//上传图片
    @Test
    public void uploadMedia() {
        String url = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
        url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
        url = url.replace("TYPE", "image");
        String string = HttpUtils.sendPost(url, null, "C:\\Users\\Administrator\\Desktop\\2.jpg", "");
        System.out.println(string);
    }

14.2 获取临时素材

获取临时素材文档

接口调用请求说明

http请求方式: GET,https调用 https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID 请求示例(示例为通过curl命令获取多媒体文件) curl -I -G “https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID"

把ACCESS_TOKEN和MEDIA_ID替换到url的位置,然后浏览器打开就可以下载了

参数说明

参数是否必须说明
access_token调用接口凭证
media_id媒体文件ID

返回说明

正确情况下的返回HTTP头如下:

HTTP/1.1 200 OK
Connection: close
Content-Type: image/jpeg
Content-disposition: attachment; filename="MEDIA_ID.jpg"
Date: Sun, 06 Jan 2013 10:20:18 GMT
Cache-Control: no-cache, must-revalidate
Content-Length: 339721
curl -G "https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID"

15 二维码生成和扫描

15.1 生成带参数的临时二维码

为了满足用户渠道推广分析和用户帐号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。

目前有2种类型的二维码:

1、临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景 2、永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。

获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借ticket到指定URL换取二维码。

生成带参数的二维码文档

测试代码将实现下面这样一个功能,点击页面上的生成按钮,在页面展示生成好的二维码

【index.jsp】

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>二维码测试页面</title>
</head>
<body>
   <button type="button">生成二维码</button><br>
   <img alt="暂无图片" src="">
</body>
<script src="jquery.js"></script>
<script>
   $("button").click(function(){
       $.ajax({
           url: "/wx/getQrCode",
           type: "get",
           dataType: "json",
           success: function(resp){
               console.log(resp);
               //通过ticket获取图片
               var src = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+resp.ticket;
               $("img").attr("src",src)
           }
       })
   })
</script>
</html>

【后端servlet】

@WebServlet("/getQrCode")
public class QrCodeServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置编码格式,不然中文会乱码
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        //发送post请求获取ticket,页面通过ticket就可以展示二维码图片了
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN";
        url = url.replace("TOKEN", WxService.getAccessToken());
        /*600表示10分钟有效  scene_str是一个唯一标识,类似点击事件的key, QR_STR_SCENE表示临时二维码
         * {
                "expire_seconds": 600, 
                "action_name": "QR_STR_SCENE", 
                "action_info": {
                    "scene": {
                        "scene_str": "test"
                        
                    }
                }
            }
         */
        String jsonStr = "{\r\n" + 
                "    \"expire_seconds\": 600, \r\n" + 
                "    \"action_name\": \"QR_STR_SCENE\", \r\n" + 
                "    \"action_info\": {\r\n" + 
                "        \"scene\": {\r\n" + 
                "            \"scene_str\": \"test\"\r\n" + 
                "            \r\n" + 
                "        }\r\n" + 
                "    }\r\n" + 
                "}";
        String string = HttpUtils.sendPost(url, jsonStr);
        JSONObject object = JSONObject.parseObject(string);
        //将响应结果返回页面,用于显示二维码
        resp.getWriter().write(string);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }
}

访问页面,点击按钮就可以看到如下效果

image-20210814182247464

15.2 扫描二维码

用户扫描带场景值二维码时,可能推送以下两种事件:

如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。

如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者

扫描临时二维码之后,会向服务器推送一个xml数据包,解析之后打印效果如下:

{
Ticket=gQFr8DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAya1JKeDQ2M3JmOEQxOGlybk54Y08AAgS6mBdhAwRYAgAA, 
FromUserName=oQxvI51GI5t9wBaBjmBXgJZZVM3A,
EventKey=test, 
Event=SCAN, 
CreateTime=1628936703, 
ToUserName=gh_c8af0521f09a, 
MsgType=event
}

实现扫码之后给用户回复一个[你扫码了]

修改【WxService】的代码,修改getRespose方法,新增dealEvent和dealScanEvent方法

/**
     * 事件消息回复
     */
    public static String getRespose(Map<String, String> requestMap) {
        BaseMsg msg = null;
        // 根据用户发送消息的类型,做不同的处理
        String msgType = requestMap.get("MsgType");
        switch (msgType) {
        case "text":
            msg = dealTextMsg(requestMap);
            break;
        case "news":
            break;
        case "event":
            //新增处理事件的方法
            msg = dealEvent(requestMap);
            break;
        default:
            break;
        }
        // System.out.println(msg);
        // 将处理结果转化成xml的字符串返回
        if (null != msg) {
            return beanToXml(msg);
        }
        return null;
    }

    //处理事件
    private static BaseMsg dealEvent(Map<String, String> requestMap) {
        String event = requestMap.get("Event");
        BaseMsg msg = null;
        //switch分发到具体事件
        switch (event) {
            case "SCAN":
                msg = dealScanEvent(requestMap);
                break;
            default:
                break;
        }
        return msg;
    }

    //处理SCAN事件
    private static BaseMsg dealScanEvent(Map<String, String> requestMap) {
        String eventKey = requestMap.get("EventKey");
        if("test".equals(eventKey)) {
            return new TextMsg(requestMap, "你扫码了");
        }
        return new TextMsg(requestMap, requestMap.toString());
    }

扫码之后效果如下:

image-20210814184046627

16 获取用户信息

一般在做网页授权的时候,会用到这个功能。

16.1 获取已关注的用户信息

获取用户基本信息(UnionID机制)

在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的。对于不同公众号,同一用户的openid不同)。公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称、头像、性别、所在城市、语言和关注时间。

获取用户基本信息(包括UnionID机制)

开发者可通过OpenID来获取用户基本信息。请使用https协议。

接口调用请求说明 http请求方式: GET https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

参数说明

参数是否必须说明
access_token调用接口凭证
openid普通用户的标识,对当前公众号唯一
lang返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语

openid可以登录测试号管理界面获取,对应关注者的微信号

image-20210814185850181

测试代码

@Test
    public void getUserInfo() {
        String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
        url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
        url = url.replace("OPENID", "oQxvI51GI5t9wBaBjmBXgJZZVM3A");
        String string = HttpUtils.sendGet(url);
        System.out.println(string);//这里就可以看到打印的用户信息了
    }

16.2 网页授权

可以获取未关注的用户信息,这部分需要有域名才能测试。

网页授权

因为ngrok默认域名已经被微信封了,所以需要申请一个域名来测试

第一步:在ngrok中使用自定义域名。

image-20210815145833067

第二步:微信测试号管理界面修改成自定义的域名

image-20210815145936625

第三步:测试号接口权限中配置网页授权的域名

image-20210815150238632

第四步:阿里云域名解析到ngrok的域名

image-20210815150319450

测试代码:

【WxService】修改dealTextMsg方法

image-20210815150631184

新增一个【getUserInfo】的servlet,这个就是用户点击之后重定向的servlet

@WebServlet("/getUserInfo")
public class GetUserInfoServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1.用户同意授权,获取code
        String code = request.getParameter("code");
        //2.通过code获取网页授权的access_token
        String url = " https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
        url = url.replace("APPID", WxService.APPID).replace("CODE", code).replace("SECRET", WxService.APPSECRET);
        String string = HttpUtils.sendGet(url);
        JSONObject jsonObject = JSONObject.parseObject(string);
        String accessToken = jsonObject.getString("access_token");
        String openid = jsonObject.getString("openid");
        //3.刷新access_token(如果需要)
        //4.通过token获取用户信息
        String getUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
        getUserInfoUrl = getUserInfoUrl.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openid);
        String userInfoJsonStr = HttpUtils.sendGet(getUserInfoUrl);
        System.out.println(userInfoJsonStr);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}

17 微信公众号开发框架

前面的开发都是原生的写法,github上有很多现成的公众号开发框架。

比如这个基于springboot的公众号开发框架:

仓库:https://github.com/binarywang/weixin-java-mp-demo

文档:https://github.com/Wechat-Group/WxJava/wiki/%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3

最后多说一句只有把原生的基础打好了,才能更好的理解和使用框架,所以建议先学原生的公众号开发,再上手框架。

------ 本文结束,感谢您的阅读 ------
本文作者: 程序员青阳
版权声明: 本文采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。