微信公众平台开发(4)-自定义菜单
作者:luckystar
日期:
参考了《微信公众账号开发教程(java).doc》
文档:http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
其实就是以POST方式调用https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN,将菜单数据以微信公众平台定义的格式写到输出流中。
链接中的access_token从哪儿来?
参考http://mp.weixin.qq.com/wiki/index.php?title=获取access_token
不论是获取access_token,还是创建菜单,都需要使用https方式调用提供的URL。
将获取access_token,创建菜单等动作封装到一个工具类中。
如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
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.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.company.project.model.menu.AccessToken;
import com.company.project.model.menu.Menu;
public class WeixinUtil {
private static Logger log = Logger.getLogger(WeixinUtil.class);
public final static String APPID = "****************";
public final static String APP_SECRET = "************************";
// 获取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";
// 创建菜单
public final static String create_menu_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
// 存放:1.token,2:获取token的时间,3.过期时间
public final static Map<String,Object> accessTokenMap = new HashMap<String,Object>();
/**
* 发起https请求并获取结果
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject handleRequest(String requestUrl,String requestMethod,String outputStr) {
JSONObject jsonObject = null;
try {
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
SSLContext ctx = SSLContext.getInstance("SSL", "SunJSSE");
TrustManager[] tm = {new MyX509TrustManager()};
ctx.init(null, tm, new SecureRandom());
SSLSocketFactory sf = ctx.getSocketFactory();
conn.setSSLSocketFactory(sf);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setRequestMethod(requestMethod);
conn.setUseCaches(false);
if ("GET".equalsIgnoreCase(requestMethod)) {
conn.connect();
}
if (StringUtils.isNotEmpty(outputStr)) {
OutputStream out = conn.getOutputStream();
out.write(outputStr.getBytes("utf-8"));
out.close();
}
InputStream in = conn.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in,"utf-8"));
StringBuffer buffer = new StringBuffer();
String line = null;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
in.close();
conn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (MalformedURLException e) {
e.printStackTrace();
log.error("URL错误!");
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return jsonObject;
}
/**
* 获取access_token
*
* @author qincd
* @date Nov 6, 2014 9:56:43 AM
*/
public static AccessToken getAccessToken(String appid,String appSecret) {
AccessToken at = new AccessToken();
// 每次获取access_token时,先从accessTokenMap获取,如果过期了就重新从微信获取
// 没有过期直接返回
// 从微信获取的token的有效期为2个小时
if (!accessTokenMap.isEmpty()) {
Date getTokenTime = (Date) accessTokenMap.get("getTokenTime");
Calendar c = Calendar.getInstance();
c.setTime(getTokenTime);
c.add(Calendar.HOUR_OF_DAY, 2);
getTokenTime = c.getTime();
if (getTokenTime.after(new Date())) {
log.info("缓存中发现token未过期,直接从缓存中获取access_token");
// token未过期,直接从缓存获取返回
String token = (String) accessTokenMap.get("token");
Integer expire = (Integer) accessTokenMap.get("expire");
at.setToken(token);
at.setExpiresIn(expire);
return at;
}
}
String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appSecret);
JSONObject object = handleRequest(requestUrl, "GET", null);
String access_token = object.getString("access_token");
int expires_in = object.getInt("expires_in");
log.info("\naccess_token:" + access_token);
log.info("\nexpires_in:" + expires_in);
at.setToken(access_token);
at.setExpiresIn(expires_in);
// 每次获取access_token后,存入accessTokenMap
// 下次获取时,如果没有过期直接从accessTokenMap取。
accessTokenMap.put("getTokenTime", new Date());
accessTokenMap.put("token", access_token);
accessTokenMap.put("expire", expires_in);
return at;
}
/**
* 创建菜单
*
* @author qincd
* @date Nov 6, 2014 9:56:36 AM
*/
public static boolean createMenu(Menu menu,String accessToken) {
String requestUrl = create_menu_url.replace("ACCESS_TOKEN", accessToken);
String menuJsonString = JSONObject.fromObject(menu).toString();
JSONObject jsonObject = handleRequest(requestUrl, "POST", menuJsonString);
String errorCode = jsonObject.getString("errcode");
if (!"0".equals(errorCode)) {
log.error(String.format("菜单创建失败!errorCode:%d,errorMsg:%s",jsonObject.getInt("errcode"),jsonObject.getString("errmsg")));
return false;
}
log.info("菜单创建成功!");
return true;
}
}
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;
}
}
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class MyX509TrustManager implements X509TrustManager{
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
菜单包含一级菜单和二级菜单,菜单类型又包括点击菜单直接跳转到一个链接,或者是点击菜单触发一个事件,在事件处理中自定义逻辑。
不论是哪种菜单,都包含菜单名字,封装到Button类中。
对于点击后直接跳转到某个链接的菜单定义为ViewButton。
对于点击后触发一个事件的菜单定义为CommandButton
一级菜单可以包含二级菜单,定义为ComplexButton
对于整个菜单来讲,可以包含多个一级菜单,定义为Menu。
各个类的结构如下:
public class Button {
private String name;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
}
public class CommandButton extends Button{
private String type;
private String key;
/**
* @return the type
*/
public String getType() {
return type;
}
/**
* @param type the type to set
*/
public void setType(String type) {
this.type = type;
}
/**
* @return the key
*/
public String getKey() {
return key;
}
/**
* @param key the key to set
*/
public void setKey(String key) {
this.key = key;
}
}
public class ViewButton extends Button {
private String type;
private String url;
/**
* @return the type
*/
public String getType() {
return type;
}
/**
* @param type the type to set
*/
public void setType(String type) {
this.type = type;
}
/**
* @return the url
*/
public String getUrl() {
return url;
}
/**
* @param url the url to set
*/
public void setUrl(String url) {
this.url = url;
}
}
public class ComplexButton extends Button{
private Button[] sub_button;
/**
* @return the sub_button
*/
public Button[] getSub_button() {
return sub_button;
}
/**
* @param sub_button the sub_button to set
*/
public void setSub_button(Button[] sub_button) {
this.sub_button = sub_button;
}
}
public class Menu {
private Button[] button;
/**
* @return the button
*/
public Button[] getButton() {
return button;
}
/**
* @param button the button to set
*/
public void setButton(Button[] button) {
this.button = button;
}
}
菜单创建测试代码:
public class WeixinUtilTest {
/**
*
* @author qincd
* @date Nov 6, 2014 9:57:54 AM
*/
public static void main(String[] args) {
// 1).获取access_token
AccessToken accessToken = WeixinUtil.getAccessToken(WeixinUtil.APPID, WeixinUtil.APP_SECRET);
// 2).创建菜单
Menu menu = new Menu();
// 菜单1
ComplexButton cb0 = new ComplexButton();
cb0.setName("超值预定");
ViewButton cb01 = new ViewButton();
cb01.setName("团购订单");
cb01.setType("view");
cb01.setUrl("http://www.meituan.com");
ViewButton cb02 = new ViewButton();
cb02.setName("微信团购");
cb02.setType("view");
cb02.setUrl("http://www.weixin.com");
cb0.setSub_button(new ViewButton[]{cb01,cb02});
// 菜单2
ComplexButton cb1 = new ComplexButton();
cb1.setName("我的服务");
ViewButton cb11 = new ViewButton();
cb11.setName("办登机牌");
cb11.setType("view");
cb11.setUrl("http://www.meituan.com");
ViewButton cb12 = new ViewButton();
cb12.setName("航班动态");
cb12.setType("view");
cb12.setUrl("http://www.meituan.com");
ViewButton cb13 = new ViewButton();
cb13.setName("里程查询");
cb13.setType("view");
cb13.setUrl("http://www.meituan.com");
cb1.setSub_button(new ViewButton[]{cb11,cb12,cb13});
// 菜单3
ComplexButton cb2 = new ComplexButton();
cb2.setName("我的测试");
CommandButton cb21 = new CommandButton();
cb21.setName("回复文字");
cb21.setType("click");
cb21.setKey("reply_words");
CommandButton cb22 = new CommandButton();
cb22.setName("回复音乐");
cb22.setType("click");
cb22.setKey("reply_music");
CommandButton cb23 = new CommandButton();
cb23.setName("回复图文");
cb23.setType("click");
cb23.setKey("reply_news");
CommandButton cb24 = new CommandButton();
cb24.setName("回复链接");
cb24.setType("click");
cb24.setKey("reply_link");
cb2.setSub_button(new CommandButton[]{cb21,cb22,cb23,cb24});
menu.setButton(new ComplexButton[]{cb0,cb1,cb2});
String menuJsonString = JSONObject.fromObject(menu).toString();
System.out.println(menuJsonString);
WeixinUtil.createMenu(menu, accessToken.getToken());
}
}
上面的代码创建了3个一级菜单,每个一级菜单都包含有二级菜单。
前2个一级菜单都是点击菜单后直接跳转到一个连接,第3个一级菜单点击后触发click事件
在后来根据菜单的key来处理。
如下:
@Service
public class WeixinService {
public static Logger log = Logger.getLogger(WeixinService.class);
public String processRequest(HttpServletRequest req) {
// 解析微信传递的参数
String str = null;
try {
Map<String,String> xmlMap = MessageUtil.parseXml(req);
str = "请求处理异常,请稍后再试!";
String ToUserName = xmlMap.get("ToUserName");
String FromUserName = xmlMap.get("FromUserName");
String MsgType = xmlMap.get("MsgType");
if (MsgType.equals(MessageUtil.MESSAGG_TYPE_TEXT)) {
// 用户发送的文本消息
String content = xmlMap.get("Content");
log.info("用户:[" + FromUserName + "]发送的文本消息:" + content);
// 链接
if (content.contains("csdn")) {
TextMessage tm = new TextMessage();
tm.setToUserName(FromUserName);
tm.setFromUserName(ToUserName);
tm.setMsgType(MessageUtil.MESSAGG_TYPE_TEXT);
tm.setCreateTime(System.currentTimeMillis());
tm.setContent("我的CSDN博客:<a href=\"http://my.csdn.net/qincidong\">我的CSDN博客</a>\n");
return MessageUtil.textMessageToXml(tm);
}
if (content.contains("图文")) {
NewsMessage nm = new NewsMessage();
nm.setFromUserName(ToUserName);
nm.setToUserName(FromUserName);
nm.setCreateTime(System.currentTimeMillis());
nm.setMsgType(MessageUtil.MESSAGG_TYPE_NEWS);
List<Articles> articles = new ArrayList<Articles>();
Articles e1 = new Articles();
e1.setTitle("马云接受外媒专访:中国的五大银行想杀了“我”");
e1.setDescription("阿里巴巴集团上市大获成功,《华尔街日报》日前就阿里巴巴集团、支付宝等话题采访了马云,马云也谈到了与苹果Apple Pay建立电子支付联盟的可能性。本文摘编自《华尔街日报》,原文标题:马云谈阿里巴巴将如何帮助美国出口商,虎嗅略有删节。");
e1.setPicUrl("http://img1.gtimg.com/finance/pics/hv1/29/53/1739/113092019.jpg");
e1.setUrl("http://finance.qq.com/a/20141105/010616.htm?pgv_ref=aio2012&ptlang=2052");
Articles e2 = new Articles();
e2.setTitle("史上最牛登机牌:姓名竟是微博名 涉事航空公司公开致歉");
e2.setDescription("世上最遥远的距离是飞机在等你登机,你却过不了安检。");
e2.setPicUrl("http://p9.qhimg.com/dmfd/328_164_100/t011946ff676981792d.png");
e2.setUrl("http://www.techweb.com.cn/column/2014-11-05/2093128.shtml");
articles.add(e1);
articles.add(e2);
nm.setArticles(articles);
nm.setArticleCount(articles.size());
String newsXml = MessageUtil.NewsMessageToXml(nm);
log.info("\n"+newsXml);
return newsXml;
}
if (content.contains("音乐")) {
MusicMessage mm = new MusicMessage();
mm.setFromUserName(ToUserName);
mm.setToUserName(FromUserName);
mm.setMsgType(MessageUtil.MESSAGG_TYPE_MUSIC);
mm.setCreateTime(System.currentTimeMillis());
Music music = new Music();
music.setTitle("Maid with the Flaxen Hair");
music.setDescription("测试音乐");
music.setMusicUrl("http://yinyueshiting.baidu.com/data2/music/123297915/1201250291415073661128.mp3?xcode=e2edf18bbe9e452655284217cdb920a7a6a03c85c06f4409");
music.setHQMusicUrl("http://yinyueshiting.baidu.com/data2/music/123297915/1201250291415073661128.mp3?xcode=e2edf18bbe9e452655284217cdb920a7a6a03c85c06f4409");
mm.setMusic(music);
String musicXml = MessageUtil.MusicMessageToXml(mm);
log.info("musicXml:\n" + musicXml);
return musicXml;
}
// 响应
TextMessage tm = new TextMessage();
tm.setToUserName(FromUserName);
tm.setFromUserName(ToUserName);
tm.setMsgType(MessageUtil.MESSAGG_TYPE_TEXT);
tm.setCreateTime(System.currentTimeMillis());
tm.setContent("你好,你发送的内容是:\n" + content);
String xml = MessageUtil.textMessageToXml(tm);
log.info("xml:" + xml);
return xml;
}
else if (MsgType.equals(MessageUtil.MESSAGG_TYPE_EVENT)) {
String event = xmlMap.get("Event");
if (event.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {
// 订阅
TextMessage tm = new TextMessage();
tm.setToUserName(FromUserName);
tm.setFromUserName(ToUserName);
tm.setMsgType(MessageUtil.MESSAGG_TYPE_TEXT);
tm.setCreateTime(System.currentTimeMillis());
tm.setContent("你好,欢迎关注[程序员的生活]公众号![愉快]/呲牙/玫瑰\n目前可以回复文本消息");
return MessageUtil.textMessageToXml(tm);
}
else if (event.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {
// 取消订阅
log.info("用户【" + FromUserName + "]取消关注了。");
}
else if (event.equals(MessageUtil.EVENT_TYPE_CLICK)) {
String eventKey = xmlMap.get("EventKey");
if (eventKey.equals("reply_words")) { // 点击了回复文字菜单
TextMessage tm = new TextMessage();
tm.setToUserName(FromUserName);
tm.setFromUserName(ToUserName);
tm.setMsgType(MessageUtil.MESSAGG_TYPE_TEXT);
tm.setCreateTime(System.currentTimeMillis());
tm.setContent("你好,你点击了回复文本菜单:\n" );
String xml = MessageUtil.textMessageToXml(tm);
log.info("xml:" + xml);
return xml;
}
else if (eventKey.equals("reply_music")) { // 点击了回复音乐
MusicMessage mm = new MusicMessage();
mm.setFromUserName(ToUserName);
mm.setToUserName(FromUserName);
mm.setMsgType(MessageUtil.MESSAGG_TYPE_MUSIC);
mm.setCreateTime(System.currentTimeMillis());
Music music = new Music();
music.setTitle("Maid with the Flaxen Hair");
music.setDescription("测试音乐");
music.setMusicUrl("http://yinyueshiting.baidu.com/data2/music/123297915/1201250291415073661128.mp3?xcode=e2edf18bbe9e452655284217cdb920a7a6a03c85c06f4409");
music.setHQMusicUrl("http://yinyueshiting.baidu.com/data2/music/123297915/1201250291415073661128.mp3?xcode=e2edf18bbe9e452655284217cdb920a7a6a03c85c06f4409");
mm.setMusic(music);
String musicXml = MessageUtil.MusicMessageToXml(mm);
log.info("musicXml:\n" + musicXml);
return musicXml;
}
else if (eventKey.equals("reply_news")) { // 点击了回复图文
NewsMessage nm = new NewsMessage();
nm.setFromUserName(ToUserName);
nm.setToUserName(FromUserName);
nm.setCreateTime(System.currentTimeMillis());
nm.setMsgType(MessageUtil.MESSAGG_TYPE_NEWS);
List<Articles> articles = new ArrayList<Articles>();
Articles e1 = new Articles();
e1.setTitle("马云接受外媒专访:中国的五大银行想杀了“我”");
e1.setDescription("阿里巴巴集团上市大获成功,《华尔街日报》日前就阿里巴巴集团、支付宝等话题采访了马云,马云也谈到了与苹果Apple Pay建立电子支付联盟的可能性。本文摘编自《华尔街日报》,原文标题:马云谈阿里巴巴将如何帮助美国出口商,虎嗅略有删节。");
e1.setPicUrl("http://img1.gtimg.com/finance/pics/hv1/29/53/1739/113092019.jpg");
e1.setUrl("http://finance.qq.com/a/20141105/010616.htm?pgv_ref=aio2012&ptlang=2052");
Articles e2 = new Articles();
e2.setTitle("史上最牛登机牌:姓名竟是微博名 涉事航空公司公开致歉");
e2.setDescription("世上最遥远的距离是飞机在等你登机,你却过不了安检。");
e2.setPicUrl("http://p9.qhimg.com/dmfd/328_164_100/t011946ff676981792d.png");
e2.setUrl("http://www.techweb.com.cn/column/2014-11-05/2093128.shtml");
articles.add(e1);
articles.add(e2);
nm.setArticles(articles);
nm.setArticleCount(articles.size());
String newsXml = MessageUtil.NewsMessageToXml(nm);
log.info("\n"+newsXml);
return newsXml;
}
else if (eventKey.equals("reply_link")) {
TextMessage tm = new TextMessage();
tm.setToUserName(FromUserName);
tm.setFromUserName(ToUserName);
tm.setMsgType(MessageUtil.MESSAGG_TYPE_TEXT);
tm.setCreateTime(System.currentTimeMillis());
tm.setContent("我的CSDN博客:<a href=\"http://my.csdn.net/qincidong\">我的CSDN博客</a>\n");
return MessageUtil.textMessageToXml(tm);
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("处理微信请求时发生异常:");
}
return str;
}
}
作者:qincidong
出处:http://qincidong.github.io/blog/2014/11/26/weixin-develop-4th.html
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
出处:http://qincidong.github.io/blog/2014/11/26/weixin-develop-4th.html
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。