• 【公众号开发】Access Token的获取 · 请求公众号服务器创建自定义菜单 · 处理自定义菜单按钮事件


    公众号开发】(3)

    在这里插入图片描述

    【公众号开发】(3)

    开始开发 / 获取 Access token (qq.com)

    access_token是公众号的全局唯一的接口调用凭据,公众号调用各接口时都需使用access_token

    开发者需要进行妥善保存

    1. access_token的存储至少要保留512个字符空间
    2. access_token的有效期目前为2个小时(7200s),需定时刷新,重复获取将导致上次获取的access_token失效

    获取到Access token,我们才能够去调用微信公众号给我们提供的一些接口(Access token就类似于第三方接口的key,验证凭据后才可以去实现一些功能)

    1. 获取Access token

    这里是几张来自文档的重点截图:

    在这里插入图片描述

    在这里插入图片描述

    1.1 确定参数

    在这里插入图片描述

    public class TokenUtils {
    
    
        private static final String APP_ID = "wxdadd0122365919e8";
    
        private static final String APP_SECRET = "69fd4a3ad04167f288e49bea9dce3e45";
    
    
        public static String getAccessToken() {
            // 获取token的url
            final String URL = "https://api.weixin.qq.com/cgi-bin/token";
            // 获取token的grant_type
            final String GRANT_TYPE = "client_credential";
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1.2 补全URL(添加query string)

    public static String getAccessToken() {
        // 获取token的url
        final String URL = "https://api.weixin.qq.com/cgi-bin/token";
        // 获取token的grant_type
        final String GRANT_TYPE = "client_credential";
        // 构造参数表
        Map<String, Object> param = new HashMap<String, Object>(){{
            this.put("grant_type", GRANT_TYPE);
            this.put("appid", APP_ID);
            this.put("secret", APP_SECRET);
        }};
        // 发起get请求
        String response = HttpUtils.doGet(URL, param);
        // 解析json
        Map<String, Object> result = JsonUtils.jsonToMap(response);
        System.out.println(result);
        // 返回token
        return (String) result.get("access_token");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.3 测试

    public static void main(String[] args) {
        System.out.println(getAccessToken());
    }
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    2. 封装AccessToken以便保存与后期使用

    这里我们全局的AccessToken唯一一份,我们希望其未过期就无需刷新,这里用的是**单例模式**!

    2.1 TokenUtils做出一些调整

    为了实现这个初心,在TokenUtils做出一些调整

    在这里插入图片描述

    • 改为获取map

    2.2 单例模式的AccessToken

    @Data
    public class AccessToken {
    
        private String token;
    
        private long expireTime;//有效期限
    
    
        volatile private static AccessToken accessToken = null;
    
        public void setExpireTime(long expireIn) {
            // 设置有效期限的时候的时间戳
            this.expireTime = System.currentTimeMillis() + expireIn * 1000;
        }
    
        public boolean isExpired() {
            return System.currentTimeMillis() > this.getExpireTime();
        }
    
    
        private static void setAccessToken() {
            if(accessToken == null) {
                accessToken = new AccessToken();
            }
            Map<String, Object> map = TokenUtils.getAccessTokenMap();
            accessToken.setToken((String) map.get("access_token"));
            accessToken.setExpireTime((Integer) map.get("expires_in"));
        }
    
        public static AccessToken getAccessToken() {
            if(accessToken == null || accessToken.isExpired()) {
                synchronized (AccessToken.class) {
                    if(accessToken == null || accessToken.isExpired()) {
                        setAccessToken();
                    }
                }
            }
            return accessToken;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    2.3 TokenUtils获取全局唯一的token字符串的方法

    public static String getToken() {
        return AccessToken.getAccessToken().getToken();
    }
    
    • 1
    • 2
    • 3

    测试

    public static void main(String[] args) {
        System.out.println(getToken());
        System.out.println(getToken());
        System.out.println(getToken());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    • 三个一样,代表我们的AccessToken单例第一次被实例和设置并且因为没有过期而没有被更新~

    有了凭据之后,我们就可以去调用微信公众号给我们提供的一些接口了,实现一些功能~

    3. 自定义菜单

    你会发现,我们的测试公众号现在还没有菜单的选项

    在这里插入图片描述

    而我们的常识也知道,公众号的菜单是必不可少的,接下来我们来完成一下自定义菜单吧

    在这里插入图片描述

    开发手册:自定义菜单 / 创建接口 (qq.com)

    抓重点:

    在这里插入图片描述

    • 按字数截取…
    • 刷新策略我们创建后再讲

    自定义菜单接口,就相当于触发各种各样事件

    在这里插入图片描述

    3.1 菜单显示的原理

    在这里插入图片描述
    我们提交的信息会给公众号服务器保存起来,构造成菜单显示给用户~

    这里我们来看个post请求body的例子:

    • (要求是json,这也合理,因为我们要传递的信息就是多个菜单,多级菜单,这个可是对象~)

    在这里插入图片描述

    我们需要什么功能,我们就查看与学习对应的按钮类型和其他参数就行了

    3.2 封装菜单类

    对于这个post请求的body,也就是这个json字符串的构造,是最大的问题,我们首先要封装菜单类

    @Data
    public class Button {
    
        private List<AbstractButton> button;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这是构造最外层的button属性:

    {
        "button": [...]
    }
    
    • 1
    • 2
    • 3

    AbstractButton是我们抽象出来的按钮类(可以是一些按钮/二级菜单)

    @Data
    public abstract class AbstractButton {
    
        private String name;
    
        public AbstractButton(String name) {
            this.name = name;
        }
        public AbstractButton() {
            
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这个name属性是按钮/二级菜单的共性(二级菜单没有type,所以这里不应该写type)

    根据刚才的json字符串,里面提到的属性就是对应类型按钮的属性~

    以这几个为示例(其他根据实际举一反三就行):

    在这里插入图片描述

    @Data
    public class ViewButton extends AbstractButton {
    
        private final String type = "view";
    
        private String url;
    
        public ViewButton(String name) {
            super(name);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    @Data
    public class ClickButton extends AbstractButton {
    
        private final String type = "click";
    
        private String key;
    
        public ClickButton(String name) {
            super(name);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    @Data
    public class PicPhotoOrAlbumButton extends AbstractButton {
    
        private final String type = "pic_photo_or_album";
    
        private String key;
    
    
        public PicPhotoOrAlbumButton(String name) {
            super(name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    @Data
    public class SubButton extends AbstractButton {
    
        private List<AbstractButton> sub_button;
    
    
        public SubButton(String name) {
            super(name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3 构造一个菜单对象

    预计菜单效果如下:

    在这里插入图片描述

    public class ButtonUtils {
    
        public static Button createButton() {
            Button button = new Button();
            button.setButton(new ArrayList<>());
            return button;
        }
    
        public static ClickButton createClickButton(String name, String key) {
            ClickButton clickButton = new ClickButton(name);
            clickButton.setKey(key);
            return clickButton;
        }
    
        public static ViewButton createViewButton(String name, String url) {
            ViewButton viewButton = new ViewButton(name);
            viewButton.setUrl(url);
            return viewButton;
        }
    
        public static SubButton createSubButton(String name) {
            SubButton subButton = new SubButton(name);
            subButton.setSub_button(new ArrayList<>());
            return subButton;
        }
        public static PicPhotoOrAlbumButton createPicPhotoOrAlbumButton(String name, String key) {
            PicPhotoOrAlbumButton picPhotoOrAlbumButton = new PicPhotoOrAlbumButton(name);
            picPhotoOrAlbumButton.setKey(key);
            return picPhotoOrAlbumButton;
        }
    
    
    
        public static void main(String[] args) {
            Button button = createButton();
            // 一级菜单的两个按钮
            button.getButton().add(createClickButton("mara\uD83D\uDE00😀😀😀", "1"));
            button.getButton().add(createViewButton("baidu\uD83D\uDE00", "https://www.baidu.com"));
            // 二级菜单
            SubButton subButton = createSubButton("更多\uD83D\uDE00");
            subButton.getSub_button().add(createClickButton("mason\uD83D\uDE00", "2"));
            subButton.getSub_button().add(createViewButton("blog\uD83D\uDE00", "https://blog.csdn.net/Carefree_State?type=blog"));
            subButton.getSub_button().add(createPicPhotoOrAlbumButton("上传图片\uD83D\uDE00", "3"));
            // 二级菜单加入到一级菜单中
            button.getButton().add(subButton);
    //        System.out.println(button);
            String json = JsonUtils.objectToJson(button);
            System.out.println(json);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    emoji可以直接复制或者用unicode码,本质没啥区别,跟普通字符差不多:

    Unicode 11.0版本的emoji表情 - emoji大全,emoji百科 (emojidaquan.com)

    打印json后查看效果:

    在这里插入图片描述

    在线 JSON 解析 | 菜鸟工具 (runoob.com)

    在这里插入图片描述

    在这里插入图片描述

    😊符合预期😊

    对于json集合属性的序列,各个元素json字符串都不一样或者有联系,可以试试抽象成一个类,具体类继承这个抽象类,序列化的时候序列的是具体的实例

    或者,你干脆写成List也行,序列化的时候自然知道这个Object是谁向上转型来的,序列化也能正确,不过每个按钮都有name的~

    3.4 发送post请求

    url的创建:

    1. https://api.weixin.qq.com/cgi-bin/menu/create,访问的接口~
    2. queryString:携带我们的access_token,调用方法获取即可~
    // 构造url
    String url = " https://api.weixin.qq.com/cgi-bin/menu/create" + HttpUtils.getQueryString(new HashMap<String, Object>() {{
        this.put("access_token", TokenUtils.getToken());
    }});
    // 发送post请求
    String response = HttpUtils.doPost(url, json);
    System.out.println(response);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里,doPost是区别于提交form格式的doPost的一个重载方法,作用就是根据url,提交json字符串:

    public static String doPost(String httpUrl, String json) {
        HttpURLConnection connection = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        BufferedReader bufferedReader = null;
        String result = null;
        try {
            URL url = new URL(httpUrl);
            // 通过远程url连接对象打开连接
            connection = (HttpURLConnection) url.openConnection();
            // 设置连接请求方式
            connection.setRequestMethod("POST");
            // 设置连接主机服务器超时时间:15000毫秒
            connection.setConnectTimeout(15000);
            // 设置读取主机服务器返回数据超时时间:60000毫秒
            connection.setReadTimeout(60000);
            // 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
            connection.setDoOutput(true);
            // 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。
            connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            // 通过连接对象获取一个输出流
            outputStream = connection.getOutputStream();
            // 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
            outputStream.write(json.getBytes());
            // 通过连接对象获取一个输入流,向远程读取
            if (connection.getResponseCode() == 200) {
                inputStream = connection.getInputStream();
                // 对输入流对象进行包装:charset根据工作项目组的要求来设置
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                StringBuilder sbf = new StringBuilder();
                String temp;
                // 循环遍历一行一行读取数据
                while ((temp = bufferedReader.readLine()) != null) {
                    sbf.append(temp);
                    sbf.append(System.getProperty("line.separator"));
                }
                result = sbf.toString();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (null != bufferedReader) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    在这里插入图片描述

    3.5 启动main方法查看效果

    在这里插入图片描述

    微信公众号查看:

    在这里插入图片描述

    点击baidu😀(view按钮)跳转:

    在这里插入图片描述

    点击更多😀上拉菜单:

    在这里插入图片描述

    点击上传按钮(photo按钮)😀:

    在这里插入图片描述

    对于click按钮,点击了似乎没什么作用,接下来俺们来研究研究这个!

    4. 处理自定义菜单事件

    其实,用户每点击一次按钮,就相当于与公众号交互,对于这个“按钮事件”,消息类型为Event

    也就是这里的其他消息类型:

    在这里插入图片描述

    而只要是用户发来的消息,都会触发公众号服务器发送post请求到我们的服务器的根路径

    • 也就是之前写的那个接口一致

    在这里插入图片描述

    开发文档:基础消息能力 / 接收事件推送 (qq.com)

    在这里插入图片描述

    以自定义菜单事件为例,其他的举一反三、自行学习😊

    这里以click按钮为例子!

    4.1 了解公众号发过来的post请求机制

    在这里插入图片描述

    如果是上拉菜单的按钮,则不会上报,也就是不会发post请求

    • 或者是弹出“拍照/上传图片”,这也算是子菜单吧,等等类似的~

    4.2 了解公众号发过来的post请求格式

    在这里插入图片描述

    在这里插入图片描述

    这个key,就是我们之前的按钮属性里的key:

    在这里插入图片描述

    这个key对于公众号服务器而已没啥作用,但是对于开发者而言很重要,因为post请求访问的是同一个接口,并且,请求并没有发送按钮名参数,并且按钮名也不一定唯一,开发者用按钮名来区分每个按钮不合理!

    所以有公众号辅助我们,以key为按钮的标识,作为参数传递给开发者

    开发者以key作为区分按钮触发事件的手段,不同的key执行不同的业务~

    4.3 分支处理请求

    对于不同的消息类型、不同的事件类型、不同的key,你可以用哈希表记录“键与业务方法”,这里的业务方法可以是一个接口,用普通类去实现接口,最后结合多态实现,输入键执行对于业务

    这里我为了方便,易懂,任意演示/调试,用的是swtich分支处理

    @PostMapping("/")
    public String receiveMessage(HttpServletRequest request) throws IOException {
        String body = HttpUtils.getBody(request);
        Map<String, Object> map = XmlUtils.xmlToMap(body);
        System.out.println(map);
        // 回复消息
        String message = "";
        String MsgType = (String) map.get("MsgType");
        switch (MsgType) {
            case "event":
                message = handleEvent(map);//处理事件
                break;
            case "text":
                message = handleText(map);//处理文本
                break;
            default:
                System.out.println("其他消息类型");
                break;
        }
        return message;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • message返回空字符串才是正常的不回复,其他都是因为错误而不回复的

    handleText:

    private String handleText(Map<String, Object> map) {
        String message = "";
        if("图文".equals(map.get("Content"))) {
            NewsMessage newsMessage = NewsMessage.getReplyNewsMessage(map);
            message = XmlUtils.objectToXml(newsMessage);
            System.out.println(message);
        }else {
            // 1. 封装对象
            TextMessage textMessage = TextMessage.getAntonym(map);
            // 2. 序列化对象
            message = XmlUtils.objectToXml(textMessage);
        }
        return message;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    handleEvent:

    • 通过事件类型分支
    private String handleEvent(Map<String, Object> map) {
        String message = "";
        // 获取event值
        String event = (String) map.get("Event");
        // 事件分支
        switch (event) {
            case "CLICK":
                message = EventUtils.handleClick(map);
                break;
            case "VIEW":
                System.out.println("view");
                break;
            default:
                break;
        }
        return message;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    EventUtils.handleClick:

    public class EventUtils {
        public static String handleClick(Map<String, Object> map) {
            String message = "";
            String key = (String) map.get("EventKey");
            switch (key) {
                case "1":
                    map.put("Content","\"触发了点击事件,key = 1\"");
                    break;
                case "2":
                    map.put("Content","\"触发了点击事件,key = 2\"");
                    break;
                case "3":
                    map.put("Content","\"触发了点击事件,key = 3\"");
                    break;
                default:
                    break;
            }
            TextMessage textMessage = TextMessage.getReplyTextMessage(map);
            message = XmlUtils.objectToXml(textMessage);
            return message;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这个只是示例,至于你要执行什么业务,是你的事咯

    4.4 测试

    在这里插入图片描述

    在这里插入图片描述

    点击view类型按钮:

    在这里插入图片描述

    查看控制台:

    在这里插入图片描述

    还是那句话,这个只是示例,至于你要执行什么业务,是你的事咯

    举一反三,由你发挥,一生万物!


    文章到此结束!谢谢观看
    可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆

    代码:wx-demo · 游离态/马拉圈2023年10月 - 码云 - 开源中国 (gitee.com)


  • 相关阅读:
    嵌入式Linux--进程间通讯--消息队列
    【牛客刷题-算法】2-算法入门-栈的压入、弹出序列
    Flutter 绘制 3D 效果动画
    shardingsphere分库分表示例(逻辑表,真实表,绑定表,广播表,单表)
    ctrl+k,ctrl+l无法切换到时限文件
    ICDE2023 | VEND:基于点编码的边存在性判定
    生命在于折腾——皮卡丘靶场源码审计(一)
    k8s 解决pvc与pv不能绑定问题
    集合篇---Map集合
    电脑重装系统word从第二页开始有页眉页脚如何设置
  • 原文地址:https://blog.csdn.net/Carefree_State/article/details/133959674