1. 测试号申请
不同的公众号类型具备不同的接口权限,一般个人申请的是未认证订阅号,没有自定义菜单的功能,只有企业组织科申请工作好认证公众号。不同类型的公众号的不同接口权限详情见:公众号接口权限说明。
我们可以申请一个接口测试号,这个测试号几乎开放所有的接口,可以供测试使用, 接口测试号申请。测试号如下图,可以用测试号的appID和appsecret进行接口调用。

2. 创建自定义菜单
2.1 获取access_token
获取access_token的接口为GET:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID
创建自定义菜单的接口为POST:https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESSTOKEN 其中需要传的body详见官网文档: 自定义菜单创建接口。
可以用postman进行接口调用。这里主要讲通过java程序进行创建。
获取access_token
TokenResponseDTO.java
1 2 3 4 5 6 7
| public class TokenResponseDTO { private Integer errcode; private String errmsg; private String access_token; private String expires_in; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private String getToken(String appId, String appSecret) { String queryTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"; String newQueryTokenUrl = MessageFormat.format(queryTokenUrl, appId,appSecret); try { ResponseEntity<TokenResponseDTO> response =restTemplate.getForEntity(newQueryTokenUrl, TokenResponseDTO.class); TokenResponseDTO tokenResponseDTO = null; if (response != null && response.getStatusCode() != null &&response.getStatusCode().value() == 200 && response.getBody() != null && response.getBody().getErrcode() == null &&response.getBody().getErrmsg() == null) { tokenResponseDTO = response.getBody(); LOGGER.info("get token from weixin, Reponse: " +JsonUtils.encode(response)); } else { throw new Exception("getToken error! " +response.getBody().getErrcode().toString() + ":" +response.getBody().getErrmsg()); } return tokenResponseDTO.getAccess_token(); } catch (Exception e) { LOGGER.error("getToken error!" + e.getMessage()); throw new RuntimeException(e.getMessage()); } }
|
因为获取接口的access_token每日的调用次数是有上限的,每次token的过期时间有2个小时,所以可以另外搭一个存access_token的服务器,定时更新access_token。
2.2 创建自定义菜单
组body的json数据,可以通过组对象,然后转换成json格式的字符串,也可以自己组好json字符串,写在程序里。我们这里采用组对象转换json串的方式,组三个三种类型的一级按钮。
MenuDTO.java
1 2 3 4
| public class MenuDTO { List<ButtonDTO> button; }
|
ButtonDTO.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class ButtonDTO { private String type; private String name; private String key; private String url; ButtonDTO(){} ButtonDTO(String type, String name, String key, String url){ this.type = type; this.name = name; this.key = key; this.url = url; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public void createMenu(String appId, String appSecret) { int result = 0; String accessToken = getToken(appId, appSecret); String menu_create_url ="https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN"; String url = menu_create_url.replace("ACCESS_TOKEN", accessToken); MenuDTO menuDTO = new MenuDTO(); ButtonDTO buttonDTO1 = new ButtonDTO("pic_photo_or_album", "上传照片", "11", null); ButtonDTO buttonDTO2 = new ButtonDTO("click", "click", "12", null); ButtonDTO buttonDTO3 = new ButtonDTO("view", "view", null, "https://weui.io/#uploader"); List<ButtonDTO> buttonDTOList = new ArrayList<>(); buttonDTOList.add(buttonDTO1); buttonDTOList.add(buttonDTO2); buttonDTOList.add(buttonDTO3); menuDTO.setButton(buttonDTOList); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); MultiValueMap<String, String> params = new LinkedMultiValueMap<String,String>(); params.add("data", JsonUtils.encode(menuDTO)); System.out.println(JsonUtils.encode(menuDTO)); HttpEntity<MultiValueMap<String, String>> requestEntity = newHttpEntity<MultiValueMap<String, String>>(params, headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.exchange(url,HttpMethod.POST, requestEntity, String.class); System.out.println(response.getBody()); }
|
运行方法,如果没有错误信息,则返回:
1 2 3 4
| { "errcode": 0, "errmsg": "ok" }
|
打开测试公众号,可以看到我们新加的自定义菜单。

菜单响应
这里的响应主要针对上传照片和click按钮,view按钮会自动跳转到初始化的url页面上。
在web.xml中配一个servlet
1 2 3 4 5 6 7 8 9
| <servlet> <servlet-name>weixin</servlet-name> <servlet-class>com.star.ott.tokenmanage.api.business.CoreServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>weixin</servlet-name> <url-pattern>/wx</url-pattern> </servlet-mapping>
|
CoreServlet.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| public class CoreServlet extends HttpServlet { private static final String OK = "OK"; public String get() { return OK; } * 确认请求来自微信服务器 */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String echostr = request.getParameter("echostr"); response.setHeader("content-type", "text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); if (SignUtil.checkSignature(signature, timestamp, nonce)) { out.print(echostr); } out.close(); out = null; } * 处理微信服务器发来的消息 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); String respMessage = processRequest(request); PrintWriter out = response.getWriter(); out.print(respMessage); out.close(); } private String processRequest(HttpServletRequest request) { String respMessage = null; try { String respContent = "请求处理异常,请稍候尝试!"; Map<String, String> requestMap = MessageUtil.parseXml(request); 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_IMAGE)) { respContent = "您发送的是图片消息!"; } if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { String eventType = requestMap.get("Event"); if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) { String eventKey = requestMap.get("EventKey"); if (eventKey.equals("12")) { respContent = "被点击!"; } } } textMessage.setContent(respContent); respMessage = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respMessage; } }
|
SignUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| public class SignUtil { private static String token = "weixintest"; * 验证签名 * * @param signature * @param timestamp * @param nonce * @return */ public static boolean checkSignature(String signature, String timestamp, String nonce) { String[] arr = new String[]{token, timestamp, nonce}; Arrays.sort(arr); StringBuilder content = new StringBuilder(); for (int i = 0; i < arr.length; i++) { content.append(arr[i]); } MessageDigest md = null; String tmpStr = null; try { md = MessageDigest.getInstance("SHA-1"); byte[] digest = md.digest(content.toString().getBytes()); tmpStr = byteToStr(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } content = null; return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false; } * 将字节数组转换为十六进制字符串 * * @param byteArray * @return */ private static String byteToStr(byte[] byteArray) { String strDigest = ""; for (int i = 0; i < byteArray.length; i++) { strDigest += byteToHexStr(byteArray[i]); } return strDigest; } * 将字节转换为十六进制字符串 * * @param mByte * @return */ private static String byteToHexStr(byte mByte) { char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; char[] tempArr = new char[2]; tempArr[0] = Digit[(mByte >>> 4) & 0X0F]; tempArr[1] = Digit[mByte & 0X0F]; String s = new String(tempArr); return s; } }
|
MessageUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| public class MessageUtil { * 返回消息类型:文本 */ public static final String RESP_MESSAGE_TYPE_TEXT = "text"; * 请求消息类型:图片 */ public static final String REQ_MESSAGE_TYPE_IMAGE = "image"; public static final String REQ_MESSAGE_TYPE_EVENT = "event"; * 事件类型:CLICK(自定义菜单点击事件) */ public static final String EVENT_TYPE_CLICK = "CLICK"; public static final String EVENT_TYPE_PHOTO = "pic_photo_or_album"; * 解析微信发来的请求(XML) * * @param request * @return * @throws Exception */ @SuppressWarnings("unchecked") public static Map<String, String> parseXml(HttpServletRequest request) throws Exception { Map<String, String> map = new HashMap<String, String>(); InputStream inputStream = request.getInputStream(); SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); Element root = document.getRootElement(); List<Element> elementList = root.elements(); for (Element e : elementList) map.put(e.getName(), e.getText()); inputStream.close(); inputStream = null; return map; } * 文本消息对象转换成xml * @param textMessage 文本消息对象 * @return xml */ public static String textMessageToXml(TextMessage textMessage) { xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } * 扩展xstream,使其支持CDATA块 */ private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { boolean cdata = true; @SuppressWarnings("unchecked") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write("<![CDATA["); writer.write(text); writer.write("]]>"); } else { writer.write(text); } } }; } }); }
|
参考文章:https://blog.csdn.net/lyq8479/article/details/8952173