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

【公众号开发】(3)

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

文章目录

  • 【公众号开发】(3)
    • 1. 获取Access token
      • 1.1 确定参数
      • 1.2 补全URL(添加query string)
      • 1.3 测试
    • 2. 封装AccessToken以便保存与后期使用
      • 2.1 TokenUtils做出一些调整
      • 2.2 单例模式的AccessToken
      • 2.3 TokenUtils获取全局唯一的token字符串的方法
    • 3. 自定义菜单
      • 3.1 菜单显示的原理
      • 3.2 封装菜单类
      • 3.3 构造一个菜单对象
      • 3.4 发送post请求
      • 3.5 启动main方法查看效果
    • 4. 处理自定义菜单事件
      • 4.1 了解公众号发过来的post请求机制
      • 4.2 了解公众号发过来的post请求格式
      • 4.3 分支处理请求
      • 4.4 测试

【公众号开发】(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

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

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

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

1.1 确定参数

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

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 补全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.3 测试

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

在这里插入图片描述

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

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

2.1 TokenUtils做出一些调整

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

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

  • 改为获取map

2.2 单例模式的AccessToken

  • 单例模式参考文章:【JavaEE】线程案例-单例模式 and 阻塞队列_s:103的博客-CSDN博客
@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;
    }

}

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

public static String getToken() {
    return AccessToken.getAccessToken().getToken();
}

测试

public static void main(String[] args) {
    System.out.println(getToken());
    System.out.println(getToken());
    System.out.println(getToken());
}

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

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

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

3. 自定义菜单

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

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

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

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

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

抓重点:

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

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

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

3.1 菜单显示的原理

【公众号开发】Access Token的获取 · 请求公众号服务器创建自定义菜单 · 处理自定义菜单按钮事件_第10张图片
我们提交的信息会给公众号服务器保存起来,构造成菜单显示给用户~

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

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

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

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

  • 自定义菜单 / 创建接口 (qq.com)

3.2 封装菜单类

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

@Data
public class Button {

    private List<AbstractButton> button;

}

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

{
    "button": [...]
}

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

@Data
public abstract class AbstractButton {

    private String name;

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

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

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

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

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

@Data
public class ViewButton extends AbstractButton {

    private final String type = "view";

    private String url;

    public ViewButton(String name) {
        super(name);
    }
}

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

@Data
public class ClickButton extends AbstractButton {

    private final String type = "click";

    private String key;

    public ClickButton(String name) {
        super(name);
    }

}

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

@Data
public class PicPhotoOrAlbumButton extends AbstractButton {

    private final String type = "pic_photo_or_album";

    private String key;


    public PicPhotoOrAlbumButton(String name) {
        super(name);
    }
}

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

@Data
public class SubButton extends AbstractButton {

    private List<AbstractButton> sub_button;


    public SubButton(String name) {
        super(name);
    }
}

3.3 构造一个菜单对象

预计菜单效果如下:

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

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);
    }
}

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

  • 之前我做的网站,文本中有emoji是不行的,因为我的服务器并不支持emoji存到数据库
  • 可以这样:‍你知道如何使用MySQL存储Emoji表情吗?明白MySQL中UTF-8和UTF-8MB4字符编码有何区别吗? - 知乎 (zhihu.com)

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

打印json后查看效果:

在这里插入图片描述

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

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

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

符合预期

对于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);

这里,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;
}

3.5 启动main方法查看效果

在这里插入图片描述

微信公众号查看:

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

点击baidu(view按钮)跳转:

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

点击更多上拉菜单:

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

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

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

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

4. 处理自定义菜单事件

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

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

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

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

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

在这里插入图片描述

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

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

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

这里以click按钮为例子!

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

在这里插入图片描述

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

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

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

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

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

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

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

这个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;
}
  • 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;
}

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;
}

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;
    }
}

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

4.4 测试

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

在这里插入图片描述

点击view类型按钮:

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

查看控制台:

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

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


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

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


你可能感兴趣的:(公众号开发,服务器,运维,微信公众平台,spring,spring,boot,mvc)