【除夕夜特辑】手把手教你微信公众号开发


各位读者朋友们好,今天是除夕,在这里先祝大家新年快乐。这篇文章其实是一个星期前写的,一直在电脑里放着,赶上今天的好日子,机缘巧合的情况下被我重新发现,于是就发布出来了。

文章主题

这篇文章的主题是微信公众号开发,我在去年也开通了自己的微信公众号,不过没有很用心地去做,然后空闲时间自己也去了解了一下微信公众号的开发, 这里便讲讲自己的所学。

开发环境的搭建

关于微信公众号,这里我不做过多介绍,公众号分为服务号和订阅号,不过一般大家的公众号应该都是订阅号。
先来搭建一下公众号开发的环境。

为了避免我有推广的嫌疑,服务器的搭建我就不介绍了,搭建好后的域名为:http://diyweixin.free.idcfengye.com,后面的开发都基于此。

这样开发环境就搭建完成了,我们可以测试一下。
打开ecplise,创建一个动态的web工程:
在这里插入图片描述
然后新建一个index.jsp文件,在页面上写一个"Hello World!"。
当我们启动该项目后,就可以通过服务器的域名进行访问了,访问的地址应为:``http://diyweixin.free.idcfengye.com/wechat/index.jsp
大家可以拿手机或者别的设备尝试一下,看看不在同一个局域网下能否成功访问。

接入微信公众平台

开发环境搭建好后,我们需要接入到微信公众平台进行公众号开发。
大家可以自己阅读公众号开发文档,文档中介绍,接入需要三个步骤:
在这里插入图片描述
我们一一来实现。

填写服务器配置

通过公众号开发,能够大大丰富自己公众号的功能,不过对于不同的公众号,微信平台提供的权限不大相同。对于普通的订阅号,权限相对较少一些。大家可以在公众号后台的接口权限中进行查看:
在这里插入图片描述
对于这样的情况,微信团队当然有所考虑,为了降低开发者的学习门槛,微信平台提供了测试号供开发者进行学习,那么下面就来申请一下测试号。
打开公众号开发文档,找到开始开发下面的接口测试号申请:
在这里插入图片描述
点击进入申请系统,然后
在这里插入图片描述
此时进入到该页面:
在这里插入图片描述
页面下方有测试号的二维码,可以关注测试后续功能:
在这里插入图片描述
这里需要填写两个信息:

  1. URL:这是开发者用来接收微信消息和事件的接口URL
  2. Token:用作生成签名,可任意填写

下面我们在Web项目中创建一个Servlet:

@WebServlet("/WxServlet")
public class WxServlet extends HttpServlet {
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		System.out.println("Get");
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		System.out.println("Post");
	}
}

编写完成后,我们启动该项目,并在URL中填入该Servlet的访问路径:
http://diyweixin.free.idcfengye.com/wechat/WxServlet
在这里插入图片描述
Token可以随意填写。
此时当我们点击提交按钮,微信服务器便会发送一个Get请求到填写的服务器地址URL上。
点击提交后,控制台便会输出Get:
在这里插入图片描述

验证消息的确来自微信服务器

下面我们还需要验证一下消息是否真的来自微信服务器,开发文档中有详细介绍验证过程:

开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

参数描述
signature微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
timestamp时间戳
nonce随机数
echostr随机字符串

开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

  1. 将token、timestamp、nonce三个参数进行字典序排序
  2. 将三个参数字符串拼接成一个字符串进行sha1加密
  3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

这里需要对参数进行sha1加密,我们单独写一个类,用于实现工具方法:

public class WxService {
	//这里的Token要和之前填写的Token一致
	private static final String TOKEN = "abcdefg";
	
	/**
	*	校验
	*/
	public static boolean check(String timestamp,String nonce,String signature) {
		//将token、timestamp、nonce进行字典排序
		String[] strs = new String[] {TOKEN,timestamp,nonce};
		Arrays.sort(strs);
		//将三个参数字符串拼接成一个字符串进行sha1加密
		String str = strs[0] + strs[1] + strs[2];
		String mySig = sha1(str);
		return mySig.equals(signature);
	}
	
	/**
	 * sha1加密
	 * @param str
	 * @return
	 */
	private static String sha1(String str) {
		try {
			MessageDigest md = MessageDigest.getInstance("sha1");
			//加密
			byte[] digest = md.digest(str.getBytes());
			char[] chars = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
			StringBuilder sb = new StringBuilder();
			//处理加密结果
			for(byte b : digest) {
				//处理高四位
				sb.append(chars[(b>>4) & 15]);
				//处理低四位
				sb.append(chars[b & 15]);
			}
			return sb.toString();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return null;
	}
}

这里的Token要和之前填写的Token一致。
实现也很简单,先将三个参数放入String数组,然后按照字典进行排序,对于字符串,默认的排序方式即为字典排序,所以直接调用Arrays类的sort方法即可。
最后将三个参数拼接成为一个字符串,并进行sha1加密。
加密算法单独抽取了一个方法,通过MessageDigest类,我们将字符串转换为一个byte数组,接着对byte数组进行加密处理。

比较常见的处理方式是,遍历byte数组,然后对数组中的每个byte进行处理,一个byte有8位,将其分为两部分:高四位和低四位。
对于高四位,我们让其右移4位,这样低四位便移除出去,但是前面四位就没有了,我们再让其与上15,因为15的二进制为:0000 1111,因为是与操作,所以前面四位一定是0,此时便将高四位转换为了一个16进制的数。
同理,对于低四位,我们也将其转换为一个16进制的数,让其与上15即可。
然后我们定义一个char类型的数组,存放的是16进制数:

char[] chars = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};

前面我们已经将一个byte的高四位和低四位转换为了16进制数,所以我们将一个byte对应的两个16进制数拼接到StringBuilder上即可。

最后返回加密结果。

工具类编写完成,我们回到主程序中对其进行调用:

@WebServlet("/WxServlet")
public class WxServlet extends HttpServlet {
	
	protected 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");
		
		//校验请求
		if(WxService.check(timestamp,nonce,signature)) {
			System.out.println("接入成功");
			//原样返回echostr参数
			PrintWriter out = response.getWriter();
			out.print(echostr);
			out.flush();
			out.close();
		}else {
			System.out.println("接入失败");
		}
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		System.out.println("Post");
	}
}

如果接入成功,需要原样返回echostr参数,此时接入生效。
我们测试一下,先运行项目,然后在页面上点击提交按钮:
在这里插入图片描述
接口配置信息也提交成功:
在这里插入图片描述
注意:有时候会出现配置失败的情况,这是由于ngrok工具的不稳定导致的,毕竟是免费的。

这样就成功接入到微信公众平台了,下面就可以进行开发了。

接收消息

我们同样阅读一下官方文档:
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。

请注意:

  1. 关于重试的消息排重,推荐使用msgid排重
  2. 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息-被动回复消息”
  3. 如果开发者需要对用户消息在5秒内立即做出回应,即使用“发送消息-被动回复消息”接口向用户被动回复消息时,可以在
    公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等API调用形式向用户发送消息,则不受影响)

我们先来尝试着接收一下文本消息。

当我们向测试公众号发送消息时,控制台打印Post:
在这里插入图片描述
说明微信服务器将消息通过Post请求返回给了我们,我们需要做的就是对消息进行处理:

参数描述
ToUserName开发者微信号
FromUserName发送方帐号(一个OpenID)
CreateTime消息创建时间 (整型)
MsgType消息类型,文本为text
Content文本消息内容
MsgId消息id,64位整型

我们导入处理xml的jar包:dom4j-1.6.1.jar。
在WxService类中编写解析方法:

	//处理消息和事件推送
	public static Map<String,String> parseRequest(InputStream in) {
		Map<String, String> map = new HashMap<String, String>();
		//解析XML数据包
		SAXReader reader = new SAXReader();
		try {
			//读取输入流,获取文档对象
			Document document = reader.read(in);
			//获取根结点
			Element root = document.getRootElement();
			//获取根结点的所有子结点
			List<Element> elements  = root.elements();
			for(Element e : elements) {
				map.put(e.getName(),e.getStringValue());
			}
		} catch (DocumentException e) {
			e.printStackTrace();
		}
		return map;
	}

我们将xml数据封装成map集合并返回,此时回到主程序:

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		Map<String, String> reqMap = WxService.parseRequest(request.getInputStream());
		System.out.println(reqMap);
	}

我们来打印一下map集合,先运行项目,然后发送一条消息给测试公众号,运行结果:

{Content=你好, CreateTime=1579419731, ToUserName=gh_44e773fedf89, FromUserName=olvcit_LiCooaDHspeDuj3FY0wCs, MsgType=text, MsgId=22611853278442571}

回复消息

接收到用户发送的消息后,我们就需要对用户进行回复。

当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。

这里同样只讲解文本消息的回复,对于其它类型的消息,回复方式是一样的。

通过官方文档我们了解到,想要回复用户消息,我们只需将一组特定的XML结构返回给服务器即可,看代码:

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		request.setCharacterEncoding("utf8");
		response.setCharacterEncoding("utf8");
		Map<String, String> reqMap = WxService.parseRequest(request.getInputStream());
		System.out.println(reqMap);
		//回复用户
		String respXml = "<xml>\r\n" + 
				"<ToUserName>"+reqMap.get("FromUserName") + "</ToUserName>\r\n" + 
				"<FromUserName>" + reqMap.get("ToUserName") + "</FromUserName>\r\n" + 
				"<CreateTime>" + System.currentTimeMillis() / 1000 + "</CreateTime>\r\n" + 
				"<MsgType><![CDATA[text]]></MsgType>\r\n" + 
				"<Content><![CDATA[测试回复]]></Content>\r\n" + 
				"</xml>";
		PrintWriter out = response.getWriter();
		out.print(respXml);
		out.flush();
		out.close();
	}

实现非常简单,需要注意的是,在XML数据中,千万不要有空格,我在第一次写的时候就发现,不管怎么尝试也实现不了,可乍一看代码都没错,找来找去才发现,是空格影响了程序。

下面测试一下,运行程序,发送消息给测试号:
在这里插入图片描述
这样回复其实是很麻烦的,当然了,我们有更优雅的解决方案,对于每种类型的消息,我们可以提供对应的Bean类,这样在给用户回复消息的时候只需要创建一个Bean对象,然后通过方法将其转换为XML数据,最终响应给服务器,这样我们的处理过程将是对每个对象的处理而不是具体的XML数据。

将对象转换为XML数据的实现,我们可以借助jar包:xstream-1.4.3.jar。

以文本消息为例,首先创建Bean类:

@XStreamAlias("xml")
public class TextMessage extends BaseMessage {
	
	@XStreamAlias("Content")
	private String content;

	public TextMessage(Map<String, String> reqMap,String content) {
		super(reqMap);
		//设置消息类型
		setMessageType("text");
		//发送的文本消息内容
		this.content = content;
	}
	
	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
}

不过为了后续处理方便,这里抽取了一个父类用于存放各种类型消息的公共属性:

@XStreamAlias("xml")
public class BaseMessage {
	
	@XStreamAlias("ToUserName")
	private String toUserName;
	@XStreamAlias("FromUserName")
	private String fromUserName;
	@XStreamAlias("CreateTime")
	private String createTime;
	@XStreamAlias("MsgType")
	private String msgType;
	
	public BaseMessage(Map<String, String> reqMap) {
		//设置发送方和接收方
		this.toUserName = reqMap.get("FromUserName");
		this.fromUserName = reqMap.get("ToUserName");
		this.createTime = String.valueOf(System.currentTimeMillis() / 1000);
	}
	
	public String getToUserName() {
		return toUserName;
	}
	public void setToUserName(String toUserName) {
		this.toUserName = toUserName;
	}
	public String getFromUserName() {
		return fromUserName;
	}
	public void setFromUserName(String fromUserName) {
		this.fromUserName = fromUserName;
	}
	public String getCreateTime() {
		return createTime;
	}
	public void setCreateTime(String createTime) {
		this.createTime = createTime;
	}
	public String getMsgType() {
		return msgType;
	}
	public void setMessageType(String msgType) {
		this.msgType = msgType;
	}
}

如果用过xstream的同学应该能明白这些注解的意思,不明白的同学也不要紧,照着写就行了。

创建好Bean类后,我们编写工具方法:

/**
	 * 用于处理所有的事件和消息回复
	 * @param reqMap
	 * @return
	 */
	public static String getData(Map<String, String> reqMap) {
		BaseMessage msg = null;
		String msgType = reqMap.get("MsgType");
		switch (msgType) {
			case "text":
				//处理文本消息
				msg = dealText(reqMap);
				break;
			case "image":
				break;
			case "voice":
				
				break;
			case "video":
				
				break;
			case "shortvideo":
				
				break;
			case "location":
				
				break;
			case "link":
				
				break;
			default:
				break;
		}
		//将bean对象转换为xml数据
		if(msg != null) {
			return beanToXml(msg);
		}else {
			return null;
		}
	}

	/**
	 * 将bean对象转换为xml数据
	 * @param msg
	 * @return
	 */
	private static String beanToXml(BaseMessage msg) {
		XStream stream = new XStream();
		//添加需要处理注释的类
		stream.processAnnotations(TextMessage.class);
		stream.processAnnotations(ImageMessage.class);
		stream.processAnnotations(MusicMessage.class);
		stream.processAnnotations(NewsMessage.class);
		stream.processAnnotations(VideoMessage.class);
		stream.processAnnotations(VoiceMessage.class);
		String xml = stream.toXML(msg);
		return xml;
	}

	/**
	 * 处理文本消息
	 * @param reqMap
	 * @return
	 */
	private static BaseMessage dealText(Map<String, String> reqMap) {
		TextMessage tMsg = new TextMessage(reqMap,"第二次测试回复");
		return tMsg;
	}

先通过getData方法处理所有消息,这里只处理了文本消息。若用户发送的是文本消息,则通过dealText方法进行处理。该方法将封装一个TextMessage对象用于回复用户,有了消息回复对象后,通过beanToXml方法将该对象转换为XML数据,最后将XML数据响应给服务器。

对于其它类型的消息处理,大家可以自己试着实现一下。

聊天机器人

学会了接收消息和回复消息后,我们就可以实现一个聊天机器人。
这里我们借助聚合数据平台提供的聊天机器人接口:
在这里插入图片描述
点击左上角申请新数据,然后找到聊天机器人进行申请即可,初始赠送100次调用次数,足够我们测试使用了。

使用方法后台都有介绍,这里我们使用最为简便的方式,GET请求,将收到的消息和APPKEY拼到url地址上即可。在这里插入图片描述
这里有参数介绍,通过info和key属性实现拼接,然后请求拼接后的url地址。
下面是返回值参数:
在这里插入图片描述
默认返回的是json数据,所以我们需要借助以下jar包进行json解析:
在这里插入图片描述

/**
	 * 处理文本消息
	 * @param reqMap
	 * @return
	 */
	private static BaseMessage dealText(Map<String, String> reqMap) {
		//接收用户发送的消息
		String msg = reqMap.get("Content");
		//返回聊天内容
		String respMsg = robotChat(msg);
		TextMessage tMsg = new TextMessage(reqMap,respMsg);
		return tMsg;
	}

	/**
	 * 调用机器人接口
	 * @param msg
	 * @return
	 */
	private static String robotChat(String msg) {
		//拼接请求url
		String url = "http://op.juhe.cn/iRobot/index?info=" + msg + "&key=" + APPKEY;
		//请求url
		OkHttpClient client = new OkHttpClient();
		Request request = new Request.Builder().get().url(url).build();
		client.newCall(request).enqueue(new Callback() {
			
			@Override
			public void onResponse(Call call, Response resp) throws IOException {
				if(resp.code() == 200) {
					//响应成功
					String json = resp.body().string();
					//解析json数据
					JSONObject jsonObject = JSONObject.fromObject(json);
					int code = jsonObject.getInt("error_code");
					if(code == 0) {
						//机器人接口访问正常
						str = jsonObject.getJSONObject("result").getString("text");
					}
				}
			}
			
			@Override
			public void onFailure(Call call, IOException e) {
				
			}
		});
		return str;
	}

这里使用了OkHttp网络框架进行网络请求,不了解的同学也不要紧,可以尝试着换成自己会的网络请求方式。

这样我们便将机器人回复的内容作为响应给用户的消息进行传入。

测试结果:
在这里插入图片描述

回复图文消息

关于消息回复,前面已经介绍了文本消息的回复,虽然其它类型的回复方式与其基本相同,但也有一定的区别,这里介绍一下关于图文消息的回复,因为该类型消息的回复是较为复杂的。
我们可以看到很多公众号每天都会推送一些图文消息,比如:
在这里插入图片描述
这是CSDN公众号每天都会推送的图文消息,它包含了图文标题,图文描述和图片信息,点进去的话是关于该描述的一篇文章,我们试着实现一下。

查阅官方文档,关于图文消息的回复,其XML数据需要遵循以下格式:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[news]]></MsgType>
  <ArticleCount>1</ArticleCount>
  <Articles>
    <item>
      <Title><![CDATA[title1]]></Title>
      <Description><![CDATA[description1]]></Description>
      <PicUrl><![CDATA[picurl]]></PicUrl>
      <Url><![CDATA[url]]></Url>
    </item>
  </Articles>
</xml>

它分为两个部分,一是图文消息本身的属性,二是每则图文消息的属性,因为图文消息它可以发送多条,所以我们在前面的基础上先设计一个Bean类:

@XStreamAlias("xml")
public class NewsMessage extends BaseMessage{

	@XStreamAlias("ArticleCount")
	private String atricleCount;
	@XStreamAlias("Articles")
	private List<Article> articles = new ArrayList<Article>();
	
	public String getAtricleCount() {
		return atricleCount;
	}
	public void setAtricleCount(String atricleCount) {
		this.atricleCount = atricleCount;
	}
	public List<Article> getArticles() {
		return articles;
	}
	public void setArticles(List<Article> articles) {
		this.articles = articles;
	}
	
	public NewsMessage(Map<String, String> reqMap, String atricleCount, List<Article> articles) {
		super(reqMap);
		setMessageType("news");
		this.atricleCount = atricleCount;
		this.articles = articles;
	}
}

还需要把每条图文消息单独抽取成一个类:

@XStreamAlias("item")
public class Article {
	
	@XStreamAlias("Title")
	private String title;
	@XStreamAlias("Description")
	private String description;
	@XStreamAlias("PicUrl")
	private String picUrl;
	@XStreamAlias("Url")
	private String url;
	
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	public String getPicUrl() {
		return picUrl;
	}
	public void setPicUrl(String picUrl) {
		this.picUrl = picUrl;
	}
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	
	public Article(String title, String description, String picUrl, String url) {
		super();
		this.title = title;
		this.description = description;
		this.picUrl = picUrl;
		this.url = url;
	}
}

Bean类定义完了,我们来处理功能的逻辑,我暂且这样设计,如果用户发送的是图片,则回复用户图文消息,大家不必跟我一样,可以发挥脑洞,自行设计。

/**
	 * 用于处理所有的事件和消息回复
	 * @param reqMap
	 * @return
	 */
	public static String getData(Map<String, String> reqMap) {
		BaseMessage msg = null;
		String msgType = reqMap.get("MsgType");
		switch (msgType) {
			case "text":
				//处理文本消息
				msg = dealText(reqMap);
				break;
			case "image":
				//如果用户发送的是图片,则回复用户图文消息
				msg = dealImage(reqMap);
				break;
			case "voice":
				
				break;
			case "video":
				
				break;
			case "shortvideo":
				
				break;
			case "location":
				
				break;
			case "link":
				
				break;
			default:
				break;
		}
		//将bean对象转换为xml数据
		if(msg != null) {
			return beanToXml(msg);
		}else {
			return null;
		}
	}

getData方法中image分支需要处理图片消息,我抽取了一个dealImage方法:

	/**
	 * 如果用户发送的是图片,则回复用户图文消息
	 * @param reqMap
	 */
	private static BaseMessage dealImage(Map<String, String> reqMap) {
		List<Article> articles = new ArrayList<Article>();
		articles.add(new Article("标题","15年老程序员自述:8个影响我职业生涯的重要技能", "https://mmbiz.qpic.cn/mmbiz_jpg/Pn4Sm0RsAuianWDokh5pic2LUZuQCxnFRxOUV19Uic1x3aiayowSxP6rb3juBgAfSbE2kqianWk1sRN3b33Vw4LW7jw/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1", "http://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650737643&idx=2&sn=322109799b7e0401957734eb9ac3c803&chksm=bea77a3889d0f32edeeaaa9dc136f9296fd84905d60f7ce6bb8320115838e79fff260a1bf824&scene=0&xtrack=1#rd"));
		NewsMessage nm = new NewsMessage(reqMap, "1", articles);
		return nm;
	}

大家会发现,前面关于消息回复的封装,其好处越来越显现出来了,使得这里的代码变得非常简短。

运行项目,发送一张图片:
在这里插入图片描述

自定义菜单

一个健全的公众号离不开菜单,菜单为用户提供了快捷的功能入口,让用户学习使用公众号的门槛降低。
在这里插入图片描述
这是CSDN公众号的菜单,功能相当丰富啊,我们仿造它做一做。

先来看看官方文档:

  1. 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单
  2. 一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“…”代替
  3. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

这是对菜单的一些介绍,我们重点看如何实现自定义菜单:

接口调用请求说明:
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

要想实现自定义菜单其实非常简单,只要以Post方式请求上面的网址,并携带规范的json数据即可完成,json数据格式如下:

{
     "button":[
     {	
          "type":"click",
          "name":"今日歌曲",
          "key":"V1001_TODAY_MUSIC"
      },
      {
           "name":"菜单",
           "sub_button":[
           {	
               "type":"view",
               "name":"搜索",
               "url":"http://www.soso.com/"
            },
            {
                 "type":"miniprogram",
                 "name":"wxa",
                 "url":"http://mp.weixin.qq.com",
                 "appid":"wx286b93c14bbf93aa",
                 "pagepath":"pages/lunar/index"
             },
            {
               "type":"click",
               "name":"赞一下我们",
               "key":"V1001_GOOD"
            }]
       }]
 }

按照我们实现回复消息的方式,我们同样将生成json数据的逻辑抽取出来,只操作对象而通过方法去转换成json。

微信平台提供的菜单有很多种,这些大家可以自行阅读文档,有按钮菜单,点击按钮后公众号作出点击事件的响应;还有链接菜单,点击菜单后会跳转到指定网址等等,这里我们只实现这两种菜单,其它类型的菜单大家可以自己尝试着写一写。

根据json数据的格式,我们需要先创建一个Button类:

public class Button {
	
	private List<MoreButton> button = new ArrayList<MoreButton>();
	

	public List<MoreButton> getButton() {
		return button;
	}

	public void setButton(List<MoreButton> button) {
		this.button = button;
	}
}

因为菜单种类很多,这里我们可以抽取一个抽象的菜单类:

public abstract class MoreButton {
	
	private String name;

	public MoreButton(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

然后定义两个具体菜单实现:

public class ClickButton extends MoreButton {

	private String type = "click";
	private String key;
	
	public ClickButton(String name, String key) {
		super(name);
		this.key = key;
	}
	
	public String getType() {
		return type;
	}
	public void setType(String type) {
		this.type = type;
	}
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
}
public class ViewButton extends MoreButton {

	private String type = "view";
	private String url;
	
	public ViewButton(String name, String url) {
		super(name);
		this.url = url;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public ViewButton(String name) {
		super(name);
	}
}

这些菜单类的属性一定要按照json数据的格式来定义,还需要定义一个子菜单类:

public class SubButton extends MoreButton {
	
	private List<MoreButton> sub_button = new ArrayList<MoreButton>();

	public SubButton(String name) {
		super(name);
	}
	
	public List<MoreButton> getSub_button() {
		return sub_button;
	}

	public void setSub_button(List<MoreButton> sub_button) {
		this.sub_button = sub_button;
	}
}

这样Bean类就建立完成了。
因为菜单的实现和服务器没有关联,这里我们脱离服务器进行开发,创建一个main函数进行测试:

public class DiyMenu {
	
	public static void main(String[] args) {
		//创建菜单对象
		Button btn = new Button();
		//创建第一个一级菜单的子菜单
		SubButton sButton = new SubButton("联系我们");
		sButton.getSub_button().add(new ViewButton("一起学AI","https://edu.csdn.net/topic/ai30?utm_source=csdncd"));
		sButton.getSub_button().add(new ViewButton("商务合作","https://mp.weixin.qq.com/s/-aP6f0efBEMFcyEQZITT1g"));
		sButton.getSub_button().add(new ViewButton("投稿须知","https://mp.weixin.qq.com/s/M1eD8KkOTKQEhR0NkqPpVQ"));
		sButton.getSub_button().add(new ViewButton("转载须知","https://mp.weixin.qq.com/s/rywCAd1U1zbzZr_yo3G2ig"));
		sButton.getSub_button().add(new ViewButton("开源|快应用|小程序|loT","https://mp.weixin.qq.com/mp/homepage?__biz=MjM5MjAwODM4MA==&hid=7&sn=6804bfb21efd6e4b1fc9499cb56fd612&scene=18"));
		btn.getButton().add(sButton);
		//创建第二个一级菜单
		btn.getButton().add(new ClickButton("精选栏目", "1"));
		//创建第三个一级菜单
		btn.getButton().add(new ClickButton("CSDN", "1"));
		//将自定义菜单对象转为json数据
		JSONObject jsonObject = JSONObject.fromObject(btn);
		String json = jsonObject.toString();
		
		//发送Post请求
		OkHttpClient client = new OkHttpClient();
		MediaType mediaType = MediaType.parse("application/json;charset=UTF-8");
		RequestBody requestBody = RequestBody.create(mediaType, json);
		//请求地址
		String url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=" + WxService.getTokenIfExpired();
		Request request = new Request.Builder().post(requestBody).url(url).build();
		client.newCall(request).enqueue(new Callback() {
			
			@Override
			public void onResponse(Call call, Response resp) throws IOException {
				String result = resp.body().string();
				System.out.println(result);
			}
			
			@Override
			public void onFailure(Call call, IOException e) {
			}
		});
	}
}

在进行Post请求的url地址中涉及到了一个ACCESS_TOKEN,从官方文档可以得知:

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

所以,我们需要获取ACCESS_TOKEN,因为该Token的有效期只有2个小时,而且获取次数是有限制的,所以我们不能每次运行都去获取它,而应该在失效或者第一次获取的时候才去申请它。

获取Token逻辑代码:

/**
	 * 获取Token
	 * @return
	 */
	private static void getToken() {
		OkHttpClient client = new OkHttpClient();
		Request request = new Request.Builder().get().url("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+APPID+"&secret="+APPSECRET).build();
		try {
			Response response = client.newCall(request).execute();
			String json = response.body().string();
			//将其封装成对象
			JSONObject jsonObject = JSONObject.fromObject(json);
			String accessToken = jsonObject.getString("access_token");
			String expiresIn = jsonObject.getString("expires_in");
			aToken = new AccessToken(accessToken, expiresIn);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}
	
	/**
	 * 获取AccessToken,若过期,则重新获取
	 */
	public static String getTokenIfExpired() {
		if(aToken == null || aToken.isExpire()) {
			//获取AccessToken
			getToken();
		}
		return aToken.getAccessToken();
	}

这里采用了OkHttp的同步请求方式,因为同步请求会阻塞线程,直到token获取成功,所以为了避免网络延迟所产生的空指针异常,这里可以采用同步请求。
为了保存Token,需定义一个Bean类:

public class AccessToken {
	
	private String accessToken;
	private long expireTime;
	
	public AccessToken(String accessToken, String expireIn) {
		this.accessToken = accessToken;
		this.expireTime = System.currentTimeMillis() + Integer.parseInt(expireIn) * 1000;
	}
	public String getAccessToken() {
		return accessToken;
	}
	public void setAccessToken(String accessToken) {
		this.accessToken = accessToken;
	}
	public long getExpireTime() {
		return expireTime;
	}
	public void setExpireTime(long expireTime) {
		this.expireTime = expireTime;
	}
	
	/**
	 * 判断AccessToken是否过期
	 * @return
	 */
	public boolean isExpire() {
		return System.currentTimeMillis() > expireTime;
	}
}

这里我们直接在获取token的时候便计算出过期时间,只要当前时间不大于过期时间,就表示它没有过期。

此时我们直接运行main函数,观察测试公众号变化:
在这里插入图片描述
如果菜单没有出现,就先取消关注,然后重新关注一下测试公众号,菜单就会刷新出来了。

菜单响应

关于菜单的响应,因为非常简单,而且涉及到前面的代码架构,所以这里不作讲解,大家可以在文末下载源代码自己看一看。

模板消息

下面介绍一下模板消息,那么什么是模板消息呢?看一幅图大家就明白了:

在这里插入图片描述
这就是公众号的模板消息,主要用于通知用户一些信息,微信平台对于模板消息的把控非常严格,大家在进行开发的时候也千万不要去触碰平台的红线。

设置所属行业

从文档中得知,要想发送模板消息,必须设置公众号所属的行业,设置方式如下:

接口调用请求说明
http请求方式: POST https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=ACCESS_TOKEN
POST数据说明
POST数据示例如下:
{
“industry_id1”:“1”,
“industry_id2”:“4”
}

这里的id也不是随便填的,必须符合如下表格:
在这里插入图片描述
行业编号比较多,就不全部贴出来了。
设置行业非常简单,直接Post请求指定url,并传入参数即可,参数包含行业编号:

	//设置行业
	public static void setIndustry() {
		String url = "https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=" + WxService.getTokenIfExpired();
		String json = "{\r\n" + 
				"    \"industry_id1\":\"1\",\r\n" + 
				"    \"industry_id2\":\"4\"\r\n" + 
				"}";
		//发送Post请求
		OkHttpClient client = new OkHttpClient();
		MediaType mediaType = MediaType.parse("application/json;charset=UTF-8");
		RequestBody requestBody = RequestBody.create(mediaType, json);
		Request request = new Request.Builder().post(requestBody).url(url).build();
		client.newCall(request).enqueue(new Callback() {
			
			@Override
			public void onResponse(Call call, Response resp) throws IOException {
				String result = resp.body().string();
				System.out.println(result);
			}
				
			@Override
			public void onFailure(Call call, IOException e) {
			}
		});
	}

这里有一点自己没有考虑好,我也没有料到后面网络请求的地方这么多,本来应该把网络请求的逻辑都抽取出来的,不过因为框架非常简便,也没有几句代码,所以我就不抽取了,大家可以自己优化一下项目代码。

这样行业就设置好了,我们同样可以获取设置的行业信息,也为了测试一下是否设置成功了:

/**
	 * 获取设置的行业信息
	 */
	public static void getIndustry() {
		OkHttpClient client = new OkHttpClient();
		String url = "https://api.weixin.qq.com/cgi-bin/template/get_industry?access_token=" + WxService.getTokenIfExpired();
		Request request = new Request.Builder().get().url(url).build();
		client.newCall(request).enqueue(new Callback() {
			
			@Override
			public void onResponse(Call call, Response resp) throws IOException {
				String result = resp.body().string();
				System.out.println(result);
			}
			
			@Override
			public void onFailure(Call call, IOException e) {
			}
		});
	}

运行结果:

{"primary_industry":{"first_class":"IT科技","second_class":"互联网|电子商务"},"secondary_industry":{"first_class":"IT科技","second_class":"电子技术"}}

发送模板消息

在发送模板消息之前,除了要设置所属行业外,还需要添加消息模板,在测试号后台找到:
在这里插入图片描述
点击新增测试模板,弹出模板设置窗口:
在这里插入图片描述
需要注意的是,这里的模板标题和模板内容不能随便写,前面也提到了,微信平台对于模板消息的管控非常严格,它对模板消息的内容有一定的约束,具体可以下载模板示例查看:
在这里插入图片描述
这里我们以企业审核结果通知为例:
在这里插入图片描述
我们首先来到测试公众号的后台,将标题和内容复制进去:
在这里插入图片描述
然后点击提交,将模板id复制下来,后面要用:
在这里插入图片描述
发送模板消息同样非常简单,只需请求下面的地址即可:

http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

携带的Post数据需要符合模板消息的数据结构定义,下面是一个参考的json数据:

 {
           "touser":"OPENID",
           "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
           "url":"http://weixin.qq.com/download",  
           "miniprogram":{
             "appid":"xiaochengxuappid12345",
             "pagepath":"index?foo=bar"
           },          
           "data":{
                   "first": {
                       "value":"恭喜你购买成功!",
                       "color":"#173177"
                   },
                   "keyword1":{
                       "value":"巧克力",
                       "color":"#173177"
                   },
                   "keyword2": {
                       "value":"39.8元",
                       "color":"#173177"
                   },
                   "keyword3": {
                       "value":"2014年9月22日",
                       "color":"#173177"
                   },
                   "remark":{
                       "value":"欢迎再次购买!",
                       "color":"#173177"
                   }
           }
       }

这里的url和miniprogram字段都不是必须的。下面是参数的说明:
在这里插入图片描述
下面来实现一下,我们先准备好自己的json数据:

{
           "touser":"olvcit_LiCooaDHspeDuj3FY0wCs",
           "template_id":"O7098ghaIZ8i_O7MKmCUL_KQ-TbWraZjTQMfwCWhoEc",   
           "data":{
                   "first": {
                       "value":"您的企业资料审核通过",
                       "color":"#173177"
                   },
                   "keyword1":{
                       "value":"11****3729",
                       "color":"#173177"
                   },
                   "keyword2": {
                       "value":"肯德基",
                       "color":"#173177"
                   },
                   "keyword3": {
                       "value":"资料齐全",
                       "color":"#173177"
                   },
                   "remark":{
                       "value":"马上开始使用吧",
                       "color":"#173177"
                   }
           }
       }

touser可以在这里找到:
在这里插入图片描述
template_id就是模板id,粘贴进去就行了。
这里的键值要跟模板消息定义的一致,否则就会出错。

下面是代码实现:

/**
	 * 发送模板消息
	 */
	public static void sendTemplateMessage() {
		String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + WxService.getTokenIfExpired();
		String json = "{\r\n" + 
				"           \"touser\":\"olvcit_LiCooaDHspeDuj3FY0wCs\",\r\n" + 
				"           \"template_id\":\"O7098ghaIZ8i_O7MKmCUL_KQ-TbWraZjTQMfwCWhoEc\",   \r\n" + 
				"           \"data\":{\r\n" + 
				"                   \"first\": {\r\n" + 
				"                       \"value\":\"您的企业资料审核通过\",\r\n" + 
				"                       \"color\":\"#173177\"\r\n" + 
				"                   },\r\n" + 
				"                   \"keyword1\":{\r\n" + 
				"                       \"value\":\"11****3729\",\r\n" + 
				"                       \"color\":\"#173177\"\r\n" + 
				"                   },\r\n" + 
				"                   \"keyword2\": {\r\n" + 
				"                       \"value\":\"肯德基\",\r\n" + 
				"                       \"color\":\"#173177\"\r\n" + 
				"                   },\r\n" + 
				"                   \"keyword3\": {\r\n" + 
				"                       \"value\":\"资料齐全\",\r\n" + 
				"                       \"color\":\"#173177\"\r\n" + 
				"                   },\r\n" + 
				"                   \"remark\":{\r\n" + 
				"                       \"value\":\"马上开始使用吧\",\r\n" + 
				"                       \"color\":\"#173177\"\r\n" + 
				"                   }\r\n" + 
				"           }\r\n" + 
				"       }";
		//发送Post请求
		OkHttpClient client = new OkHttpClient();
		MediaType mediaType = MediaType.parse("application/json;charset=UTF-8");
		RequestBody requestBody = RequestBody.create(mediaType, json);
		Request request = new Request.Builder().post(requestBody).url(url).build();
		client.newCall(request).enqueue(new Callback() {
					
			@Override
			public void onResponse(Call call, Response resp) throws IOException {
				String result = resp.body().string();
				System.out.println(result);
			}
						
			@Override
			public void onFailure(Call call, IOException e) {
			}
		});
	}

运行该方法:
在这里插入图片描述

生成带参数的二维码

为了满足用户渠道推广分析和用户帐号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。

查阅文档得知,要想生成带参数的二维码,需要进行如下步骤:

  1. 创建二维码ticket
  2. 请求二维码

二维码又分为临时二维码和永久二维码,不过永久二维码有数量限制,这里以临时二维码为例,首先获取临时二维码的ticket:

临时二维码请求说明
http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST数据格式:json POST数据例子:{“expire_seconds”: 604800, “action_name”: “QR_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数:{“expire_seconds”: 604800, “action_name”: “QR_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}}

操作步骤如上所述,代码实现如下:

/**
	 * 获取带参数二维码的ticket
	 * @return
	 */
	public static String getQrCodeTicket() {
		String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + WxService.getTokenIfExpired();
		String json = "{\"expire_seconds\": 604800, \"action_name\": \"QR_SCENE\", \"action_info\": {\"scene\": {\"scene_id\": 123}}}";
		//发送Post请求
		OkHttpClient client = new OkHttpClient();
		MediaType mediaType = MediaType.parse("application/json;charset=UTF-8");
		RequestBody requestBody = RequestBody.create(mediaType, json);
		Request request = new Request.Builder().post(requestBody).url(url).build();
		try {
			Response response = client.newCall(request).execute();
			String result = response.body().string();
			JSONObject jsonObject = JSONObject.fromObject(result);
			String ticket = jsonObject.getString("ticket");
			return ticket;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

获取到ticket后,就可以请求二维码了:

HTTP GET请求(请使用https协议)https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 提醒:TICKET记得进行UrlEncode

代码实现如下:

/**
	 * 生成带参数的二维码
	 */
	public static void createQrCode() {
		String ticket = getQrCodeTicket();
		String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
		OkHttpClient client = new OkHttpClient();
		Request request = new Request.Builder().get().url(url).build();
		try {
			Response response = client.newCall(request).execute();
			InputStream in = response.body().byteStream();
			//保存图片
			FileOutputStream out = new FileOutputStream(new File("qrcode.png"));
			//保存图片
			byte[] buffer = new byte[1024];
			int len = -1;
			while((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}
			in.close();
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

这里对二维码图片进行了保存,运行该方法:
在这里插入图片描述
用微信扫描该二维码,未关注的用户会提示关注该公众号,并推送消息;已经关注的用户会直接进入会话界面,并推送消息。

获取用户基本信息

在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的。对于不同公众号,同一用户的openid不同)。公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称、头像、性别、所在城市、语言和关注时间。

获取用户信息也非常简单,请求下面的地址即可:

接口调用请求说明 http请求方式: GET https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

因为获取信息需要用户的OpenID,而OpenID只有关注了公众号后才能获取,所以你只能够获取关注了你的用户信息。

实现很简单,就不讲解了,直接看代码:

/**
	 * 获取用户信息
	 */
	public static String getUserInfo(String openId) {
		String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + WxService.getTokenIfExpired() + "&openid=" + openId + "&lang=zh_CN";
		OkHttpClient client = new OkHttpClient();
		Request request = new Request.Builder().get().url(url).build();
		try {
			Response response = client.newCall(request).execute();
			String result = response.body().string();
			System.out.println(result);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

运行结果:

{"subscribe":1,"openid":"olvcit_LiCooaDHspeDuj3FY0wCs","nickname":"Y","sex":1,"language":"zh_CN","city":"上饶","province":"江西","country":"中国","headimgurl":"http:\/\/thirdwx.qlogo.cn\/mmopen\/nTKjHRiceOUowEIPTD97uWpeUkFHsVpibRENcEnT8j8KFnxzuPWy7eJbrxiaF53JvChCO2L0ZBJicR749d1HuPGb0pal7VEVP6cic\/132","subscribe_time":1579420103,"remark":"","groupid":0,"tagid_list":[],"subscribe_scene":"ADD_SCENE_QR_CODE","qr_scene":0,"qr_scene_str":""}

源代码

文中项目源码已上传至Github,项目地址:

https://github.com/blizzawang/WeChatDevelopment

学习了本篇文章的一些基础开发之后,相信大家已经有能力自己阅读文档进行开发学习了,所以后续的公众号开发就由大家自己去探索了。

最后再次祝大家新春快乐!

微信搜索【码视界】或者扫描下方二维码关注我的微信公众号,更多精彩等着你!
~wangweijun CSDN认证博客专家 Spring Java
人生如逆旅,我亦是行人。
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 39.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值