手把手教你微信公众号开发

文章主题

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

开发环境的搭建

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

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

这样开发环境就搭建完成了,我们可以测试一下。 打开ecplise,创建一个动态的web工程:

然后新建一个index.jsp文件,在页面上写一个"Hello World!"。 当我们启动该项目后,就可以通过服务器的域名进行访问了,访问的地址应为:``diyweixin.free.idcfengye.com/wechat/inde... 大家可以拿手机或者别的设备尝试一下,看看不在同一个局域网下能否成功访问。

接入微信公众平台

开发环境搭建好后,我们需要接入到微信公众平台进行公众号开发。 大家可以自己阅读公众号开发文档,文档中介绍,接入需要三个步骤:

我们一一来实现。

填写服务器配置

通过公众号开发,能够大大丰富自己公众号的功能,不过对于不同的公众号,微信平台提供的权限不大相同。对于普通的订阅号,权限相对较少一些。大家可以在公众号后台的接口权限中进行查看:

对于这样的情况,微信团队当然有所考虑,为了降低开发者的学习门槛,微信平台提供了测试号供开发者进行学习,那么下面就来申请一下测试号。 打开公众号开发文档,找到开始开发下面的接口测试号申请:

点击进入申请系统,然后

此时进入到该页面:

页面下方有测试号的二维码,可以关注测试后续功能:

这里需要填写两个信息:

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

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

java 复制代码
@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加密,我们单独写一个类,用于实现工具方法:

java 复制代码
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进制数:

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

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

最后返回加密结果。

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

java 复制代码
@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类中编写解析方法:

java 复制代码
	//处理消息和事件推送
	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集合并返回,此时回到主程序:

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

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

c 复制代码
{Content=你好, CreateTime=1579419731, ToUserName=gh_44e773fedf89, FromUserName=olvcit_LiCooaDHspeDuj3FY0wCs, MsgType=text, MsgId=22611853278442571}

回复消息

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

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

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

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

java 复制代码
	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类:

java 复制代码
@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;
	}
}

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

java 复制代码
@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类后,我们编写工具方法:

java 复制代码
/**
	 * 用于处理所有的事件和消息回复
	 * @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解析:

java 复制代码
/**
	 * 处理文本消息
	 * @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网络框架进行网络请求,不了解的同学也不要紧,可以尝试着换成自己会的网络请求方式。

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

测试结果:

回复图文消息

关于消息回复,前面已经介绍了文本消息的回复,虽然其它类型的回复方式与其基本相同,但也有一定的区别,这里介绍一下关于图文消息的回复,因为该类型消息的回复是较为复杂的。 我们可以看到很多公众号每天都会推送一些图文消息,比如:

这是掘金公众号每天都会推送的图文消息,它包含了图文标题,图文描述和图片信息,点进去的话是关于该描述的一篇文章,我们试着实现一下。

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

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类:

java 复制代码
@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;
	}
}

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

java 复制代码
@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类定义完了,我们来处理功能的逻辑,我暂且这样设计,如果用户发送的是图片,则回复用户图文消息,大家不必跟我一样,可以发挥脑洞,自行设计。

java 复制代码
/**
	 * 用于处理所有的事件和消息回复
	 * @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方法:

java 复制代码
	/**
	 * 如果用户发送的是图片,则回复用户图文消息
	 * @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;
	}

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

运行项目,发送一张图片:

自定义菜单

一个健全的公众号离不开菜单,菜单为用户提供了快捷的功能入口,让用户学习使用公众号的门槛降低。

这是掘金公众号的菜单,功能相当丰富啊,我们仿造它做一做。

先来看看官方文档:

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

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

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

要想实现自定义菜单其实非常简单,只要以Post方式请求上面的网址,并携带规范的json数据即可完成,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类:

java 复制代码
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;
	}
}

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

java 复制代码
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;
	}
}

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

java 复制代码
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;
	}
}
java 复制代码
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数据的格式来定义,还需要定义一个子菜单类:

java 复制代码
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函数进行测试:

java 复制代码
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.juejin.net/topic/ai30?utm_source=juejincd"));
		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("JUEJIN", "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逻辑代码:

java 复制代码
/**
	 * 获取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类:

java 复制代码
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 api.weixin.qq.com/cgi-bin/tem... POST数据说明 POST数据示例如下: { "industry_id1":"1", "industry_id2":"4" }

这里的id也不是随便填的,必须符合如下表格:

行业编号比较多,就不全部贴出来了。 设置行业非常简单,直接Post请求指定url,并传入参数即可,参数包含行业编号:

java 复制代码
	//设置行业
	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) {
			}
		});
	}

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

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

java 复制代码
/**
	 * 获取设置的行业信息
	 */
	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) {
			}
		});
	}

运行结果:

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

发送模板消息

在发送模板消息之前,除了要设置所属行业外,还需要添加消息模板,在测试号后台找到:

点击新增测试模板,弹出模板设置窗口:

需要注意的是,这里的模板标题和模板内容不能随便写,前面也提到了,微信平台对于模板消息的管控非常严格,它对模板消息的内容有一定的约束,具体可以下载模板示例查看:

这里我们以企业审核结果通知为例:

我们首先来到测试公众号的后台,将标题和内容复制进去:

然后点击提交,将模板id复制下来,后面要用:

发送模板消息同样非常简单,只需请求下面的地址即可:

http请求方式: POST api.weixin.qq.com/cgi-bin/mes...

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

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数据:

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,粘贴进去就行了。 这里的键值要跟模板消息定义的一致,否则就会出错。

下面是代码实现:

java 复制代码
/**
	 * 发送模板消息
	 */
	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: api.weixin.qq.com/cgi-bin/qrc... 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"}}}

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

java 复制代码
/**
	 * 获取带参数二维码的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协议)mp.weixin.qq.com/cgi-bin/sho... 提醒:TICKET记得进行UrlEncode

代码实现如下:

java 复制代码
/**
	 * 生成带参数的二维码
	 */
	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 api.weixin.qq.com/cgi-bin/use...

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

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

java 复制代码
/**
	 * 获取用户信息
	 */
	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;
	}

运行结果:

c 复制代码
{"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,项目地址:

github.com/blizzawang/...

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

相关推荐
他日若遂凌云志2 小时前
深入剖析 Fantasy 框架的消息设计与序列化机制:协同架构下的高效转换与场景适配
后端
快手技术2 小时前
快手Klear-Reasoner登顶8B模型榜首,GPPO算法双效强化稳定性与探索能力!
后端
二闹2 小时前
三个注解,到底该用哪一个?别再傻傻分不清了!
后端
用户49055816081252 小时前
当控制面更新一条 ACL 规则时,如何更新给数据面
后端
林太白2 小时前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端
码事漫谈2 小时前
VS Code 终端完全指南
后端
该用户已不存在3 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端
怀刃3 小时前
内存监控对应解决方案
后端
码事漫谈3 小时前
VS Code Copilot 内联聊天与提示词技巧指南
后端
Moonbit3 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞 (上):编译前端实现
后端·算法·编程语言