系列文章传送门
整个项目的源码已经上传到百度网盘(博主的Git在维护,就不拿出来丢人了),永久有效,免费,在ChatConf类中填写自己的APPID和开发者密钥,在相关地方替换一下外网域名,即可使用,如有任何问题,欢迎在下方评论:
链接:百度网盘传送门
提取码:03eb
目录
前言
在上一篇文章中,我们将项目接入了微信,并且与用户进行了简单的交互,但是我们的公众号看上去依然很单调,一个成熟的公众号必然有属于自己的业务,而不是简单的回答用户的问题。那么在这篇文章中,博主就带大家在自己的公众号中创建菜单,实现更丰富的功能!
在进行开发之前,请大家先在微信测试号系统进行登录,以及使用natapp进行内网穿透,新来的同学可参考我的前两篇文章。
微信测试号系统传送门:
https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
根据文档进行技术准备
老规矩,我们先看微信文档怎么说。强烈建议大家一定要有阅读文档、并且根据文档进行学习的能力,这是程序员的根,命。
微信公众平台官方文档传送门:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1445241432
这里博主只贴出这一张图,其他的部分各位自己看,主要是有以下几个信息就够了:
- 如果想创建菜单,需要以HTTPS协议去调用微信公众平台的相关接口
- 调用的时候需要传入一个JSON格式的请求体,JSON字符串的内容就是对菜单的描述
- 调用接口时需要传入access_token
好,我们只需要知道以上3点信息,就够了。这里涉及到access_token,之前我们在公众号进行的所有开发,都没有用到过它,因为之前并没有涉及到调用微信的接口,那access_token是什么玩意呢?
做过前后端分离的小伙伴应该对token不陌生,我们把token作为验证用户身份的凭证,这里的access_token也是一个意思,微信这么大的平台,提供的接口总不能谁都可以调用吧?而且你每次请求肯定要带一个标识,要不然这么多入驻微信的公众号,它怎么知道此次请求要操作哪个公众号?所以,access_token就是我们调用微信接口的凭证、身份证。
话不多说,今天的干货就从获取access_token开始。
定期获取access_token并保存
获取access_token的接口,并不是可以随心所欲地调用的,每天都有请求上限,而且每次需要它的时候都去获取,在性能上也是一笔不菲的开销。根据微信公众平台的描述,每次获取到的access_token,有效期是7200秒左右,又因为每天有请求上限,所以这就要求我们获取到access_token之后进行保存,并定期去刷新它的值。而且,微信会在老的access_token过期的5分钟之内,保证新老access_token都可用,使业务过渡更加平滑。
OK,我们开始吧!根据接口要求,我们需要传入appid和secret参数,因为我们是用测试号进行开发,所以先去测试号系统获取你的appID和appsecret。
仍然继续我们之前创建的项目,我们创建一个ChatConf类,保存一个必要的共用配置:
package com.blog.wechat.conf;
/**
* 公众号开发配置类,保存一些必要的配置
* @author 秋枫艳梦
* @date 2019-06-07
* */
public class ChatConf {
//获取到的凭证
public static volatile String token;
public static String getToken() {
return token;
}
public static void setToken(String token) {
ChatConf.token = token;
}
//第三方用户唯一凭证
public static final String APPID = "*****";
//第三方用户唯一凭证密钥
public static final String SECRET = "*****";
//获取access_token的接口请求地址
public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+APPID+"&secret="+SECRET;
//创建菜单的接口请求地址
public static final String CREATE_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/create";
}
又因为我们需要访问HTTPS类型的接口,所以要做一些处理,博主这里使用okhttp(需要引入maven依赖,不懂的可参考我之前的文章),所以先创建下面这个类:
package com.blog.wechat.conf;
import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/** HTTPS认证配置类
* @author 秋枫艳梦
* @date 2019-05-25
* */
public class SSLConf {
private static SSLSocketFactory sslSocketFactory; //SSLSocketFactory对象
/**
* 返回SSLSocketFactory工厂
* */
public static SSLSocketFactory getSslSocketFactory() {
return sslSocketFactory;
}
/**
* 静态块,实例化SSLSocketFactory工厂对象
* */
static {
SSLContext sslContext= null;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,new TrustManager[]{new TrustAllManager()},new SecureRandom());
sslSocketFactory=sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 静态内部类,实现X509TrustManager接口
* */
public static class TrustAllManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
/**
* 静态内部类,实现HostnameVerifier接口
* */
public static class TrustAllHost implements HostnameVerifier {
/** 此方法用于验证客户机,省略验证逻辑,保证返回true即可通过验证
* @param s 认证字符串,类似于token
* @param sslSession SSL会话
* @return 是否通过验证
* */
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}
}
然后我们创建一个工具类,用于获取并定时刷新access_token:
package com.blog.wechat.utils;
import com.alibaba.fastjson.JSON;
import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 获取access_token并定期刷新的工具类
* @author 秋枫艳梦
* @date 2019-06-07
* */
public class TokenUtil {
/**
* 获取access_token并保存
* */
public static void getToken() throws InterruptedException {
while (true){
//客户端
OkHttpClient client = null;
//响应体
Response response = null;
//请求体
Request request = null;
try {
//创建一个可以访问HTTPS的客户机
client = new OkHttpClient.Builder()
.sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
.hostnameVerifier(new SSLConf.TrustAllHost()).build();
//构建请求体
request = new Request.Builder().url(ChatConf.ACCESS_TOKEN_URL).get().build();
//发起请求,获取响应体
response = client.newCall(request).execute();
if (response.isSuccessful()){
String token = JSON.parseObject(response.body().string()).getString("access_token");
ChatConf.setToken(token);
}
}catch (IOException e){
}finally {
if (response!=null){
response.close();
}
client.dispatcher().executorService().shutdown();
}
System.out.println("此次获取到的token是:"+ChatConf.getToken());
//不到两小时去获取一次
TimeUnit.MINUTES.sleep(115);
}
}
/**
* 开始任务
* */
public static void startTask(){
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
TokenUtil.getToken();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(runnable).start();
}
}
然后创建一个监听器,项目一启动就开始获取access_token的任务:
package com.blog.wechat.listener;
import com.blog.wechat.utils.TokenUtil;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class ProjectListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
TokenUtil.startTask();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
在web.xml中配置监听器:
<listener>
<listener-class>com.blog.wechat.listener.ProjectListener</listener-class>
</listener>
启动项目,我们可以看到已经成功获取到了:
创建不同类型的菜单
接下来我们就开始创建菜单,由于最常用的菜单类型就是click、view型,所以我们这里将创建的菜单格式如下:
其中红色的是view类型的,点击可跳到目标页面;黄色的是click类型的,点击之后触发交互事件;白色的为父菜单。
注意:其实在真正的环境中,创建菜单的动作一般很少有,甚至只创建一次,所以我们直接复制粘贴控制台打印的token,直接走一个main()方法执行。
创建一个MenuUtil类,用于创建菜单:
package com.blog.wechat.utils;
import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.*;
import java.io.IOException;
/**
* 创建菜单的工具类
* @author 秋枫艳梦
* @date 2019-06-07
* */
public class MenuUtil {
//今日热点的URL,这里跳向网易新闻
private static final String HOTSPOT = "https://www.163.com/";
//激流勇进的URL,跳向百度百科
private static final String GAME = "https://baike.baidu.com/item/%E6%BF%80%E6%B5%81%E5%8B%87%E8%BF%9B/66432?fr=aladdin";
//全民冒险的URL,这里跳到吃鸡首页
private static final String ADVENTURE = "https://gp.qq.com/main.shtml?ADTAG=media.buy.baidukeyword.fppc_HPJY_u24796905.k121990619513.a29552693737";
//折扣专场,跳到京东
private static final String BUY = "https://h5.m.jd.com/pc/dev/2QurYgV498yahfXFcbmXeNuQpCyQ/index.html?unionActId=31067&d=CoY67X&s=&cu=true&utm_source=home.firefoxchina.cn&utm_medium=tuiguang&utm_campaign=t_220520384_&utm_term=8a904ba935904ef1b59178369b0faca7";
//我的订单URL,跳向当前项目中的页面
private static final String ORDER = "http://sc2ess.natappfree.cc/home.html";
public static void main(String[] args) {
String paramStr = "{\n" +
" \"button\":[\n" +
" {\n" +
" \"type\":\"view\",\n" +
" \"name\":\"今日热点\",\n" +
" \"url\":\""+HOTSPOT+"\"\n" +
" },\n" +
" {\n" +
" \"name\":\"热情一夏\",\n" +
" \"sub_button\":[\n" +
" {\n" +
" \"type\":\"view\",\n" +
" \"name\":\"激流勇进\",\n" +
" \"url\":\""+GAME+"\"\n" +
" },\n" +
" {\n" +
" \"type\":\"view\",\n" +
" \"name\":\"全民冒险\",\n" +
" \"url\":\""+ADVENTURE+"\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" {\n" +
" \"name\":\"更多服务\",\n" +
" \"sub_button\":[\n" +
" {\n" +
" \"type\":\"view\",\n" +
" \"name\":\"我的订单\",\n" +
" \"url\":\""+ORDER+"\"\n" +
" },\n" +
" {\n" +
" \"type\":\"click\",\n" +
" \"name\":\"生成海报\",\n" +
" \"key\":\"CREATE_POSTER\"\n" +
" },\n" +
" {\n" +
" \"type\":\"view\",\n" +
" \"name\":\"折扣专场\",\n" +
" \"url\":\""+BUY+"\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}";
OkHttpClient client = new OkHttpClient.Builder().sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
.hostnameVerifier(new SSLConf.TrustAllHost()).build();
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"),paramStr);
Request request = new Request.Builder()
.url(ChatConf.CREATE_MENU_URL+"?access_token=22_RlBbwwMK3goOKuKek61zJSc6uZzw-Byw_____Jj0e1g64RXeT4SmVHnMuwHxaEuPhzCgqztfBdcmvZ1MdNzHZy1exaNyNvYzaE14Eqt1bviWYacK2nYJDgI9PwoqSfybUrSgDGdLHoeBYE8pQEJaAJAMAF")
.post(requestBody).build();
Response response = null;
try {
response = client.newCall(request).execute();
if (response.isSuccessful()){
System.out.println(response.body().string());
}
}catch (IOException e){
}finally {
if (response!=null){
response.close();
}
client.dispatcher().executorService().shutdown();
}
}
}
运行这个类,可以发现菜单已经创建成功了:
然后我们去公众号看一下:
到这里,我们的菜单就创建完了,效果也都有。细心的朋友会发现,点击“生成海报”会提示异常,这是因为我们并没有在服务器端处理点击菜单的事件,很简单,我们完善之前的一段代码:
//先判断是事件消息,还是普通消息
if (map.get("MsgType").equals("event")){
//如果是被关注事件,向用户回复内容,只需要将整理好的XML文本参数返回给微信即可
if (map.get("Event").equals("subscribe")){
content = "欢迎关注秋枫艳梦的测试公众号!";
}else if (map.get("Event").equals("CLICK")){
//点击菜单事件,判断EventKey
if (map.get("EventKey").equals("CREATE_POSTER")){
content = "为您生成带二维码的海报,将在下一篇博文实现,敬请期待";
}
}
}else if (map.get("MsgType").equals("text")){
//如果是普通文本消息,先拿到用户发送过来的内容,模拟自动答疑的场景
String text = map.get("Content");
if (text.equals("1")){
content = "您可以在“我的账户——服务——退款”中查看您的退款明细";
}else if (text.equals("2")){
content = "如果您购买了本店的产品,订单页面会展示在您的主菜单中";
}else if (text.equals("3")){
content = "如有更多问题,请拨打我们的客服热线:xxxxx";
}else {
//否则,不管用户输入什么,都返回给ta这个列表,这也是最常见的场景
content = "请输入您遇到的问题编号:\n"+
"1、如何查看退款进度?\n"+
"2、我的订单在哪里查看?\n"+
"3、其他问题";
}
}
重新启动项目,再次去公众号测试,就不会提示异常了:
总结
今天的文章,就记录到这里,相信大家的收获还是很大的,毕竟一旦在公众号创建了菜单,用户就可以通过点击菜单访问我们的网页,这已经很大力度的促进了我们的业务推广。
但是总有一些复杂的场景要做,微信公众号的开发也远不止这些。
在下一篇博文中,博主将带大家创建带参数的微信二维码,以及将二维码和图片生成一张海报,在用户点击“生成海报”的时候返回一张海报图片。这也是经常用的营销手段,用户可以在生成海报之后将图片分享,其他人通过他生成的海报扫码关注公众号,我们再拿到二维码中的参数,对这个生成海报的用户提供奖励机制……
是不是很刺激?敬请关注此系列博文,持续更新,连载!!!