【公众号开发】(3)
文章目录
- 【公众号开发】(3)
- 1. 获取Access token
- 1.1 确定参数
- 1.2 补全URL(添加query string)
- 1.3 测试
- 2. 封装AccessToken以便保存与后期使用
- 2.1 TokenUtils做出一些调整
- 2.2 单例模式的AccessToken
- 2.3 TokenUtils获取全局唯一的token字符串的方法
- 3. 自定义菜单
- 3.1 菜单显示的原理
- 3.2 封装菜单类
- 3.3 构造一个菜单对象
- 3.4 发送post请求
- 3.5 启动main方法查看效果
- 4. 处理自定义菜单事件
- 4.1 了解公众号发过来的post请求机制
- 4.2 了解公众号发过来的post请求格式
- 4.3 分支处理请求
- 4.4 测试
【公众号开发】(3)
开始开发 / 获取 Access token (qq.com)
access_token是公众号的全局唯一的接口调用凭据,公众号调用各接口时都需使用access_token
开发者需要进行妥善保存
- access_token的存储至少要保留512个字符空间
- access_token的有效期目前为2个小时(7200s),需定时刷新,重复获取将导致上次获取的access_token失效
获取到Access token,我们才能够去调用微信公众号给我们提供的一些接口(Access token就类似于第三方接口的key,验证凭据后才可以去实现一些功能)
1. 获取Access token
这里是几张来自文档的重点截图:
1.1 确定参数
public class TokenUtils {
private static final String APP_ID = "wxdadd0122365919e8";
private static final String APP_SECRET = "69fd4a3ad04167f288e49bea9dce3e45";
public static String getAccessToken() {
// 获取token的url
final String URL = "https://api.weixin.qq.com/cgi-bin/token";
// 获取token的grant_type
final String GRANT_TYPE = "client_credential";
}
}
1.2 补全URL(添加query string)
public static String getAccessToken() {
// 获取token的url
final String URL = "https://api.weixin.qq.com/cgi-bin/token";
// 获取token的grant_type
final String GRANT_TYPE = "client_credential";
// 构造参数表
Map<String, Object> param = new HashMap<String, Object>(){{
this.put("grant_type", GRANT_TYPE);
this.put("appid", APP_ID);
this.put("secret", APP_SECRET);
}};
// 发起get请求
String response = HttpUtils.doGet(URL, param);
// 解析json
Map<String, Object> result = JsonUtils.jsonToMap(response);
System.out.println(result);
// 返回token
return (String) result.get("access_token");
}
1.3 测试
public static void main(String[] args) {
System.out.println(getAccessToken());
}
2. 封装AccessToken以便保存与后期使用
这里我们全局的AccessToken唯一一份,我们希望其未过期就无需刷新,这里用的是**单例模式**!
2.1 TokenUtils做出一些调整
为了实现这个初心,在TokenUtils做出一些调整
- 改为获取map
2.2 单例模式的AccessToken
- 单例模式参考文章:【JavaEE】线程案例-单例模式 and 阻塞队列_s:103的博客-CSDN博客
@Data
public class AccessToken {
private String token;
private long expireTime;//有效期限
volatile private static AccessToken accessToken = null;
public void setExpireTime(long expireIn) {
// 设置有效期限的时候的时间戳
this.expireTime = System.currentTimeMillis() + expireIn * 1000;
}
public boolean isExpired() {
return System.currentTimeMillis() > this.getExpireTime();
}
private static void setAccessToken() {
if(accessToken == null) {
accessToken = new AccessToken();
}
Map<String, Object> map = TokenUtils.getAccessTokenMap();
accessToken.setToken((String) map.get("access_token"));
accessToken.setExpireTime((Integer) map.get("expires_in"));
}
public static AccessToken getAccessToken() {
if(accessToken == null || accessToken.isExpired()) {
synchronized (AccessToken.class) {
if(accessToken == null || accessToken.isExpired()) {
setAccessToken();
}
}
}
return accessToken;
}
}
2.3 TokenUtils获取全局唯一的token字符串的方法
public static String getToken() {
return AccessToken.getAccessToken().getToken();
}
测试:
public static void main(String[] args) {
System.out.println(getToken());
System.out.println(getToken());
System.out.println(getToken());
}
- 三个一样,代表我们的AccessToken单例第一次被实例和设置并且因为没有过期而没有被更新~
有了凭据之后,我们就可以去调用微信公众号给我们提供的一些接口了,实现一些功能~
3. 自定义菜单
你会发现,我们的测试公众号现在还没有菜单的选项
而我们的常识也知道,公众号的菜单是必不可少的,接下来我们来完成一下自定义菜单吧
开发手册:自定义菜单 / 创建接口 (qq.com)
抓重点:
- 按字数截取…
- 刷新策略我们创建后再讲
自定义菜单接口,就相当于触发各种各样事件
3.1 菜单显示的原理
我们提交的信息会给公众号服务器保存起来,构造成菜单显示给用户~
这里我们来看个post请求body的例子:
- (要求是json,这也合理,因为我们要传递的信息就是多个菜单,多级菜单,这个可是对象~)
我们需要什么功能,我们就查看与学习对应的按钮类型和其他参数就行了
- 自定义菜单 / 创建接口 (qq.com)
3.2 封装菜单类
对于这个post请求的body,也就是这个json字符串的构造,是最大的问题,我们首先要封装菜单类
@Data
public class Button {
private List<AbstractButton> button;
}
这是构造最外层的button属性:
{
"button": [...]
}
AbstractButton是我们抽象出来的按钮类(可以是一些按钮/二级菜单)
@Data
public abstract class AbstractButton {
private String name;
public AbstractButton(String name) {
this.name = name;
}
public AbstractButton() {
}
}
这个name属性是按钮/二级菜单的共性(二级菜单没有type,所以这里不应该写type)
根据刚才的json字符串,里面提到的属性就是对应类型按钮的属性~
以这几个为示例(其他根据实际举一反三就行):
@Data public class ViewButton extends AbstractButton { private final String type = "view"; private String url; public ViewButton(String name) { super(name); } }
@Data public class ClickButton extends AbstractButton { private final String type = "click"; private String key; public ClickButton(String name) { super(name); } }
@Data public class PicPhotoOrAlbumButton extends AbstractButton { private final String type = "pic_photo_or_album"; private String key; public PicPhotoOrAlbumButton(String name) { super(name); } }
@Data public class SubButton extends AbstractButton { private List<AbstractButton> sub_button; public SubButton(String name) { super(name); } }
3.3 构造一个菜单对象
预计菜单效果如下:
public class ButtonUtils {
public static Button createButton() {
Button button = new Button();
button.setButton(new ArrayList<>());
return button;
}
public static ClickButton createClickButton(String name, String key) {
ClickButton clickButton = new ClickButton(name);
clickButton.setKey(key);
return clickButton;
}
public static ViewButton createViewButton(String name, String url) {
ViewButton viewButton = new ViewButton(name);
viewButton.setUrl(url);
return viewButton;
}
public static SubButton createSubButton(String name) {
SubButton subButton = new SubButton(name);
subButton.setSub_button(new ArrayList<>());
return subButton;
}
public static PicPhotoOrAlbumButton createPicPhotoOrAlbumButton(String name, String key) {
PicPhotoOrAlbumButton picPhotoOrAlbumButton = new PicPhotoOrAlbumButton(name);
picPhotoOrAlbumButton.setKey(key);
return picPhotoOrAlbumButton;
}
public static void main(String[] args) {
Button button = createButton();
// 一级菜单的两个按钮
button.getButton().add(createClickButton("mara\uD83D\uDE00😀😀😀", "1"));
button.getButton().add(createViewButton("baidu\uD83D\uDE00", "https://www.baidu.com"));
// 二级菜单
SubButton subButton = createSubButton("更多\uD83D\uDE00");
subButton.getSub_button().add(createClickButton("mason\uD83D\uDE00", "2"));
subButton.getSub_button().add(createViewButton("blog\uD83D\uDE00", "https://blog.csdn.net/Carefree_State?type=blog"));
subButton.getSub_button().add(createPicPhotoOrAlbumButton("上传图片\uD83D\uDE00", "3"));
// 二级菜单加入到一级菜单中
button.getButton().add(subButton);
// System.out.println(button);
String json = JsonUtils.objectToJson(button);
System.out.println(json);
}
}
emoji可以直接复制或者用unicode码,本质没啥区别,跟普通字符差不多:
- 之前我做的网站,文本中有emoji是不行的,因为我的服务器并不支持emoji存到数据库
- 可以这样:👨💻你知道如何使用MySQL存储Emoji表情吗?明白MySQL中UTF-8和UTF-8MB4字符编码有何区别吗? - 知乎 (zhihu.com)
Unicode 11.0版本的emoji表情 - emoji大全,emoji百科 (emojidaquan.com)
打印json后查看效果:
在线 JSON 解析 | 菜鸟工具 (runoob.com)
😊符合预期😊
对于json集合属性的序列,各个元素json字符串都不一样或者有联系,可以试试抽象成一个类,具体类继承这个抽象类,序列化的时候序列的是具体的实例!
或者,你干脆写成List<Object>
也行,序列化的时候自然知道这个Object是谁向上转型来的,序列化也能正确,不过每个按钮都有name的~
3.4 发送post请求
url的创建:
- https://api.weixin.qq.com/cgi-bin/menu/create,访问的接口~
- queryString:携带我们的access_token,调用方法获取即可~
// 构造url
String url = " https://api.weixin.qq.com/cgi-bin/menu/create" + HttpUtils.getQueryString(new HashMap<String, Object>() {{
this.put("access_token", TokenUtils.getToken());
}});
// 发送post请求
String response = HttpUtils.doPost(url, json);
System.out.println(response);
这里,doPost是区别于提交form格式的doPost的一个重载方法,作用就是根据url,提交json字符串:
public static String doPost(String httpUrl, String json) {
HttpURLConnection connection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
String result = null;
try {
URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置连接请求方式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
// 通过连接对象获取一个输出流
outputStream = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
outputStream.write(json.getBytes());
// 通过连接对象获取一个输入流,向远程读取
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 对输入流对象进行包装:charset根据工作项目组的要求来设置
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
// 循环遍历一行一行读取数据
while ((temp = bufferedReader.readLine()) != null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();
}
}
return result;
}
3.5 启动main方法查看效果
微信公众号查看:
点击baidu😀(view按钮)跳转:
点击更多😀上拉菜单:
点击上传按钮(photo按钮)😀:
对于click按钮,点击了似乎没什么作用,接下来俺们来研究研究这个!
4. 处理自定义菜单事件
其实,用户每点击一次按钮,就相当于与公众号交互,对于这个“按钮事件”,消息类型为Event
也就是这里的其他消息类型:
而只要是用户发来的消息,都会触发公众号服务器发送post请求到我们的服务器的根路径
- 也就是之前写的那个接口一致
开发文档:基础消息能力 / 接收事件推送 (qq.com)
以自定义菜单事件为例,其他的举一反三、自行学习😊
这里以click按钮为例子!
4.1 了解公众号发过来的post请求机制
如果是上拉菜单的按钮,则不会上报,也就是不会发post请求
- 或者是弹出“拍照/上传图片”,这也算是子菜单吧,等等类似的~
4.2 了解公众号发过来的post请求格式
这个key,就是我们之前的按钮属性里的key:
这个key对于公众号服务器而已没啥作用,但是对于开发者而言很重要,因为post请求访问的是同一个接口,并且,请求并没有发送按钮名参数,并且按钮名也不一定唯一,开发者用按钮名来区分每个按钮不合理!
所以有公众号辅助我们,以key为按钮的标识,作为参数传递给开发者
开发者以key作为区分按钮触发事件的手段,不同的key执行不同的业务~
4.3 分支处理请求
对于不同的消息类型、不同的事件类型、不同的key,你可以用哈希表记录“键与业务方法”,这里的业务方法可以是一个接口,用普通类去实现接口,最后结合多态实现,输入键执行对于业务
这里我为了方便,易懂,任意演示/调试,用的是swtich分支处理
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException {
String body = HttpUtils.getBody(request);
Map<String, Object> map = XmlUtils.xmlToMap(body);
System.out.println(map);
// 回复消息
String message = "";
String MsgType = (String) map.get("MsgType");
switch (MsgType) {
case "event":
message = handleEvent(map);//处理事件
break;
case "text":
message = handleText(map);//处理文本
break;
default:
System.out.println("其他消息类型");
break;
}
return message;
}
- message返回空字符串才是正常的不回复,其他都是因为错误而不回复的
handleText:
private String handleText(Map<String, Object> map) {
String message = "";
if("图文".equals(map.get("Content"))) {
NewsMessage newsMessage = NewsMessage.getReplyNewsMessage(map);
message = XmlUtils.objectToXml(newsMessage);
System.out.println(message);
}else {
// 1. 封装对象
TextMessage textMessage = TextMessage.getAntonym(map);
// 2. 序列化对象
message = XmlUtils.objectToXml(textMessage);
}
return message;
}
handleEvent:
- 通过事件类型分支
private String handleEvent(Map<String, Object> map) {
String message = "";
// 获取event值
String event = (String) map.get("Event");
// 事件分支
switch (event) {
case "CLICK":
message = EventUtils.handleClick(map);
break;
case "VIEW":
System.out.println("view");
break;
default:
break;
}
return message;
}
EventUtils.handleClick:
public class EventUtils {
public static String handleClick(Map<String, Object> map) {
String message = "";
String key = (String) map.get("EventKey");
switch (key) {
case "1":
map.put("Content","\"触发了点击事件,key = 1\"");
break;
case "2":
map.put("Content","\"触发了点击事件,key = 2\"");
break;
case "3":
map.put("Content","\"触发了点击事件,key = 3\"");
break;
default:
break;
}
TextMessage textMessage = TextMessage.getReplyTextMessage(map);
message = XmlUtils.objectToXml(textMessage);
return message;
}
}
这个只是示例,至于你要执行什么业务,是你的事咯
4.4 测试
点击view类型按钮:
查看控制台:
还是那句话,这个只是示例,至于你要执行什么业务,是你的事咯
举一反三,由你发挥,一生万物!
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!代码:wx-demo · 游离态/马拉圈2023年10月 - 码云 - 开源中国 (gitee.com)