各位读者朋友们好,今天是除夕,在这里先祝大家新年快乐。这篇文章其实是一个星期前写的,一直在电脑里放着,赶上今天的好日子,机缘巧合的情况下被我重新发现,于是就发布出来了。
文章主题
这篇文章的主题是微信公众号开发,我在去年也开通了自己的微信公众号,不过没有很用心地去做,然后空闲时间自己也去了解了一下微信公众号的开发, 这里便讲讲自己的所学。
开发环境的搭建
关于微信公众号,这里我不做过多介绍,公众号分为服务号和订阅号,不过一般大家的公众号应该都是订阅号。
先来搭建一下公众号开发的环境。
为了避免我有推广的嫌疑,服务器的搭建我就不介绍了,搭建好后的域名为:http://diyweixin.free.idcfengye.com,后面的开发都基于此。
这样开发环境就搭建完成了,我们可以测试一下。
打开ecplise,创建一个动态的web工程:
然后新建一个index.jsp文件,在页面上写一个"Hello World!"。
当我们启动该项目后,就可以通过服务器的域名进行访问了,访问的地址应为:``http://diyweixin.free.idcfengye.com/wechat/index.jsp
大家可以拿手机或者别的设备尝试一下,看看不在同一个局域网下能否成功访问。
接入微信公众平台
开发环境搭建好后,我们需要接入到微信公众平台进行公众号开发。
大家可以自己阅读公众号开发文档,文档中介绍,接入需要三个步骤:
我们一一来实现。
填写服务器配置
通过公众号开发,能够大大丰富自己公众号的功能,不过对于不同的公众号,微信平台提供的权限不大相同。对于普通的订阅号,权限相对较少一些。大家可以在公众号后台的接口权限中进行查看:
对于这样的情况,微信团队当然有所考虑,为了降低开发者的学习门槛,微信平台提供了测试号供开发者进行学习,那么下面就来申请一下测试号。
打开公众号开发文档,找到开始开发下面的接口测试号申请:
点击进入申请系统,然后
此时进入到该页面:
页面下方有测试号的二维码,可以关注测试后续功能:
这里需要填写两个信息:
- URL:这是开发者用来接收微信消息和事件的接口URL
- 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参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
- 将token、timestamp、nonce三个参数进行字典序排序
- 将三个参数字符串拼接成一个字符串进行sha1加密
- 开发者获得加密后的字符串可与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上。
请注意:
- 关于重试的消息排重,推荐使用msgid排重
- 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息-被动回复消息”
- 如果开发者需要对用户消息在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公众号的菜单,功能相当丰富啊,我们仿造它做一做。
先来看看官方文档:
- 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单
- 一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“…”代替
- 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号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) {
}
});
}
运行该方法:
生成带参数的二维码
为了满足用户渠道推广分析和用户帐号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。
查阅文档得知,要想生成带参数的二维码,需要进行如下步骤:
- 创建二维码ticket
- 请求二维码
二维码又分为临时二维码和永久二维码,不过永久二维码有数量限制,这里以临时二维码为例,首先获取临时二维码的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
学习了本篇文章的一些基础开发之后,相信大家已经有能力自己阅读文档进行开发学习了,所以后续的公众号开发就由大家自己去探索了。
最后再次祝大家新春快乐!
转载:https://blog.csdn.net/qq_42453117/article/details/104040111