想要实现自定义菜单的功能,需要有已认证订阅号和已认证服务号。对于测试开发来说,可以直接申请一个测试账号:http://mp.weixin.qq.com/debug...
同样需要token的验证,前期接口已经定义好了,直接拿来就可以
根据开发者文档,自定义菜单注意:
1、自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
2、一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。
3、创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
自定义菜单接口可实现多种类型按钮,如下:
接口调用请求说明
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi...
click和view的请求示例
{
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"miniprogram",
"name":"wxa",
"url":"http://mp.weixin.qq.com",
"appid":"wx286b93c14bbf93aa",
"pagepath":"pages/lunar/index"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}]
}]
}
参数说明:
返回结果
正确时的返回JSON数据包如下:
{"errcode":0,"errmsg":"ok"}
错误时的返回JSON数据包如下(示例为无效菜单名长度):
{"errcode":40018,"errmsg":"invalid button name size"}
以上均来自官方说明文档。
pom引入jar包:
net.sf.ezmorph
ezmorph
1.0.6
commons-beanutils
commons-beanutils
1.8.0
commons-collections
commons-collections
3.2.1
commons-lang
commons-lang
2.3
commons-logging
commons-logging
1.1.1
net.sf.json-lib
json-lib
2.4
jdk15
dom4j
dom4j
1.6.1
com.thoughtworks.xstream
xstream
1.4.9
定义菜单实体类:
/**
* 按钮基类
* @author zhoumin
* @create 2018-07-11 15:22
*/
@Setter
@Getter
public class BasicButton {
private String name;
private String url;
}
/**
* 普通按钮
*
* @author zhoumin
* @create 2018-07-12 9:56
*/
@Setter
@Getter
public class CommonButton extends BasicButton {
private String type;
private String key;
}
/**
* 父按钮
* @author zhoumin
* @create 2018-07-11 15:24
*/
@Setter
@Getter
public class ComplexButton extends BasicButton {
private BasicButton[] sub_button;
}
/**
* 菜单
* @author zhoumin
* @create 2018-07-11 15:22
*/
@Setter
@Getter
public class Menu {
private BasicButton[] button;
}
/**
* @author zhoumin
* @create 2018-07-11 15:23
*/
@Setter
@Getter
public class ViewButton extends BasicButton {
private String type;
private String name;
private String url;
}
/**
* 凭证
* @author zhoumin
* @create 2018-07-11 15:22
*/
@Setter
@Getter
public class AccessToken {
/**
* 获取到的凭证
*/
private String accessToken;
/**
* 凭证有效时间,单位:秒
*/
private int expiresIn;
}
定义工具类:
/**
* 实现接口
* @author zhoumin
* @create 2018-07-12 10:01
*/
public class MyX509TrustManager implements X509TrustManager {
// 检查客户端证书
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
// 检查服务器端证书
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
// 返回受信任的X509证书数组
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
/**
* @author zhoumin
* @create 2018-07-12 10:04
*/
public class CommonWechatUtil {
private static Logger log = LoggerFactory.getLogger(CommonWechatUtil.class);
// 凭证获取(GET)
public final static String token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
/**
* 发送https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
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 conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
log.error("连接超时:{}", ce);
} catch (Exception e) {
log.error("https请求异常:{}", e);
}
return jsonObject;
}
/**
* 获取接口访问凭证
*
* @param appid 凭证
* @param appsecret 密钥
* @return
*/
public static AccessToken getToken(String appid, String appsecret) {
AccessToken token = null;
String requestUrl = token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
// 发起GET请求获取凭证
JSONObject jsonObject = httpsRequest(requestUrl, "GET", null);
if (null != jsonObject) {
try {
token = new AccessToken();
token.setAccessToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
} catch (JSONException e) {
token = null;
// 获取token失败
log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}
}
return token;
}
/**
* URL编码(utf-8)
*
* @param source
* @return
*/
public static String urlEncodeUTF8(String source) {
String result = source;
try {
result = java.net.URLEncoder.encode(source, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
/**
* 根据内容类型判断文件扩展名
*
* @param contentType 内容类型
* @return
*/
public static String getFileExt(String contentType) {
String fileExt = "";
if ("image/jpeg".equals(contentType))
fileExt = ".jpg";
else if ("audio/mpeg".equals(contentType))
fileExt = ".mp3";
else if ("audio/amr".equals(contentType))
fileExt = ".amr";
else if ("video/mp4".equals(contentType))
fileExt = ".mp4";
else if ("video/mpeg4".equals(contentType))
fileExt = ".mp4";
return fileExt;
}
// 菜单创建(POST) 限100(次/天)
public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
/**
* 创建菜单
*
* @param menu 菜单实例
* @param accessToken 有效的access_token
* @return 0表示成功,其他值表示失败
*/
public static int createMenu(Menu menu, String accessToken) {
int result = 0;
// 拼装创建菜单的url
String url = menu_create_url.replace("ACCESS_TOKEN", accessToken);
// 将菜单对象转换成json字符串
String jsonMenu = JSONObject.fromObject(menu).toString();
// 调用接口创建菜单
JSONObject jsonObject = httpsRequest(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"));
}
}
return result;
}
public static String menu_get_url = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";
/**
* 查询菜单
*
* @param accessToken 有效的access_token
* @return 0表示成功,其他值表示失败
*/
public static JSONObject getMenu(String accessToken) {
int result = 0;
// 拼装创建菜单的url
String url = menu_get_url.replace("ACCESS_TOKEN", accessToken);
// 将菜单对象转换成json字符串
// String jsonMenu = JSONObject.fromObject(menu).toString();
// 调用接口创建菜单
JSONObject jsonObject = httpsRequest(url, "POST", null);
return jsonObject;
}
public static String menu_delete_url = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";
/**
* 查询菜单
*
* @param accessToken 有效的access_token
* @return 0表示成功,其他值表示失败
*/
public static int deleteMenu(String accessToken) {
int result = 0;
// 拼装创建菜单的url
String url = menu_delete_url.replace("ACCESS_TOKEN", accessToken);
// 调用接口创建菜单
JSONObject jsonObject = httpsRequest(url, "POST", 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;
}
定义常量:
/**
* 添加id和密码信息
* @author zhoumin
* @create 2018-07-11 17:07
*/
public class ConstantWeChat {
public static final String APPID = "自己的AppId";
public static final String APPSECRET = "自己的APPSecret";
}
实现方法:
/**
* @author zhoumin
* @create 2018-07-11 15:39
*/
public interface MenuService {
}
/**
* @author zhoumin
* @create 2018-07-11 15:40
*/
@Service("menuService")
public class MenuServiceImpl implements MenuService {
private static final Logger LOGGER = LoggerFactory.getLogger(MenuServiceImpl.class);
// @Override
public static Boolean createMenu() {
// 第三方用户唯一凭证
String appId = ConstantWeChat.APPID;
// 第三方用户唯一凭证密钥
String appSecret = ConstantWeChat.APPSECRET;
// 调用接口获取access_token
AccessToken at = CommonWechatUtil.getToken(appId, appSecret);
if (null != at) {
// 调用接口创建菜单
int result = CommonWechatUtil.createMenu(getMenu(), at.getAccessToken());
// 判断菜单创建结果
if (0 == result){
LOGGER.info("菜单创建成功!");
return true;
}
else{
LOGGER.info("菜单创建失败,错误码:" + result);
return false;
}
}
return false;
}
// @Override
public static JSONObject getMenuBtn() {
// 第三方用户唯一凭证
String appId = ConstantWeChat.APPID;
// 第三方用户唯一凭证密钥
String appSecret = ConstantWeChat.APPSECRET;
// 调用接口获取access_token
AccessToken at = CommonWechatUtil.getToken(appId, appSecret);
if (null != at) {
// 调用接口获取菜单
JSONObject result = CommonWechatUtil.getMenu(at.getAccessToken());
// 判断菜单创建结果
if (null != result && result.size()>0){
LOGGER.info("菜单查询成功!");
return result;
}
else{
LOGGER.info("菜单查询失败,错误码:" + result);
return null;
}
}
return null;
}
// @Override
public static Boolean deleteMenu() {
// 第三方用户唯一凭证
String appId = ConstantWeChat.APPID;
// 第三方用户唯一凭证密钥
String appSecret = ConstantWeChat.APPSECRET;
// 调用接口获取access_token
AccessToken at = CommonWechatUtil.getToken(appId, appSecret);
if (null != at) {
// 调用接口删除菜单
int result = CommonWechatUtil.deleteMenu(at.getAccessToken());
// 判断菜单删除结果
if (0 == result){
LOGGER.info("菜单删除成功!");
return true;
}
else{
LOGGER.info("菜单删除失败,错误码:" + result);
return false;
}
}
return false;
}
/**
* 组装菜单数据
*
* @return
* @throws UnsupportedEncodingException
*/
private static Menu getMenu() {
ViewButton btn11 = new ViewButton();
btn11.setName("我是");
btn11.setType("view");
btn11.setUrl("https://segmentfault.com/u/panzi_5abcaf30a5e6b");
ViewButton btn21 = new ViewButton();
btn21.setName("盘子");
btn21.setType("view");
btn21.setUrl("https://segmentfault.com/u/panzi_5abcaf30a5e6b");
ViewButton btn31 = new ViewButton();
btn31.setName("谢谢");
btn31.setType("view");
btn31.setUrl("https://segmentfault.com/u/panzi_5abcaf30a5e6b");
ViewButton btn41 = new ViewButton();
btn41.setName("关注");
btn41.setType("view");
btn41.setUrl("https://segmentfault.com/u/panzi_5abcaf30a5e6b");
CommonButton btn12 = new CommonButton();
btn12.setName("赞");
btn12.setType("click");
btn12.setKey("return_content");
ComplexButton mainBtn1 = new ComplexButton();
mainBtn1.setName("自我介绍");
mainBtn1.setSub_button(new BasicButton[] { btn11, btn21,btn31});
ComplexButton mainBtn2 = new ComplexButton();
mainBtn2.setName("谢谢!");
mainBtn2.setSub_button(new BasicButton[] { btn41, btn12 });
/**
*在某个一级菜单下没有二级菜单的情况,menu应该这样定义:
* menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 });
*/
Menu menu = new Menu();
menu.setButton(new BasicButton[] { mainBtn1, mainBtn2});
return menu;
}
public static void main(String[] args) {
createMenu();
}
}
这里直接运行main方法就好了
找到测试二维码,扫描关注,可以看到菜单已经有啦!!
如果修改了话,可以取消关注再添加关注,就能看到更改信息后的菜单信息。
对于菜单的点击事件,可以回到我们的newMessageRequest方法中添加代码:
// 自定义菜单点击事件
else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {
String eventKey = requestMap.get("EventKey");// 事件KEY值,与创建自定义菜单时指定的KEY值对应
if (eventKey.equals("return_content")) {
TextMessage text = new TextMessage();
text.setContent("赞赞赞");
text.setToUserName(fromUserName);
text.setFromUserName(toUserName);
text.setCreateTime(new Date().getTime());
text.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
respMessage = MessageUtil.textMessageToXml(text);
}
}
源码地址:https://github.com/zhouminpz/...