1. 测试号申请

不同的公众号类型具备不同的接口权限,一般个人申请的是未认证订阅号,没有自定义菜单的功能,只有企业组织科申请工作好认证公众号。不同类型的公众号的不同接口权限详情见:公众号接口权限说明

我们可以申请一个接口测试号,这个测试号几乎开放所有的接口,可以供测试使用, 接口测试号申请。测试号如下图,可以用测试号的appID和appsecret进行接口调用。

image

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;
//省略get set
}

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;
// get set
}

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;
}
// get set
}

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);
// 拼装创建菜单的url
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);
// 将菜单对象转换成json字符串
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"
}

打开测试公众号,可以看到我们新加的自定义菜单。

image

菜单响应

这里的响应主要针对上传照片和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();
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
out.print(echostr);
}
out.close();
out = null;
}
/**
* 处理微信服务器发来的消息
*/
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 将请求、响应的编码均设置为UTF-8(防止中文乱码)
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 = "请求处理异常,请稍候尝试!";
// 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_IMAGE)) {
respContent = "您发送的是图片消息!";
}
// 事件推送
if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {
// 事件类型
String eventType = requestMap.get("Event");
if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {
// 事件KEY值,与创建自定义菜单时指定的KEY值对应
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};
// 将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");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
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 {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
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) {
// 对所有xml节点的转换都增加CDATA标记
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