Java微信公众号开发6-自定义菜单

上一篇:Java微信公众号开发5-消息接收与响应

自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。

菜单的刷新策略

创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

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

  1. click:点击推事件用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;
  2. view:跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
6_1_自定义菜单.jpg

封装菜单和按钮实体类

新建菜单按钮基类BasicButton.java

import lombok.Data;

/**
 * @auther: zqtao
 * @description: 菜单按钮基类
 * 所有一级菜单、二级菜单都共有相同的属性,name type
 * @version: 1.0
 */

@Data
public class BasicButton {
    /**
     * 菜单标题,不超过16个字节,子菜单不超过60个字节
     */
    private String name;
    /**
     * 菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型
     */
    private String type;
}

新建click类型的按钮类ClickButton.java

import lombok.Data;

/**
 * @auther: zqtao
 * @Description: click类型的按钮(有type、name、key3个属性)
 * @version: 1.0
 */
@Data
public class ClickButton extends BasicButton {
    /**
     * 菜单KEY值,用于消息接口推送,不超过128字节
     */
    private String key;
}

新建view类型的按钮类ViewButton.java

import lombok.Data;

/**
 * @auther: zqtao
 * @description: view类型的按钮(有type 、 name 、 url三个属性)
 * @version: 1.0
 */
@Data
public class ViewButton extends BasicButton {
    /**
     * 网页链接,用户点击菜单可打开链接,不超过1024字节。
     * type为miniprogram时,不支持小程序的老版本客户端将打开本url。
     */
    private String url;
}

新建复合类型的按钮类ComplexButton.java

/**
 * @auther: zqtao
 * @description: 复合类型的按钮(含有子菜单的一级菜单)
 * @version: 1.0
 */
@Data
public class ComplexButton extends BasicButton {
    private BasicButton[] sub_button;
}

新建菜单类Menu.java

import lombok.Data;

/**
 * @auther: zqtao
 * @description: 整个菜单对象的封装
 * 菜单对象包含多个菜单项(最多只能有3个)
 * 这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项)
 * @version: 1.0
 */
@Data
public class Menu {
    private BasicButton[] button;
}

新建微信官方提供的功能性接口常量类WechatInterface.java

/**
 * @auther: zqtao
 * @description: 微信官方提供的功能性接口常量
 * @version: 1.0
 */

public class WechatInterface {
    /**
     * 获取access_token的接口地址(GET) 限200(次/天)
     */
    public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

    /**
     * 自定义菜单删除接口
     */
    public static final String MENU_DELETE_URL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";

    /**
     * 自定义菜单的创建接口
     */
    public static final String MENU_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";

    /**
     * 自定义菜单的查询接口
     */
    public static final String MENU_GET_URL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";
}

菜单和按钮等实体封装好,接下来是如何进行菜单的创建,删除操作。

1、根据AppId和AppSecret,以https get方式获取访问特殊接口所必须的凭证access_token;

2、根据access_token,将json格式的菜单数据通过https post方式提交。

封装通用的请求方法

创建菜单需要调用https请求接口。所以首要问题是实现HTTPS 请求方法

https请求,需要一个证书信任管理器,这个管理器类需要自己定义,实现X509TrustManager接口

import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * @auther: zqtao
 * @description: 证书管理器
 * @version: 1.0
 */
public class MyX509TrustManagerUtil implements X509TrustManager {
    // 证书管理器的作用就是让它信任我们指定的证书,下面的代码意味着信任所有证书,不管是否权威机构颁发
    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

封装HTTPS请求工具HttpRequestUtil

import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;

/**
 * @auther: zqtao
 * @description: 公众接口http请求发起通用工具类
 * 封装通用的请求方法
 * 自定义菜单需要调用的接口,都是https请求,而非http,同时需要一个证书信任管理器类 MyX509TrustManagerUtil
 *
 * 1)支持HTTPS请求;
 *
 * 2)支持GET、POST两种方式;
 *
 * 3)支持参数提交,也支持无参数的情况;
 * @version: 1.0
 */
@Slf4j
public class HttpRequestUtil {
    /**
     * 发起https请求并获取结果
     *
     * @param requestUrl    请求地址
     * @param requestMethod 请求方式(GET、POST)
     * @param outputStr     提交的数据
     * @return JSONObject(通过JSONObject.get ( key)的方式获取json对象的属性值)
     */
    public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
        JSONObject jsonObject = null;
        StringBuffer buffer = new StringBuffer();
        try {
            // 创建SSLContext对象,并使用我们指定的信任管理器初始化
            TrustManager[] tm = {new MyX509TrustManagerUtil()};
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            // 从上述SSLContext对象中得到SSLSocketFactory对象
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            URL url = new URL(requestUrl);
            HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
            httpUrlConn.setSSLSocketFactory(ssf);

            httpUrlConn.setDoOutput(true);
            httpUrlConn.setDoInput(true);
            httpUrlConn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            httpUrlConn.setRequestMethod(requestMethod);

            if ("GET".equalsIgnoreCase(requestMethod))
                httpUrlConn.connect();

            // 当有数据需要提交时
            if (null != outputStr) {
                OutputStream outputStream = httpUrlConn.getOutputStream();
                // 注意编码格式,防止中文乱码
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }

            // 将返回的输入流转换成字符串
            InputStream inputStream = httpUrlConn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String str = null;
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            bufferedReader.close();
            inputStreamReader.close();
            // 释放资源
            inputStream.close();
            inputStream = null;
            httpUrlConn.disconnect();
            jsonObject = JSONObject.fromObject(buffer.toString());
        } catch (ConnectException ce) {
            log.error("Weixin server connection timed out.");
        } catch (Exception e) {
            log.error("https request error:{}", e);
        }
        return jsonObject;
    }
}

新建公众平台AccessToken 获取工具类AccessTokenUtil

封装获取凭证access_token的方法

import lombok.extern.slf4j.Slf4j;
import me.zqt.wx.constant.WechatInterface;
import me.zqt.wx.model.AccessToken;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;

/**
 * @auther: zqtao
 * @description: 公众平台AccessToken 获取工具类
 * @version: 1.0
 */
@Slf4j
public class AccessTokenUtil {

    /**
     * 获取access_token
     *
     * @param appid     凭证
     * @param appsecret 密钥
     * @return 接口访问凭证
     */
    public static AccessToken getAccessToken(String appid, String appsecret) {
        AccessToken accessToken = null;

        // access_token的接口地址ACCESS_TOKEN_URL (GET) 限200(次/天)
        String requestUrl = WechatInterface.ACCESS_TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret);
        JSONObject jsonObject = HttpRequestUtil.httpRequest(requestUrl, "GET", null);
        // 如果请求成功
        if (null != jsonObject) {
            try {
                accessToken = new AccessToken();
                accessToken.setToken(jsonObject.getString("access_token"));
                accessToken.setExpiresIn(jsonObject.getInt("expires_in"));
            } catch (JSONException e) {
                accessToken = null;
                // 获取token失败
                log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
            }
        }
        return accessToken;
    }
}

新建微信自定义菜单核心服务实现类MenuManagerServiceImpl

import lombok.extern.slf4j.Slf4j;
import me.zqt.wx.constant.LogConstant;
import me.zqt.wx.constant.WechatInterface;
import me.zqt.wx.model.menu.Menu;
import me.zqt.wx.service.MenuManagerService;
import me.zqt.wx.utils.HttpRequestUtil;
import net.sf.json.JSONObject;


/**
 * @auther: zqtao
 * @description: 微信自定义菜单核心服务实现类
 * @version: 1.0
 */
@Slf4j
public class MenuManagerServiceImpl implements MenuManagerService {

    /**
     * 创建菜单
     *
     * @param menu        菜单实例
     * @param accessToken 有效的access_token
     * @return 0表示成功,其他值表示失败
     */
    @Override
    public int createMenu(Menu menu, String accessToken) {
        log.info(LogConstant.LOG_INFO.replace("INFO","开始创建菜单"));

        int result = 0;
        // MENU_CREATE_URL菜单创建(POST) 限100(次/天)
        // 拼装创建菜单的url
        String url = WechatInterface.MENU_CREATE_URL.replace("ACCESS_TOKEN", accessToken);
        // 将菜单对象转换成json字符串
        String jsonMenu = JSONObject.fromObject(menu).toString();
        // 调用接口创建菜单
        JSONObject jsonObject = HttpRequestUtil.httpRequest(url, "POST", jsonMenu);

        if (null != jsonObject) {
            if (0 != jsonObject.getInt("errcode")) {
                result = jsonObject.getInt("errcode");
                log.error("======>>>   创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
            }
        }
        log.info(LogConstant.LOG_INFO.replace("INFO","结束创建菜单"));
        return result;
    }

    /**
     * 删除菜单
     * 对应创建接口,正确的Json返回结果:
     * {"errcode":0,"errmsg":"ok"}
     *
     * @param accessToken 有效的access_token
     * @return 0表示成功,其他值表示失败
     */
    @Override
    public int deleteMenu(String accessToken) {
        int result = 0;
        String url = WechatInterface.MENU_DELETE_URL.replace("ACCESS_TOKEN", accessToken);
        // 调用接口删除菜单
        JSONObject jsonObject = HttpRequestUtil.httpRequest(url, "GET", null);
        if (null != jsonObject) {
            if (0 != jsonObject.getInt("errcode")) {
                result = jsonObject.getInt("errcode");
                log.error("======>>>   删除菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
            }
        }
        return result;
    }
}

菜单管理器类MenuManagerDriverMain: 初始化自定义菜单

import lombok.extern.slf4j.Slf4j;
import me.zqt.wx.constant.LogConstant;
import me.zqt.wx.constant.SignatureConstant;
import me.zqt.wx.model.AccessToken;
import me.zqt.wx.model.menu.*;
import me.zqt.wx.service.MenuManagerService;
import me.zqt.wx.service.impl.MenuManagerServiceImpl;
import me.zqt.wx.utils.AccessTokenUtil;

/**
 * @auther: zqtao
 * @description: 菜单管理器类: 初始化自定义菜单
 * @version: 1.0
 */
@Slf4j
public class MenuManagerDriverMain {

    public static void main(String[] args) {
        log.info(LogConstant.LOG_INFO.replace("INFO", "开始初始化自定义菜单"));
        // 第三方用户唯一凭证
        String appId = SignatureConstant.APP_ID;
        // 第三方用户唯一凭证密钥
        String appSecret = SignatureConstant.APP_SECRET;

        // 调用接口获取access_token
        AccessToken at = AccessTokenUtil.getAccessToken(appId, appSecret);
        log.info(LogConstant.LOG_INFO.replace("INFO", "AccessToken is :" + at.getToken()));

        if (at != null) {
            MenuManagerService menuManagerService = new MenuManagerServiceImpl();
            // 调用接口创建菜单
            int result = menuManagerService.createMenu(getMenu(), at.getToken());
            // 调用接口删除菜单
//            int result = menuManagerService.deleteMenu(at.getToken());
            // 判断菜单创建结果
            if (0 == result)
                log.info(LogConstant.LOG_INFO.replace("INFO", "菜单操作成功!"));
            else
                log.info(LogConstant.LOG_INFO.replace("INFO", "菜单操作失败,错误码:" + result));
        }
        log.info(LogConstant.LOG_INFO.replace("ERROR", "结束初始化自定义菜单"));
    }

    /**
     * 组装菜单数据
     */
    private static Menu getMenu() {

        // 子按钮(菜单)
        ClickButton btn11 = new ClickButton();
        btn11.setName("开发工具");
        btn11.setType("click");
        btn11.setKey("11");

        ViewButton btn12 = new ViewButton();
        btn12.setName("资源合集");
        btn12.setType("view");
        btn12.setUrl("https://www.baidu.com/");

        ViewButton btn21 = new ViewButton();
        btn21.setName("知乎");
        btn21.setType("view");
        btn21.setUrl("https://www.zhihu.com/people/zqtao23/activities");

        ViewButton btn22 = new ViewButton();
        btn22.setName("");
        btn22.setType("view");
        btn22.setUrl("https://www.jianshu.com/u/7110a2ba6f9e");

        ViewButton btn31 = new ViewButton();
        btn31.setName("资源屋");
        btn31.setType("view");
        btn31.setUrl("http://www.baidu.com");

        ViewButton btn32 = new ViewButton();
        btn32.setName("Github");
        btn32.setType("view");
        btn32.setUrl("https://github.com/zqtao2332");

        ViewButton btn33 = new ViewButton();
        btn33.setName("博客");
        btn33.setType("view");
        btn33.setUrl("http://www.zqtaotao.cn");


        // 一级菜单
        ComplexButton mainBtn1 = new ComplexButton();
        mainBtn1.setName("开发助手");
        mainBtn1.setSub_button(new BasicButton[]{btn11, btn12});

        ComplexButton mainBtn2 = new ComplexButton();
        mainBtn2.setName("知识驿站");
        mainBtn2.setSub_button(new BasicButton[]{btn21, btn22});

        ComplexButton mainBtn3 = new ComplexButton();
        mainBtn3.setName("更多体验");
        mainBtn3.setSub_button(new BasicButton[]{btn31, btn32, btn33});

        Menu menu = new Menu();
        menu.setButton(new BasicButton[]{mainBtn1, mainBtn2, mainBtn3});
        return menu;
    }
}

运行MenuManagerDriverMain.java 即可创建自定义的菜单

删除自定义菜单只需要开放删除操作

// int result = menuManagerService.createMenu(getMenu(), at.getToken());
// 调用接口删除菜单
int result = menuManagerService.deleteMenu(at.getToken());

更多内容以及源码获取请关注公众号:怪兽疯了

我们一起学习探讨!

怪兽疯了.jpg

你可能感兴趣的:(Java微信公众号开发6-自定义菜单)