微信开发者模式先删除在创建菜单

2019-12-04 10:52:02 weixin_43229729 阅读数 204

借鉴于:https://blog.csdn.net/u014740338/article/details/79670779

微信打开开发者模式后,以前通过微信后台配置的菜单就失效了,需要通过我们自己服务器后台配置(需要开发),还有一种比较简单的方法,就是通过微信提供的“微信公众平台接口测试工具”进行配置。下面重点说一下通过微信提供的测试工具进行配置的过程。

一、进入微信接口调试页面

接口参数:

access_token: 是公众号访问接口用的,需要填写要修改的公众号access_token。

body: 创建菜单的JSON串。示例如下:(创建一级菜单+二级菜单

 

{
	"button": [{
		"type": "view",
		"name": "技术查看",
		"url": "https://blog.csdn.net/weixin_43229729"
	},
	{
		"name": "学习",
		"sub_button": [{
			"type": "view",
			"name": "遇到的问题",
			"url": "https://blog.csdn.net/weixin_43229729"
		},
		{
			"type": "view",
			"name": "学习记录",
			"url": "https://blog.csdn.net/weixin_43229729"
		}]
	},
	{
		"type": "view",
		"name": "我的博客",
		"url": "https://blog.csdn.net/weixin_43229729"
	}]

}

body写好之后点击“检查问题”按钮。若创建成功会返回一下信息:

 

二,通过调用微信api进行设置(自定义菜单创建/查询/删除)

关于自定义菜单的查询和删除,直接看微信官方文档。

微信官网文档:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Deleting_Custom-Defined_Menu.html

2016-06-16 11:34:55 w410589502 阅读数 12534

注:文章代码来源于柳峰的微信公众平台应用开发

微信开发公众平台自定义菜单需要花钱认证才能实现,不想花钱只能玩测试账号了,不过这并不影响开发。我的开发都是基于柳峰老师的微信公众平台应用开发做的。

只要我们使用公众平台测试账号就可以开发自定义菜单了,比较方便,测试账号开放了很多接口,很方便。

在开发自定义菜单的时候可以参考微信公众平台开发者文档的自定义菜单创建。

一、自定义菜单

1、自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
2、一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。
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
二、访问自定义菜单接口

1:获取access_token

  自定义菜单的创建、查询和删除需要调用公众平台开放的自定义菜单接口,而调用该接口需要先获取access_token(接口访问凭证),这些接口全都是基于https协议的,因此我们先要解决如何在java程序中发送https请求的问题。

  获取接口访问凭证access_token:


获取access_token是通过GET方式访问如下链接:
<span style="font-size:12px;">https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET</span>

链接中有三个参数,分别是grant_type、appid和secret。根据图中的参数说明,grant_type传固定值client_credential,而appid和secret就是申请完自定义菜单后微信分配给我们的。

请求发送成功后,微信服务器会返回一个json串,包含access_token和expires_in两个元素。其中,access_token就是我们最终需要的凭证,而expires_in是凭证的有效期,单位是秒,7200秒也就是2个小时。这就意味着,不是每次访问特殊接口,都需要重新获取一次access_token,只要access_token还在有效期内,就一直可以使用。

2:菜单创建

创建菜单要调用菜单接口

接口调用请求说明

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

其实就是向地址https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN以POST方式提交一个JSON格式的菜单字符串。

三、封装通用请求方法(自定义信任管理器)

创建菜单需要两个接口,一个是获取access_token接口,一个是自定义菜单接口,都是https请求。

1:创建证书信任管理器

对于https请求,我们需要一个证书信任管理器,这个管理器类需要自己定义,但需要实现X509TrustManager接口,代码如下:

package org.liufeng.weixin.util;  
  
import java.security.cert.CertificateException;  
import java.security.cert.X509Certificate;  
  
import javax.net.ssl.X509TrustManager;  
  
/** 
 * 证书信任管理器(用于https请求) 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class MyX509TrustManager implements X509TrustManager {  
  
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {  
    }  
  
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {  
    }  
  
    public X509Certificate[] getAcceptedIssuers() {  
        return null;  
    }  
} 
证书管理器的作用就是信任指定所有证书。
2:创建通用https请求

通用https请求应该

1)支持HTTPS请求;

2)支持GET、POST两种方式;

3)支持参数提交,也支持无参数的情况;

package org.liufeng.weixin.util;  
  
import java.io.BufferedReader;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.io.OutputStream;  
import java.net.ConnectException;  
import java.net.URL;  
  
import javax.net.ssl.HttpsURLConnection;  
import javax.net.ssl.SSLContext;  
import javax.net.ssl.SSLSocketFactory;  
import javax.net.ssl.TrustManager;  
  
import net.sf.json.JSONObject;  
  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
/** 
 * 公众平台通用接口工具类 
 *  
 * @author liuyq 
 * @date 2013-08-09 
 */  
public class WeixinUtil {  
    private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);  
  
    /** 
     * 发起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 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 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;  
    }  
}  


四、封装菜单实体类

  1:按钮的分类

  我们通常会把自定义菜单项看做是按钮,按钮的类型分成click(点击事件)和view(访问网页)。

  click类型的按钮有type、name和key3个属性,而view类型的按钮有type、name和url3个属性

  

  2:首先是调用获取凭证接口后,微信服务器会返回json格式的数据:{"access_token":"ACCESS_TOKEN","expires_in":7200},我们将其封装为一个AccessToken对象,对象有二个属性:token和expiresIn,代码如下:

package org.liufeng.weixin.pojo;  
  
/** 
 * 微信通用接口凭证 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class AccessToken {  
    // 获取到的凭证  
    private String token;  
    // 凭证有效时间,单位:秒  
    private int expiresIn;  
  
    public String getToken() {  
        return token;  
    }  
  
    public void setToken(String token) {  
        this.token = token;  
    }  
  
    public int getExpiresIn() {  
        return expiresIn;  
    }  
  
    public void setExpiresIn(int expiresIn) {  
        this.expiresIn = expiresIn;  
    }  
}  

接下来是对菜单结构的封装。因为我们是采用面向对象的编程方式,最终提交的json格式菜单数据就应该是由对象直接转换得到,而不是在程序代码中拼一大堆json数据。菜单结构封装的依据是公众平台API文档中给出的那一段json格式的菜单结构,如下所示:

click和view的请求示例

{
     "button":[
     {	
          "type":"click",
          "name":"今日歌曲",
          "key":"V1001_TODAY_MUSIC"
      },
      {
           "name":"菜单",
           "sub_button":[
           {	
               "type":"view",
               "name":"搜索",
               "url":"http://www.soso.com/"
            },
            {
               "type":"view",
               "name":"视频",
               "url":"http://v.qq.com/"
            },
            {
               "type":"click",
               "name":"赞一下我们",
               "key":"V1001_GOOD"
            }]
       }]
 }
3:封装菜单结构

每个按钮对象都要一个共同的name属性,因此需要定义一个按钮对象基类,所有的按钮对象都需要继承该类。基类的代码如下:

package org.liufeng.weixin.pojo;  
  
/** 
 * 按钮的基类 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class Button {  
    private String name;  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
}

接着是子菜单项的封装。这里对子菜单是这样定义的:没有子菜单的菜单项,有可能是二级菜单项,也有可能是不含二级菜单的一级菜单。这类子菜单项一定会包含三个属性:type、name和key,封装的代码如下:

package org.liufeng.weixin.pojo;  
  
/** 
 * 普通按钮(子按钮) 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class CommonButton extends Button {  
    private String type;  
    private String 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;  
    }  
}  

对父菜单项的定义:包含有二级菜单项的一级菜单。这类菜单项包含有二个属性:name和sub_button,而sub_button以是一个子菜单项数组。父菜单项的封装代码如下:

package org.liufeng.weixin.pojo;  
  
/** 
 * 复杂按钮(父按钮) 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class ComplexButton extends Button {  
    private Button[] sub_button;  
  
    public Button[] getSub_button() {  
        return sub_button;  
    }  
  
    public void setSub_button(Button[] sub_button) {  
        this.sub_button = sub_button;  
    }  
} 

对整个菜单进行封装,菜单对象包含多个菜单项(最多只能有3个),这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项)

package org.liufeng.weixin.pojo;  
  
/** 
 * 菜单 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class Menu {  
    private Button[] button;  
  
    public Button[] getButton() {  
        return button;  
    }  
  
    public void setButton(Button[] button) {  
        this.button = button;  
    }  
}  
这样我们就完成了菜单实体类的封装。


凭证access_token的获取方法

继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于获取接口访问凭证:


// 获取access_token的接口地址(GET) 限200(次/天)  
public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";  
  
/** 
 * 获取access_token 
 *  
 * @param appid 凭证 
 * @param appsecret 密钥 
 * @return 
 */  
public static AccessToken getAccessToken(String appid, String appsecret) {  
    AccessToken accessToken = null;  
  
    String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);  
    JSONObject jsonObject = 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;  
} 

自定义菜单的创建方法

继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于创建自定义菜单:

// 菜单创建(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 = 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"));  
        }  
    }  
  
    return result;  
}  

调用封装的方法创建自定义菜单

 

package org.liufeng.weixin.main;  
  
import org.liufeng.weixin.pojo.AccessToken;  
import org.liufeng.weixin.pojo.Button;  
import org.liufeng.weixin.pojo.CommonButton;  
import org.liufeng.weixin.pojo.ComplexButton;  
import org.liufeng.weixin.pojo.Menu;  
import org.liufeng.weixin.util.WeixinUtil;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
  
/** 
 * 菜单管理器类 
 *  
 * @author liufeng 
 * @date 2013-08-08 
 */  
public class MenuManager {  
    private static Logger log = LoggerFactory.getLogger(MenuManager.class);  
  
    public static void main(String[] args) {  
        // 第三方用户唯一凭证  
        String appId = "000000000000000000";  
        // 第三方用户唯一凭证密钥  
        String appSecret = "00000000000000000000000000000000";  
  
        // 调用接口获取access_token  
        AccessToken at = WeixinUtil.getAccessToken(appId, appSecret);  
  
        if (null != at) {  
            // 调用接口创建菜单  
            int result = WeixinUtil.createMenu(getMenu(), at.getToken());  
  
            // 判断菜单创建结果  
            if (0 == result)  
                log.info("菜单创建成功!");  
            else  
                log.info("菜单创建失败,错误码:" + result);  
        }  
    }  
  
    /** 
     * 组装菜单数据 
     *  
     * @return 
     */  
    private static Menu getMenu() {  
        CommonButton btn11 = new CommonButton();  
        btn11.setName("天气预报");  
        btn11.setType("click");  
        btn11.setKey("11");  
  
        CommonButton btn12 = new CommonButton();  
        btn12.setName("公交查询");  
        btn12.setType("click");  
        btn12.setKey("12");  
  
        CommonButton btn13 = new CommonButton();  
        btn13.setName("周边搜索");  
        btn13.setType("click");  
        btn13.setKey("13");  
  
        CommonButton btn14 = new CommonButton();  
        btn14.setName("历史上的今天");  
        btn14.setType("click");  
        btn14.setKey("14");  
  
        CommonButton btn21 = new CommonButton();  
        btn21.setName("歌曲点播");  
        btn21.setType("click");  
        btn21.setKey("21");  
  
        CommonButton btn22 = new CommonButton();  
        btn22.setName("经典游戏");  
        btn22.setType("click");  
        btn22.setKey("22");  
  
        CommonButton btn23 = new CommonButton();  
        btn23.setName("美女电台");  
        btn23.setType("click");  
        btn23.setKey("23");  
  
        CommonButton btn24 = new CommonButton();  
        btn24.setName("人脸识别");  
        btn24.setType("click");  
        btn24.setKey("24");  
  
        CommonButton btn25 = new CommonButton();  
        btn25.setName("聊天唠嗑");  
        btn25.setType("click");  
        btn25.setKey("25");  
  
        CommonButton btn31 = new CommonButton();  
        btn31.setName("Q友圈");  
        btn31.setType("click");  
        btn31.setKey("31");  
  
        CommonButton btn32 = new CommonButton();  
        btn32.setName("电影排行榜");  
        btn32.setType("click");  
        btn32.setKey("32");  
  
        CommonButton btn33 = new CommonButton();  
        btn33.setName("幽默笑话");  
        btn33.setType("click");  
        btn33.setKey("33");  
  
        ComplexButton mainBtn1 = new ComplexButton();  
        mainBtn1.setName("生活助手");  
        mainBtn1.setSub_button(new CommonButton[] { btn11, btn12, btn13, btn14 });  
  
        ComplexButton mainBtn2 = new ComplexButton();  
        mainBtn2.setName("休闲驿站");  
        mainBtn2.setSub_button(new CommonButton[] { btn21, btn22, btn23, btn24, btn25 });  
  
        ComplexButton mainBtn3 = new ComplexButton();  
        mainBtn3.setName("更多体验");  
        mainBtn3.setSub_button(new CommonButton[] { btn31, btn32, btn33 });  
  
        /** 
         * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项<br> 
         *  
         * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢?<br> 
         * 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义:<br> 
         * menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 }); 
         */  
        Menu menu = new Menu();  
        menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 });  
  
        return menu;  
    }  
}

注意:在运行以上代码时,需要将appId和appSecret换成你自己公众号的。


响应菜单点击事件

package org.liufeng.course.service;  
  
import java.util.Date;  
import java.util.Map;  
  
import javax.servlet.http.HttpServletRequest;  
  
import org.liufeng.course.message.resp.TextMessage;  
import org.liufeng.course.util.MessageUtil;  
  
/** 
 * 核心服务类 
 *  
 * @author liufeng 
 * @date 2013-05-20 
 */  
public class CoreService {  
    /** 
     * 处理微信发来的请求 
     *  
     * @param request 
     * @return 
     */  
    public static String processRequest(HttpServletRequest request) {  
        String respMessage = null;  
        try {  
            // 默认返回的文本消息内容  
            String respContent = "请求处理异常,请稍候尝试!";  
  
            // xml请求解析  
            Map<String, String> requestMap = MessageUtil.parseXml(request);  
  
            // 发送方帐号(open_id)  
            String fromUserName = requestMap.get("FromUserName");  
            // 公众帐号  
            String toUserName = requestMap.get("ToUserName");  
            // 消息类型  
            String msgType = requestMap.get("MsgType");  
  
            // 回复文本消息  
            TextMessage textMessage = new TextMessage();  
            textMessage.setToUserName(fromUserName);  
            textMessage.setFromUserName(toUserName);  
            textMessage.setCreateTime(new Date().getTime());  
            textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);  
            textMessage.setFuncFlag(0);  
  
            // 文本消息  
            if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {  
                respContent = "您发送的是文本消息!";  
            }  
            // 图片消息  
            else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {  
                respContent = "您发送的是图片消息!";  
            }  
            // 地理位置消息  
            else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {  
                respContent = "您发送的是地理位置消息!";  
            }  
            // 链接消息  
            else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {  
                respContent = "您发送的是链接消息!";  
            }  
            // 音频消息  
            else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {  
                respContent = "您发送的是音频消息!";  
            }  
            // 事件推送  
            else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {  
                // 事件类型  
                String eventType = requestMap.get("Event");  
                // 订阅  
                if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {  
                    respContent = "谢谢您的关注!";  
                }  
                // 取消订阅  
                else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {  
                    // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息  
                }  
                // 自定义菜单点击事件  
                else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {  
                    // 事件KEY值,与创建自定义菜单时指定的KEY值对应  
                    String eventKey = requestMap.get("EventKey");  
  
                    if (eventKey.equals("11")) {  
                        respContent = "天气预报菜单项被点击!";  
                    } else if (eventKey.equals("12")) {  
                        respContent = "公交查询菜单项被点击!";  
                    } else if (eventKey.equals("13")) {  
                        respContent = "周边搜索菜单项被点击!";  
                    } else if (eventKey.equals("14")) {  
                        respContent = "历史上的今天菜单项被点击!";  
                    } else if (eventKey.equals("21")) {  
                        respContent = "歌曲点播菜单项被点击!";  
                    } else if (eventKey.equals("22")) {  
                        respContent = "经典游戏菜单项被点击!";  
                    } else if (eventKey.equals("23")) {  
                        respContent = "美女电台菜单项被点击!";  
                    } else if (eventKey.equals("24")) {  
                        respContent = "人脸识别菜单项被点击!";  
                    } else if (eventKey.equals("25")) {  
                        respContent = "聊天唠嗑菜单项被点击!";  
                    } else if (eventKey.equals("31")) {  
                        respContent = "Q友圈菜单项被点击!";  
                    } else if (eventKey.equals("32")) {  
                        respContent = "电影排行榜菜单项被点击!";  
                    } else if (eventKey.equals("33")) {  
                        respContent = "幽默笑话菜单项被点击!";  
                    }  
                }  
            }  
  
            textMessage.setContent(respContent);  
            respMessage = MessageUtil.textMessageToXml(textMessage);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
  
        return respMessage;  
    }  
} <span style="color:#3333ff;"> </span>





2018-07-24 16:53:07 Andrewniu 阅读数 16159

微信5.0发布

2013年8月5日,伴随着微信5.0 iPhone版的发布,公众平台也进行了重要的更新,主要包括:

1)运营主体为组织,可选择成为服务号或者订阅号;

2)服务号可以申请自定义菜单;

3)使用QQ登录的公众号,可以升级为邮箱登录;

4)使用邮箱登录的公众号,可以修改登录邮箱;

5)编辑图文消息可选填作者;

6)群发消息可以同步到腾讯微博。

其中,大家议论最多的当属前两条,就是关于帐号类型和自定义菜单的更新,我这里做几点补充说明:

1)目前公众号类型分为两种:服务号和订阅号,8月5日平台更新后所有的帐号默认为订阅号,有一次转换成服务号的机会;

2)服务号主要面向企业、政府和其他组织,而订阅号主要面向媒体和个人;

3)只有服务号可以申请自定义菜单,订阅号不能申请;

4)服务号每月只能群发一条消息,而订阅号每天能群发一条消息。

平台更新后,让很多人纠结的是自定义菜单和每天群发一条消息不可兼得,对此,我不想过多评论。

 

引言及内容概要

在微信5.0以前,自定义菜单是作为一种内测资格使用的,只有少数公众帐号拥有菜单,因此出现很多企业为了弄到菜单不惜重金求购。现如今,一大批帐号从订阅号转为服务号,很多都是奔着自定义菜单去的。而且,经测试发现,微信最近的审核放松很多,只要申请服务号、自定义菜单的基本都成功了,根本不管填写的资料真伪。不知道以后微信会不会翻脸,要求补全企业资料,那将会是一种给小孩一颗糖吃再把他打哭的感觉。。。

自定义菜单是申请到了,到底该怎么创建、怎么使用呢?最近几天不管是微信官方交流群,还是在我博客留言里,都能够看到不少开发者都在为这个发愁。本篇文章就为大家解决这个难题。

 

自定义菜单的创建步骤

1、找到AppId和AppSecret。自定义菜单申请成功后,在“高级功能”-“开发模式”-“接口配置信息”的最后两项就是;

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

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

 

分析创建菜单的难点

原来创建菜单这么简单,三步就能搞定?跟把大象放冰箱差不多。呵呵,当然没有这么简单,那我们一步步来看,到底难在哪里?

首先,第1步肯定都没有问题,只要成功申请了自定义菜单,一定能拿到AppId和AppSecret这两个值。

再来看第2步,由于是get方式获取access_token,很多人直接把拼好的url放在浏览器里执行,access_token就拿到了。抛开是不是用编程方式实现的来说,这真是个好办法,显然大家在第二步上也没有问题。

最后再看第3步,拼装json格式的菜单数据,虽然繁锁一点,但基本上也都没有什么问题的,因为官方给了个例子,照猫画虎就行了。那问题一定就出现在https post提交上了。

结论:不知道如何创建自定义菜单的朋友,大都可以归为以下三种情况:

1)根本不看或者没看懂公众平台API文档中关于“通用接口”、“自定义菜单接口”和“使用限制”部分的说明;

2)不知道如何发起HTTPS请求(平时的http请求,直接使用HttpUrlConnection就可以轻松搞定,但https请求要复杂一点);

3)不知道如何通过POST方式提交json格式的菜单数据。

正在看文章的你,不知道是属于哪一种,或者几种情况都有,不妨留言说出来,也可以做个调查。不管属于哪一种情况,既然看到了这篇文章,相信一定会让你弄明白的。

 

解读通用接口文档---凭证的获取

我们先来看通用接口文档的简介部分,如下图所示。

通俗点讲,这段简介可以这么理解:公众平台还有很多特殊的接口,像自定义菜单的创建、语音文件的获取、主动发送消息等,如果开发者想通过HTTP请求访问这些特殊接口,就必须要有访问凭证,也就是access_token。

那么,又该如何获取接口访问凭证access_token呢?让我们继续往下看。

图中已经表达的很清楚了,获取access_token是通过GET方式访问如下链接:

[java] view plain copy

  1. https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET  

链接中有三个参数,分别是grant_type、appid和secret。根据图中的参数说明,grant_type传固定值client_credential,而appid和secret就是申请完自定义菜单后微信分配给我们的。

请求发送成功后,微信服务器会返回一个json串,包含access_token和expires_in两个元素。其中,access_token就是我们最终需要的凭证,而expires_in是凭证的有效期,单位是秒,7200秒也就是2个小时。这就意味着,不是每次访问特殊接口,都需要重新获取一次access_token,只要access_token还在有效期内,就一直可以使用。

 

解读自定义菜单接口文档

还是一样,先来看看自定义菜单接口的简介部分,如下图所示。

从图中我们能够获取到以下信息:

1)拿到凭证access_token后,我们能对菜单执行三种操作:创建、查询和删除;

2)自定义菜单目前只支持click一种事件,即用户点击后回复某种类型的消息;不能够实现点击菜单项直接打开页面(type=view未开放,目前只是微生活有);

3)由于微信客户端缓存的原因,菜单创建后并不会立即在微信上显示出来,需要过24小时。在测试菜单创建时,可以通过取消关注后,再关注的方式达到立即看菜单的目的。

 

继续往下看,就是关于菜单怎么创建的介绍了,如下图所示。

其实就是向地址https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN以POST方式提交一个JSON格式的菜单字符串。

后面,关于参数说明的部分我就不一一贴图说明了,把重点说一下:

1)自定义菜单是一个3x5结构的,即菜单最多只能有二级,一级菜单最多只能有3个,每个一级菜单下最多可以有5个二级菜单项;

2)菜单项都有一个key值。当用户点击某个菜单项时,微信会将该菜单项的key值以事件推送的方式发送给我们的后台处理程序。

关于菜单的查询、创建我就不提了,这两个接口使用的频率非常小,一般都用不上。如果需要,再按照我上面提供的思路也不难理解。

 

解读API文档之使用限制

很多小伙伴看到这张图就开始疑惑了:怎么菜单还限制使用次数,用户量越来越大的时候,根本不够用啊。看清楚,这个限制是针对接口调用的,也就是针对开发者的,和用户数、使用次数半点关系也没有。

就先拿获取凭证接口来说吧,限制一天只能调用200次。还记得前面提到过access_token是有有效期的,并且有效期为两小时,也就是获取一次access_token后的两小时内,都可以继续使用,那么理想情况一天24小时内,是不是只需要获取12次就够了?难道200次还不够用?

再来看下菜单创建接口限制一天只能调用100次。我就这么解释吧,菜单创建一次后,只要你不切换模式(指的是在编辑模式和开发模式间切换)、不调用删除接口,这个菜单会永远存在的。谁没事干,一天要创建100次菜单,就算是测试,测个10次8次足够了吧?

菜单的查询和删除接口的限制我就不解释了,至今为止这二个接口我都没使用过一次。就算有这样的使用需求,一天这么多次的调用,完全足够了。

 

封装通用的请求方法

读到这里,就默认大家已经掌握了上面讲到的所有关于自定义菜单的理论知识,下面就进入代码实战讲解的部分。

先前我们了解到,创建菜单需要调用二个接口,并且都是https请求,而非http。如果要封装一个通用的请求方法,该方法至少需要具备以下能力:

1)支持HTTPS请求;

2)支持GET、POST两种方式;

3)支持参数提交,也支持无参数的情况;

对于https请求,我们需要一个证书信任管理器,这个管理器类需要自己定义,但需要实现X509TrustManager接口,代码如下:

[java] view plain copy

  1. package org.liufeng.weixin.util;  
  2.   
  3. import java.security.cert.CertificateException;  
  4. import java.security.cert.X509Certificate;  
  5.   
  6. import javax.net.ssl.X509TrustManager;  
  7.   
  8. /** 
  9.  * 证书信任管理器(用于https请求) 
  10.  *  
  11.  * @author liufeng 
  12.  * @date 2013-08-08 
  13.  */  
  14. public class MyX509TrustManager implements X509TrustManager {  
  15.   
  16.     public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {  
  17.     }  
  18.   
  19.     public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {  
  20.     }  
  21.   
  22.     public X509Certificate[] getAcceptedIssuers() {  
  23.         return null;  
  24.     }  
  25. }  

这个证书管理器的作用就是让它信任我们指定的证书,上面的代码意味着信任所有证书,不管是否权威机构颁发。

证书有了,通用的https请求方法就不难实现了,实现代码如下:

[java] view plain copy

  1. package org.liufeng.weixin.util;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.InputStream;  
  5. import java.io.InputStreamReader;  
  6. import java.io.OutputStream;  
  7. import java.net.ConnectException;  
  8. import java.net.URL;  
  9.   
  10. import javax.net.ssl.HttpsURLConnection;  
  11. import javax.net.ssl.SSLContext;  
  12. import javax.net.ssl.SSLSocketFactory;  
  13. import javax.net.ssl.TrustManager;  
  14.   
  15. import net.sf.json.JSONObject;  
  16.   
  17. import org.slf4j.Logger;  
  18. import org.slf4j.LoggerFactory;  
  19.   
  20. /** 
  21.  * 公众平台通用接口工具类 
  22.  *  
  23.  * @author liuyq 
  24.  * @date 2013-08-09 
  25.  */  
  26. public class WeixinUtil {  
  27.     private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);  
  28.   
  29.     /** 
  30.      * 发起https请求并获取结果 
  31.      *  
  32.      * @param requestUrl 请求地址 
  33.      * @param requestMethod 请求方式(GET、POST) 
  34.      * @param outputStr 提交的数据 
  35.      * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) 
  36.      */  
  37.     public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {  
  38.         JSONObject jsonObject = null;  
  39.         StringBuffer buffer = new StringBuffer();  
  40.         try {  
  41.             // 创建SSLContext对象,并使用我们指定的信任管理器初始化  
  42.             TrustManager[] tm = { new MyX509TrustManager() };  
  43.             SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");  
  44.             sslContext.init(null, tm, new java.security.SecureRandom());  
  45.             // 从上述SSLContext对象中得到SSLSocketFactory对象  
  46.             SSLSocketFactory ssf = sslContext.getSocketFactory();  
  47.   
  48.             URL url = new URL(requestUrl);  
  49.             HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();  
  50.             httpUrlConn.setSSLSocketFactory(ssf);  
  51.   
  52.             httpUrlConn.setDoOutput(true);  
  53.             httpUrlConn.setDoInput(true);  
  54.             httpUrlConn.setUseCaches(false);  
  55.             // 设置请求方式(GET/POST)  
  56.             httpUrlConn.setRequestMethod(requestMethod);  
  57.   
  58.             if ("GET".equalsIgnoreCase(requestMethod))  
  59.                 httpUrlConn.connect();  
  60.   
  61.             // 当有数据需要提交时  
  62.             if (null != outputStr) {  
  63.                 OutputStream outputStream = httpUrlConn.getOutputStream();  
  64.                 // 注意编码格式,防止中文乱码  
  65.                 outputStream.write(outputStr.getBytes("UTF-8"));  
  66.                 outputStream.close();  
  67.             }  
  68.   
  69.             // 将返回的输入流转换成字符串  
  70.             InputStream inputStream = httpUrlConn.getInputStream();  
  71.             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");  
  72.             BufferedReader bufferedReader = new BufferedReader(inputStreamReader);  
  73.   
  74.             String str = null;  
  75.             while ((str = bufferedReader.readLine()) != null) {  
  76.                 buffer.append(str);  
  77.             }  
  78.             bufferedReader.close();  
  79.             inputStreamReader.close();  
  80.             // 释放资源  
  81.             inputStream.close();  
  82.             inputStream = null;  
  83.             httpUrlConn.disconnect();  
  84.             jsonObject = JSONObject.fromObject(buffer.toString());  
  85.         } catch (ConnectException ce) {  
  86.             log.error("Weixin server connection timed out.");  
  87.         } catch (Exception e) {  
  88.             log.error("https request error:{}", e);  
  89.         }  
  90.         return jsonObject;  
  91.     }  
  92. }  

 代码说明:

1)41~50行:解决https请求的问题,很多人问题就出在这里;

2)55~59行:兼容GET、POST两种方式;

3)61~67行:兼容有数据提交、无数据提交两种情况,也有相当一部分人不知道如何POST提交数据;

 

Pojo类的封装

在获取凭证创建菜单前,我们还需要封装一些pojo,这会让我们的代码更美观,有条理。

首先是调用获取凭证接口后,微信服务器会返回json格式的数据:{"access_token":"ACCESS_TOKEN","expires_in":7200},我们将其封装为一个AccessToken对象,对象有二个属性:token和expiresIn,代码如下:

[java] view plain copy

  1. package org.liufeng.weixin.pojo;  
  2.   
  3. /** 
  4.  * 微信通用接口凭证 
  5.  *  
  6.  * @author liufeng 
  7.  * @date 2013-08-08 
  8.  */  
  9. public class AccessToken {  
  10.     // 获取到的凭证  
  11.     private String token;  
  12.     // 凭证有效时间,单位:秒  
  13.     private int expiresIn;  
  14.   
  15.     public String getToken() {  
  16.         return token;  
  17.     }  
  18.   
  19.     public void setToken(String token) {  
  20.         this.token = token;  
  21.     }  
  22.   
  23.     public int getExpiresIn() {  
  24.         return expiresIn;  
  25.     }  
  26.   
  27.     public void setExpiresIn(int expiresIn) {  
  28.         this.expiresIn = expiresIn;  
  29.     }  
  30. }  

接下来是对菜单结构的封装。因为我们是采用面向对象的编程方式,最终提交的json格式菜单数据就应该是由对象直接转换得到,而不是在程序代码中拼一大堆json数据。菜单结构封装的依据是公众平台API文档中给出的那一段json格式的菜单结构,如下所示:

[java] view plain copy

  1. {  
  2.     "button":[  
  3.     {     
  4.          "type":"click",  
  5.          "name":"今日歌曲",  
  6.          "key":"V1001_TODAY_MUSIC"  
  7.      },  
  8.      {  
  9.           "type":"click",  
  10.           "name":"歌手简介",  
  11.           "key":"V1001_TODAY_SINGER"  
  12.      },  
  13.      {  
  14.           "name":"菜单",  
  15.           "sub_button":[  
  16.            {  
  17.               "type":"click",  
  18.               "name":"hello word",  
  19.               "key":"V1001_HELLO_WORLD"  
  20.            },  
  21.            {  
  22.               "type":"click",  
  23.               "name":"赞一下我们",  
  24.               "key":"V1001_GOOD"  
  25.            }]  
  26.       }]  
  27. }  

首先是菜单项的基类,所有一级菜单、二级菜单都共有一个相同的属性,那就是name。菜单项基类的封装代码如下:

[java] view plain copy

  1. package org.liufeng.weixin.pojo;  
  2.   
  3. /** 
  4.  * 按钮的基类 
  5.  *  
  6.  * @author liufeng 
  7.  * @date 2013-08-08 
  8.  */  
  9. public class Button {  
  10.     private String name;  
  11.   
  12.     public String getName() {  
  13.         return name;  
  14.     }  
  15.   
  16.     public void setName(String name) {  
  17.         this.name = name;  
  18.     }  
  19. }  

接着是子菜单项的封装。这里对子菜单是这样定义的:没有子菜单的菜单项,有可能是二级菜单项,也有可能是不含二级菜单的一级菜单。这类子菜单项一定会包含三个属性:type、name和key,封装的代码如下:

[java] view plain copy

  1. package org.liufeng.weixin.pojo;  
  2.   
  3. /** 
  4.  * 普通按钮(子按钮) 
  5.  *  
  6.  * @author liufeng 
  7.  * @date 2013-08-08 
  8.  */  
  9. public class CommonButton extends Button {  
  10.     private String type;  
  11.     private String key;  
  12.   
  13.     public String getType() {  
  14.         return type;  
  15.     }  
  16.   
  17.     public void setType(String type) {  
  18.         this.type = type;  
  19.     }  
  20.   
  21.     public String getKey() {  
  22.         return key;  
  23.     }  
  24.   
  25.     public void setKey(String key) {  
  26.         this.key = key;  
  27.     }  
  28. }  

再往下是父菜单项的封装。对父菜单项的定义:包含有二级菜单项的一级菜单。这类菜单项包含有二个属性:name和sub_button,而sub_button以是一个子菜单项数组。父菜单项的封装代码如下:

[java] view plain copy

  1. package org.liufeng.weixin.pojo;  
  2.   
  3. /** 
  4.  * 复杂按钮(父按钮) 
  5.  *  
  6.  * @author liufeng 
  7.  * @date 2013-08-08 
  8.  */  
  9. public class ComplexButton extends Button {  
  10.     private Button[] sub_button;  
  11.   
  12.     public Button[] getSub_button() {  
  13.         return sub_button;  
  14.     }  
  15.   
  16.     public void setSub_button(Button[] sub_button) {  
  17.         this.sub_button = sub_button;  
  18.     }  
  19. }  

最后是整个菜单对象的封装,菜单对象包含多个菜单项(最多只能有3个),这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项),如果能明白上面所讲的,再来看封装后的代码就很容易理解了:

[java] view plain copy

  1. package org.liufeng.weixin.pojo;  
  2.   
  3. /** 
  4.  * 菜单 
  5.  *  
  6.  * @author liufeng 
  7.  * @date 2013-08-08 
  8.  */  
  9. public class Menu {  
  10.     private Button[] button;  
  11.   
  12.     public Button[] getButton() {  
  13.         return button;  
  14.     }  
  15.   
  16.     public void setButton(Button[] button) {  
  17.         this.button = button;  
  18.     }  
  19. }  

关于POJO类的封装就介绍完了。

 

凭证access_token的获取方法

继续在先前通用请求方法的类WeixinUtil.Java中加入以下代码,用于获取接口访问凭证:

[java] view plain copy

  1. // 获取access_token的接口地址(GET) 限200(次/天)  
  2. public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";  
  3.   
  4. /** 
  5.  * 获取access_token 
  6.  *  
  7.  * @param appid 凭证 
  8.  * @param appsecret 密钥 
  9.  * @return 
  10.  */  
  11. public static AccessToken getAccessToken(String appid, String appsecret) {  
  12.     AccessToken accessToken = null;  
  13.   
  14.     String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);  
  15.     JSONObject jsonObject = httpRequest(requestUrl, "GET", null);  
  16.     // 如果请求成功  
  17.     if (null != jsonObject) {  
  18.         try {  
  19.             accessToken = new AccessToken();  
  20.             accessToken.setToken(jsonObject.getString("access_token"));  
  21.             accessToken.setExpiresIn(jsonObject.getInt("expires_in"));  
  22.         } catch (JSONException e) {  
  23.             accessToken = null;  
  24.             // 获取token失败  
  25.             log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));  
  26.         }  
  27.     }  
  28.     return accessToken;  
  29. }  


自定义菜单的创建方法

继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于创建自定义菜单:

[java] view plain copy

  1. // 菜单创建(POST) 限100(次/天)  
  2. public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";  
  3.   
  4. /** 
  5.  * 创建菜单 
  6.  *  
  7.  * @param menu 菜单实例 
  8.  * @param accessToken 有效的access_token 
  9.  * @return 0表示成功,其他值表示失败 
  10.  */  
  11. public static int createMenu(Menu menu, String accessToken) {  
  12.     int result = 0;  
  13.   
  14.     // 拼装创建菜单的url  
  15.     String url = menu_create_url.replace("ACCESS_TOKEN", accessToken);  
  16.     // 将菜单对象转换成json字符串  
  17.     String jsonMenu = JSONObject.fromObject(menu).toString();  
  18.     // 调用接口创建菜单  
  19.     JSONObject jsonObject = httpRequest(url, "POST", jsonMenu);  
  20.   
  21.     if (null != jsonObject) {  
  22.         if (0 != jsonObject.getInt("errcode")) {  
  23.             result = jsonObject.getInt("errcode");  
  24.             log.error("创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));  
  25.         }  
  26.     }  
  27.   
  28.     return result;  
  29. }  

 

调用封装的方法创建自定义菜单

[java] view plain copy

  1. package org.liufeng.weixin.main;  
  2.   
  3. import org.liufeng.weixin.pojo.AccessToken;  
  4. import org.liufeng.weixin.pojo.Button;  
  5. import org.liufeng.weixin.pojo.CommonButton;  
  6. import org.liufeng.weixin.pojo.ComplexButton;  
  7. import org.liufeng.weixin.pojo.Menu;  
  8. import org.liufeng.weixin.util.WeixinUtil;  
  9. import org.slf4j.Logger;  
  10. import org.slf4j.LoggerFactory;  
  11.   
  12. /** 
  13.  * 菜单管理器类 
  14.  *  
  15.  * @author liufeng 
  16.  * @date 2013-08-08 
  17.  */  
  18. public class MenuManager {  
  19.     private static Logger log = LoggerFactory.getLogger(MenuManager.class);  
  20.   
  21.     public static void main(String[] args) {  
  22.         // 第三方用户唯一凭证  
  23.         String appId = "000000000000000000";  
  24.         // 第三方用户唯一凭证密钥  
  25.         String appSecret = "00000000000000000000000000000000";  
  26.   
  27.         // 调用接口获取access_token  
  28.         AccessToken at = WeixinUtil.getAccessToken(appId, appSecret);  
  29.   
  30.         if (null != at) {  
  31.             // 调用接口创建菜单  
  32.             int result = WeixinUtil.createMenu(getMenu(), at.getToken());  
  33.   
  34.             // 判断菜单创建结果  
  35.             if (0 == result)  
  36.                 log.info("菜单创建成功!");  
  37.             else  
  38.                 log.info("菜单创建失败,错误码:" + result);  
  39.         }  
  40.     }  
  41.   
  42.     /** 
  43.      * 组装菜单数据 
  44.      *  
  45.      * @return 
  46.      */  
  47.     private static Menu getMenu() {  
  48.         CommonButton btn11 = new CommonButton();  
  49.         btn11.setName("天气预报");  
  50.         btn11.setType("click");  
  51.         btn11.setKey("11");  
  52.   
  53.         CommonButton btn12 = new CommonButton();  
  54.         btn12.setName("公交查询");  
  55.         btn12.setType("click");  
  56.         btn12.setKey("12");  
  57.   
  58.         CommonButton btn13 = new CommonButton();  
  59.         btn13.setName("周边搜索");  
  60.         btn13.setType("click");  
  61.         btn13.setKey("13");  
  62.   
  63.         CommonButton btn14 = new CommonButton();  
  64.         btn14.setName("历史上的今天");  
  65.         btn14.setType("click");  
  66.         btn14.setKey("14");  
  67.   
  68.         CommonButton btn21 = new CommonButton();  
  69.         btn21.setName("歌曲点播");  
  70.         btn21.setType("click");  
  71.         btn21.setKey("21");  
  72.   
  73.         CommonButton btn22 = new CommonButton();  
  74.         btn22.setName("经典游戏");  
  75.         btn22.setType("click");  
  76.         btn22.setKey("22");  
  77.   
  78.         CommonButton btn23 = new CommonButton();  
  79.         btn23.setName("美女电台");  
  80.         btn23.setType("click");  
  81.         btn23.setKey("23");  
  82.   
  83.         CommonButton btn24 = new CommonButton();  
  84.         btn24.setName("人脸识别");  
  85.         btn24.setType("click");  
  86.         btn24.setKey("24");  
  87.   
  88.         CommonButton btn25 = new CommonButton();  
  89.         btn25.setName("聊天唠嗑");  
  90.         btn25.setType("click");  
  91.         btn25.setKey("25");  
  92.   
  93.         CommonButton btn31 = new CommonButton();  
  94.         btn31.setName("Q友圈");  
  95.         btn31.setType("click");  
  96.         btn31.setKey("31");  
  97.   
  98.         CommonButton btn32 = new CommonButton();  
  99.         btn32.setName("电影排行榜");  
  100.         btn32.setType("click");  
  101.         btn32.setKey("32");  
  102.   
  103.         CommonButton btn33 = new CommonButton();  
  104.         btn33.setName("幽默笑话");  
  105.         btn33.setType("click");  
  106.         btn33.setKey("33");  
  107.   
  108.         ComplexButton mainBtn1 = new ComplexButton();  
  109.         mainBtn1.setName("生活助手");  
  110.         mainBtn1.setSub_button(new CommonButton[] { btn11, btn12, btn13, btn14 });  
  111.   
  112.         ComplexButton mainBtn2 = new ComplexButton();  
  113.         mainBtn2.setName("休闲驿站");  
  114.         mainBtn2.setSub_button(new CommonButton[] { btn21, btn22, btn23, btn24, btn25 });  
  115.   
  116.         ComplexButton mainBtn3 = new ComplexButton();  
  117.         mainBtn3.setName("更多体验");  
  118.         mainBtn3.setSub_button(new CommonButton[] { btn31, btn32, btn33 });  
  119.   
  120.         /** 
  121.          * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项<br> 
  122.          *  
  123.          * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢?<br> 
  124.          * 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义:<br> 
  125.          * menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 }); 
  126.          */  
  127.         Menu menu = new Menu();  
  128.         menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 });  
  129.   
  130.         return menu;  
  131.     }  
  132. }  

注意:在运行以上代码时,需要将appId和appSecret换成你自己公众号的。

 

整个工程的结构

为了保证文章的完整独立性和可读性,我是新建了一个Java Project(Java web工程也可以,没有太大关系),没有在前几篇文章所讲到的weixinCourse工程中添加代码。如果需要,读者可以自己实现将菜单创建的代码移到自己已有的工程中去。

图中所有Java文件的源代码都在文章中贴出并进行了说明,图中使用到的jar也是Java开发中通用的jar包,很容易在网上下载到。

工程中引入的jar包主要分为两类:

1)第一类是json开发工具包,用于Java对象和Json字符串之间的转换;json开发工具包一共有3个jar:ezmorph-1.0.6.jar,json-lib-2.2.3-jdk13.jar和morph-1.1.1.jar。

2)第二类是slf4j日志工具包,用于记录系统运行所产生的日志,日志可以输出到控制台或文件中。

整个工程中,唯一没有讲到的是src下的log4j.properties的配置,也把它贴出来,方便大家参考,这样才是一个完整的工程源码。log4j.properties文件的内容如下:

[java] view plain copy

  1. log4j.rootLogger=info,console,file  
  2.   
  3. log4j.appender.console=org.apache.log4j.ConsoleAppender  
  4. log4j.appender.console.layout=org.apache.log4j.PatternLayout  
  5. log4j.appender.console.layout.ConversionPattern=[%-5p] %m%n  
  6.   
  7. log4j.appender.file=org.apache.log4j.DailyRollingFileAppender  
  8. log4j.appender.file.DatePattern='-'yyyy-MM-dd  
  9. log4j.appender.file.File=./logs/weixinmpmenu.log  
  10. log4j.appender.file.Append=true  
  11. log4j.appender.file.layout=org.apache.log4j.PatternLayout  
  12. log4j.appender.file.layout.ConversionPattern=[%-5p] %d %37c %3x - %m%n  

 

如何响应菜单点击事件
自定义菜单的创建工作已经完成,那么该如何接收和响应菜单的点击事件呢,也就是说在公众帐号后台处理程序中,如何识别用户点击的是哪个菜单,以及做出响应。这部分内容其实在教程的第5篇各种消息的接收与响应中已经讲解清楚了。

来看一下第一篇教程weixinCourse项目中的CoreService类要怎么改写,才能接收响应菜单点击事件,该类修改后的完整代码如下:

[java] view plain copy

  1. package org.liufeng.course.service;  
  2.   
  3. import java.util.Date;  
  4. import java.util.Map;  
  5.   
  6. import javax.servlet.http.HttpServletRequest;  
  7.   
  8. import org.liufeng.course.message.resp.TextMessage;  
  9. import org.liufeng.course.util.MessageUtil;  
  10.   
  11. /** 
  12.  * 核心服务类 
  13.  *  
  14.  * @author liufeng 
  15.  * @date 2013-05-20 
  16.  */  
  17. public class CoreService {  
  18.     /** 
  19.      * 处理微信发来的请求 
  20.      *  
  21.      * @param request 
  22.      * @return 
  23.      */  
  24.     public static String processRequest(HttpServletRequest request) {  
  25.         String respMessage = null;  
  26.         try {  
  27.             // 默认返回的文本消息内容  
  28.             String respContent = "请求处理异常,请稍候尝试!";  
  29.   
  30.             // xml请求解析  
  31.             Map<String, String> requestMap = MessageUtil.parseXml(request);  
  32.   
  33.             // 发送方帐号(open_id)  
  34.             String fromUserName = requestMap.get("FromUserName");  
  35.             // 公众帐号  
  36.             String toUserName = requestMap.get("ToUserName");  
  37.             // 消息类型  
  38.             String msgType = requestMap.get("MsgType");  
  39.   
  40.             // 回复文本消息  
  41.             TextMessage textMessage = new TextMessage();  
  42.             textMessage.setToUserName(fromUserName);  
  43.             textMessage.setFromUserName(toUserName);  
  44.             textMessage.setCreateTime(new Date().getTime());  
  45.             textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);  
  46.             textMessage.setFuncFlag(0);  
  47.   
  48.             // 文本消息  
  49.             if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {  
  50.                 respContent = "您发送的是文本消息!";  
  51.             }  
  52.             // 图片消息  
  53.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {  
  54.                 respContent = "您发送的是图片消息!";  
  55.             }  
  56.             // 地理位置消息  
  57.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {  
  58.                 respContent = "您发送的是地理位置消息!";  
  59.             }  
  60.             // 链接消息  
  61.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {  
  62.                 respContent = "您发送的是链接消息!";  
  63.             }  
  64.             // 音频消息  
  65.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {  
  66.                 respContent = "您发送的是音频消息!";  
  67.             }  
  68.             // 事件推送  
  69.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {  
  70.                 // 事件类型  
  71.                 String eventType = requestMap.get("Event");  
  72.                 // 订阅  
  73.                 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {  
  74.                     respContent = "谢谢您的关注!";  
  75.                 }  
  76.                 // 取消订阅  
  77.                 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {  
  78.                     // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息  
  79.                 }  
  80.                 // 自定义菜单点击事件  
  81.                 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {  
  82.                     // 事件KEY值,与创建自定义菜单时指定的KEY值对应  
  83.                     String eventKey = requestMap.get("EventKey");  
  84.   
  85.                     if (eventKey.equals("11")) {  
  86.                         respContent = "天气预报菜单项被点击!";  
  87.                     } else if (eventKey.equals("12")) {  
  88.                         respContent = "公交查询菜单项被点击!";  
  89.                     } else if (eventKey.equals("13")) {  
  90.                         respContent = "周边搜索菜单项被点击!";  
  91.                     } else if (eventKey.equals("14")) {  
  92.                         respContent = "历史上的今天菜单项被点击!";  
  93.                     } else if (eventKey.equals("21")) {  
  94.                         respContent = "歌曲点播菜单项被点击!";  
  95.                     } else if (eventKey.equals("22")) {  
  96.                         respContent = "经典游戏菜单项被点击!";  
  97.                     } else if (eventKey.equals("23")) {  
  98.                         respContent = "美女电台菜单项被点击!";  
  99.                     } else if (eventKey.equals("24")) {  
  100.                         respContent = "人脸识别菜单项被点击!";  
  101.                     } else if (eventKey.equals("25")) {  
  102.                         respContent = "聊天唠嗑菜单项被点击!";  
  103.                     } else if (eventKey.equals("31")) {  
  104.                         respContent = "Q友圈菜单项被点击!";  
  105.                     } else if (eventKey.equals("32")) {  
  106.                         respContent = "电影排行榜菜单项被点击!";  
  107.                     } else if (eventKey.equals("33")) {  
  108.                         respContent = "幽默笑话菜单项被点击!";  
  109.                     }  
  110.                 }  
  111.             }  
  112.   
  113.             textMessage.setContent(respContent);  
  114.             respMessage = MessageUtil.textMessageToXml(textMessage);  
  115.         } catch (Exception e) {  
  116.             e.printStackTrace();  
  117.         }  
  118.   
  119.         return respMessage;  
  120.     }  
  121. }  

代码说明:

1)第69行、第81行这两行代码说明了如何判断菜单的点击事件。当消息类型MsgType=event,并且Event=CLICK时,就表示是自定义菜单点击事件;

2)第83行是判断具体点击的是哪个菜单项,根据菜单的key值来判断;

3)第85~109行表示当用户点击某个菜单项后,具体返回什么消息,我只是做个简单示例,统一返回文本消息,读者可以根据实际需要来灵活处理。

 

总结

到这里关于自定义菜单的创建、菜单事件的判断和处理响应就全部介绍完了。我只希望看过文章的人不要只是拷贝代码,如果是这样,我完全不用花这么多的时间来写这篇文章,直接把工程放在下载区多简单。另外,网上是有很多工具,让你填入appid,appsecret和菜单结构,提交就能创建菜单,请慎用!因为appid和appsecret一旦告诉别人,你的公众号的菜单控制权就在别人手上了,总会有别有用心的人出来搞点事的。

2014-07-07 16:33:00 chuanwumi7056 阅读数 8

微信支持开发者在开发模式下通过post数据的方式自定义菜单,鉴于其菜单格式复杂,暂未定义其菜单实体类,具体数据格式请参看微信开发者文档。微信提供的功能有“自定义菜单创建接口”,“自定义菜单查询接口”,“自定义菜单删除接口”,本次仅实现“自定义菜单创建接口”。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

/// <summary>
/// WxMenuHelper 的摘要说明
/// </summary>
public class WxMenuHelper
{
    private static String menuUrl = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
    public static WxErrorEntity UpdateWxMenu(String ACCESS_TOKEN, String menuData)
    {
        String finalUrl = menuUrl.Replace("ACCESS_TOKEN", ACCESS_TOKEN);
        String result = RequestHelper.sendPost(finalUrl, menuData);
        if (result == null || result == "")
        {
            throw new Exception("微信无返回数据");
        } 
        try
        {
            WxErrorEntity error = (WxErrorEntity)JsonHelper.FromJsonToModel(result, typeof(WxErrorEntity));
            if (error == null)
            {
                throw new Exception("数据解析异常,原因未知");
            }
            return error;
        }
        catch (Exception ex)
        {
            throw new Exception("数据解析异常," + ex.Message);
        }
    }
}

(1)参数ACCESS_TOKEN为前文中获取的access_token,请注意其过期问题及获取次数限制问题。

(2)参数menuData为实际发送的json数据。

(3)其结果类WxErrorEntity 在前文中有定义,此处不再详解。

哪位有好的微信菜单的抽象类定义欢迎分享下,邮箱:lufaxin_t@163.com.


转载于:https://my.oschina.net/u/1867097/blog/287992

2015-07-17 12:08:00 weixin_33713503 阅读数 35

简介

开发者获取使用凭证(如何获取凭证)后,可以使用该凭证对公众账号的自定义菜单进行创建、查询和删除等操作。 自定义菜单接口可实现以下类型按钮:

click(点击事件):

用户点击click类型按钮后,微信服务器会通过消息接口(event类型)推送点击事件给开发者,并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值进行消息回复

创建自定义菜单后,由于微信客户端缓存,需要24小时微信客户端才会展现出来。建议测试时可以尝试取消关注公众账号后,再次关注,则可以看到创建后的效果。

菜单创建

接口说明

通过POST一个特定结构体,实现在微信客户端创建自定义菜单。

请求说明

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

请求示例

{
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"type":"click",
"name":"歌手简介",
"key":"V1001_TODAY_SINGER"
},
{
"name":"菜单",
"sub_button":[
{
"type":"click",
"name":"hello word",
"key":"V1001_HELLO_WORLD"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}]
}]
}

创建后效果:

创建后效果

参数说明

参数是否必须说明
button 按钮数组,按钮个数应为1~3个
sub_button 子按钮数组,按钮个数应为1~5个
type 按钮类型,目前有click类型
name 按钮描述,既按钮名字,不超过16个字节,子菜单不超过40个字节
key 类型为click必须 按钮KEY值,用于消息接口(event类型)推送,不超过128字节

返回说明

正确的Json返回结果:

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

错误的Json返回结果

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

统一返回码说明

菜单查询

接口说明

查询当前使用的自定义菜单结构。

请求说明

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

返回说明

对应创建接口,正确的Json返回结果:  {"menu":{"button":[{"type":"click","name":"今日歌曲","key":"V1001_TODAY_MUSIC","sub_button":[]},{"type":"click","name":"歌手简介","key":"V1001_TODAY_SINGER","sub_button":[]},{"name":"菜单","sub_button":[{"type":"click","name":"hello word","key":"V1001_HELLO_WORLD","sub_button":[]},{"type":"click","name":"赞一下我们","key":"V1001_GOOD","sub_button":[]}]}]}}  

统一返回码说明

菜单删除

接口说明

取消当前使用的自定义菜单。

请求说明

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

返回说明

对应创建接口,正确的Json返回结果:  {"errcode":0,"errmsg":"ok"}  

统一返回码说明

请先确保公众账号已经拥有接口调用权限。

默认每个公众帐号都不能超过下面的频率限制。 当超出调用接口频率限制,调用对应接口将会收到如下错误信息:

{"errcode":45009,"errmsg":"api freq out of limit"}  

接口调用频率限制

接口名称频率限制
获取凭证接口 200(次/天)
自定义菜单创建接口 100(次/天)
自定义菜单查询接口 1000(次/天)
自定义菜单删除接口 100(次/天)

 

返回码说明

返回码说明
-1 系统繁忙
0 请求成功
40001 验证失败
40002 不合法的凭证类型
40003 不合法的OpenID
40004 不合法的媒体文件类型
40005 不合法的文件类型
40006 不合法的文件大小
40007 不合法的媒体文件id
40008 不合法的消息类型
40009 不合法的图片文件大小
40010 不合法的语音文件大小
40011 不合法的视频文件大小
40012 不合法的缩略图文件大小
40013 不合法的APPID
40014 不合法的access_token
40014 不合法的access_token
40015 不合法的菜单类型
40016 不合法的按钮个数
40017 不合法的按钮个数
40018 不合法的按钮名字长度
40019 不合法的按钮KEY长度
40020 不合法的按钮URL长度
40021 不合法的菜单版本号
40022 不合法的子菜单级数
40023 不合法的子菜单按钮个数
40024 不合法的子菜单按钮类型
40025 不合法的子菜单按钮名字长度
40026 不合法的子菜单按钮KEY长度
40027 不合法的子菜单按钮URL长度
40028 不合法的自定义菜单使用用户
41001 缺少access_token参数
41002 缺少appid参数
41003 缺少refresh_token参数
41004 缺少secret参数
41005 缺少多媒体文件数据
41006 缺少media_id参数
41007 缺少子菜单数据
42001 access_token超时
43001 需要GET请求
43002 需要POST请求
43003 需要HTTPS请求
44001 多媒体文件为空
44002 POST的数据包为空
44003 图文消息内容为空
45001 多媒体文件大小超过限制
45002 消息内容超过限制
45003 标题字段超过限制
45004 描述字段超过限制
45005 链接字段超过限制
45006 图片链接字段超过限制
45007 语音播放时间超过限制
45008 图文消息超过限制
45009 接口调用超过限制
45010 创建菜单个数超过限制
46001 不存在媒体数据
46002 不存在的菜单版本
46003 不存在的菜单数据
47001 解析JSON/XML内容错误

转载于:https://www.cnblogs.com/lihuanliu/p/4654077.html