微信公众账号开发教程
一、第1篇-引言
本文转载来自柳峰老师的博客,在这里非常感谢柳峰老师的分享和贡献!
内容方面,大概会涉及到:
1)前沿知识:微信公众帐号的分类、两种模式各自的特点和区别、开发模式的配置使用等;
2)API中各类消息的使用(我已经对api进行封装并打成了jar包,到时候会考虑分享出来);
3)微信公众帐号开发中的小技巧(如换行、通过代码发送表情、屏幕飘雪花、表情的接收识别、在Android和iOS上表现不一致等等);
4)与业务系统对接的方法(链接、短信等,除了技术讲解还会做一定的分析对比);
5)微信公众平台上常见功能的开发(如像小黄鸡那样的人机对话、天气预报、精确的定位及百度地图的使用、音乐搜索、语音识别解析等)
二、第2篇-微信公众帐号的类型
在微信公众平台开发前首先需要注册微信公众号,微信公众账号类型分为以下几种:
A、订阅号:主要偏于为用户传达咨询,认证前后每天可群发一条消息。可以适用于个人和企业。
B、服务号:主要偏于服务交互(类似银行、114、提供服务查询),认证前后每月可群发4条消息。适用于企业。
C、企业号:主要用于公司内部通讯使用,要先有成员通讯信息验证才可以关注成功企业号。适用于企业。
D、小程序:适合有服务内容的企业和组织注册。
注:注册公众号时,微信号必须绑定本人银行卡
三、第3篇-开发模式启用及接口配置
编辑模式与开发模式
编辑模式:主要针对非编程人员及信息发布类公众帐号使用。开启该模式后,可以方便地通过界面配置“自定义菜单”和“自动回复的消息”。
开发模式:主要针对具备开发能力的人使用。开启该模式后,能够使用微信公众平台开放的接口,通过编程方式实现自定义菜单的创建、用户消息的接收/处理/响应。这种模式更加灵活,建议有开发能力的公司或个人都采用该模式。
启用开发模式(上)
微信公众帐号注册完成后,默认开启的是编辑模式。那么该如何开启开发模式呢?操作步骤如下:
1)登录公众号,进入公众平台页面,选择“开发”--“基本配置”,进入如下图的页面,点击“我同意”成为开发者。
点击同意成为开发者之后,跳转到如下页面:
2)修改服务器配置,启用开发模式
成为开发者之后,点击“修改配置”按钮,填写服务器地址(URL,URL指的是能够接收处理微信服务器发送的GET/POST请求的地址,并且是已经存在的,现在就能够在浏览器访问到的地址,这就要求我们先把公众帐号后台处理程序开发好(至少应该完成了对GET请求的处理)并部署在公网服务器上。另外,这里的URL必须以http或https开头,分别支持80端口和443端口。)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性。两个token要保持一致。)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下图所示:
上面写的很清楚,其实你只要能理解上面在说什么就OK了,至于怎么编写相关代码,请继续往下看。
创建公众帐号后台接口程序
创建一个JavaWeb工程,并新建一个能够处理请求的Servlet,命名任意,我在这里将其命名为org.liufeng.course.servlet.CoreServlet,代码如下:
[java] view plain copy
- package org.liufeng.course.servlet;
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.liufeng.course.util.SignUtil;
- /**
- * 核心请求处理类
- *
- * @author liufeng
- * @date 2013-05-18
- */
- public class CoreServlet extends HttpServlet {
- private static final long serialVersionUID = 4440739483644821986L;
- /**
- * 确认请求来自微信服务器
- */
- public 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");
- PrintWriter out = response.getWriter();
- // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
- if (SignUtil.checkSignature(signature, timestamp, nonce)) {
- out.print(echostr);
- }
- out.close();
- out = null;
- }
- /**
- * 处理微信服务器发来的消息
- */
- public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // TODO 消息的接收、处理、响应
- }
- }
可以看到,代码中只完成了doGet方法,它的作用正是确认请求是否来自于微信服务器;而doPost方法不是我们这次要讲的内容,并且完成接口配置也不需要管doPost方法,就先空在那里。
在doGet方法中调用了org.liufeng.course.util.SignUtil.checkSignature方法,SignUtil.Java的实现如下:
[java] view plain copy
- package org.liufeng.course.util;
- import java.security.MessageDigest;
- import java.security.NoSuchAlgorithmException;
- import java.util.Arrays;
- /**
- * 请求校验工具类
- *
- * @author liufeng
- * @date 2013-05-18
- */
- public class SignUtil {
- // 与接口配置信息中的Token要一致
- private static String token = "weixinCourse";
- /**
- * 验证签名
- *
- * @param signature
- * @param timestamp
- * @param nonce
- * @return
- */
- public static boolean checkSignature(String signature, String timestamp, String nonce) {
- String[] arr = new String[] { token, timestamp, nonce };
- // 将token、timestamp、nonce三个参数进行字典序排序
- Arrays.sort(arr);
- StringBuilder content = new StringBuilder();
- for (int i = 0; i < arr.length; i++) {
- content.append(arr[i]);
- }
- MessageDigest md = null;
- String tmpStr = null;
- try {
- md = MessageDigest.getInstance("SHA-1");
- // 将三个参数字符串拼接成一个字符串进行sha1加密
- byte[] digest = md.digest(content.toString().getBytes());
- tmpStr = byteToStr(digest);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- }
- content = null;
- // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
- return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
- }
- /**
- * 将字节数组转换为十六进制字符串
- *
- * @param byteArray
- * @return
- */
- private static String byteToStr(byte[] byteArray) {
- String strDigest = "";
- for (int i = 0; i < byteArray.length; i++) {
- strDigest += byteToHexStr(byteArray[i]);
- }
- return strDigest;
- }
- /**
- * 将字节转换为十六进制字符串
- *
- * @param mByte
- * @return
- */
- private static String byteToHexStr(byte mByte) {
- char[] Digit = { ‘0‘, ‘1‘, ‘2‘, ‘3‘, ‘4‘, ‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘, ‘A‘, ‘B‘, ‘C‘, ‘D‘, ‘E‘, ‘F‘ };
- char[] tempArr = new char[2];
- tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
- tempArr[1] = Digit[mByte & 0X0F];
- String s = new String(tempArr);
- return s;
- }
- }
这里唯一需要注意的就是SignUtil类中的成员变量token,这里赋予什么值,在接口配置信息中的Token就要填写什么值,两边保持一致即可,没有其他要求,建议用项目名称、公司名称缩写等,我在这里用的是项目名称weixinCourse。
最后再来看一下web.xml中,CoreServlet是怎么配置的,web.xml中的配置代码如下:
[html] view plain copy
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
- http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
- <servlet>
- <servlet-name>coreServlet</servlet-name>
- <servlet-class>
- org.liufeng.course.servlet.CoreServlet
- </servlet-class>
- </servlet>
- <!-- url-pattern中配置的/coreServlet用于指定该Servlet的访问路径 -->
- <servlet-mapping>
- <servlet-name>coreServlet</servlet-name>
- <url-pattern>/coreServlet</url-pattern>
- </servlet-mapping>
- <welcome-file-list>
- <welcome-file>index.jsp</welcome-file>
- </welcome-file-list>
- </web-app>
到这里,所有编码都完成了,就是这么简单。接下来就是将工程发布到公网服务器上,如果没有公网服务器环境,可以去了解下花生壳。发布到服务器上后,我们在浏览器里访问CoreServlet,如果看到如下界面就表示我们的代码没有问题:
啊,代码都报空指针异常了还说证明没问题?那当然了,因为直接在地址栏访问coreServlet,就相当于提交的是GET请求,而我们什么参数都没有传,在验证的时候当然会报空指针异常。
接下来,把coreServlet的访问路径拷贝下来,再回到微信公众平台的接入配置信息界面,将coreServlet的访问路径粘贴到URL中,并将SignUtil类中指定的token值weixinCourse填入到Token中,填写后的结果如下图所示:
接着点击“提交”,如果程序写的没问题,并且URL、Token都填写正确,可以在页面最上方看到“提交成功”的提示。
启用开发模式(下)
这个时候就已经成为开发者了,百般周折啊,哈哈,到这里还没有完哦,还有最后一步工作就是将开发模式开启。点击右上角的“启用”按钮,如下图所示:
提示“操作成功”,即表示已成功开启开发模式。
到这里,接口配置、开发模式的开启就都完成了,本章节的内容也就讲到这里。接下来要章节要讲的就是如何接收、处理、响应由微信服务器转发的用户发送给公众帐号的消息,也就是完成CoreServlet中doPost方法的编写。
四、第4篇-消息及消息处理工具的封装
工欲善其事必先利其器!本篇内容主要讲解如何将微信公众平台定义的消息及消息相关的操作封装成工具类,方面后期的使用。这里需要明确的是消息其实是由用户发给你的公众帐号的,消息先被微信平台接收到,然后微信平台会将该消息转给你在开发模式接口配置中指定的URL地址。
微信公众平台消息接口
要接收微信平台发送的消息,我们需要先熟悉微信公众平台API中消息接口部分,点此进入,点击后将进入到消息接口指南部分,如下图所示:
在上图左侧可以看到微信公众平台目前开放的接口有三种:消息接口、通用接口和自定义菜单接口。通用接口和自定义菜单接口只有拿到内测资格才能调用,而内测资格的申请也已经关闭了,我们只有期待将来某一天微信会对大众用户开放吧,所以没有内测资格的用户就不要再浪费时间在这两个接口上,只需要用好消息接口就可以了。
消息推送和消息回复
下面将主要介绍消息接口。对于消息的接收、响应我们只需要关注上图中的“4 消息推送”和“5 消息回复”就足够了。
我们先来了解接口中的“消息推送”指的是什么,点击“4 消息推送”,可以看到接口中的“消息推送”指的是“当普通用户向公众帐号发消息时,微信服务器将POST该消息到填写的URL上”,即这里定义的是用户能够发送哪些类型的消息、消息有哪些字段、消息被微信服务器以什么方式转发给我们的公众帐号后台。
消息推送中定义了我们将会接收到的消息类型有5种:文本消息、图片消息、地理位置消息、链接消息和事件推送,其实语音消息我们也能够接收到的,只不过拿不到具体的语音文件而以(需要内测资格才能够获取语音文件)。
接口中的“消息回复”定义了我们能回复给用户的消息类型、消息字段和消息格式,微信公众平台的接口指南中是这样描述的:
上面说到我们能回复给用户的消息有5种,但目前在开发模式下能回复的消息只有3种:文本消息、音乐消息和图文消息,而语音消息和视频消息目前只能在编辑模式下使用。
消息的封装
接下来要做的就是将消息推送(请求)、消息回复(响应)中定义的消息进行封装,建立与之对应的Java类(Java是一门面向对象的编程语言,封装后使用起来更方便),下面的请求消息是指消息推送中定义的消息,响应消息指消息回复中定义的消息。
请求消息的基类
把消息推送中定义的所有消息都有的字段提取出来,封装成一个基类,这些公有的字段包括:ToUserName(开发者微信号)、FromUserName(发送方帐号,OPEN_ID)、CreateTime(消息的创建时间)、MsgType(消息类型)、MsgId(消息ID),封装后基类org.liufeng.course.message.req.BaseMessage的代码如下:
[java] view plain copy
- package org.liufeng.course.message.req;
- /**
- * 消息基类(普通用户 -> 公众帐号)
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class BaseMessage {
- // 开发者微信号
- private String ToUserName;
- // 发送方帐号(一个OpenID)
- private String FromUserName;
- // 消息创建时间 (整型)
- private long CreateTime;
- // 消息类型(text/image/location/link)
- private String MsgType;
- // 消息id,64位整型
- private long MsgId;
- public String getToUserName() {
- return ToUserName;
- }
- public void setToUserName(String toUserName) {
- ToUserName = toUserName;
- }
- public String getFromUserName() {
- return FromUserName;
- }
- public void setFromUserName(String fromUserName) {
- FromUserName = fromUserName;
- }
- public long getCreateTime() {
- return CreateTime;
- }
- public void setCreateTime(long createTime) {
- CreateTime = createTime;
- }
- public String getMsgType() {
- return MsgType;
- }
- public void setMsgType(String msgType) {
- MsgType = msgType;
- }
- public long getMsgId() {
- return MsgId;
- }
- public void setMsgId(long msgId) {
- MsgId = msgId;
- }
- }
请求消息之文本消息
[java] view plain copy
- package org.liufeng.course.message.req;
- /**
- * 文本消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class TextMessage extends BaseMessage {
- // 消息内容
- private String Content;
- public String getContent() {
- return Content;
- }
- public void setContent(String content) {
- Content = content;
- }
- }
请求消息之图片消息
[java] view plain copy
- package org.liufeng.course.message.req;
- /**
- * 图片消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class ImageMessage extends BaseMessage {
- // 图片链接
- private String PicUrl;
- public String getPicUrl() {
- return PicUrl;
- }
- public void setPicUrl(String picUrl) {
- PicUrl = picUrl;
- }
- }
请求消息之地理位置消息
[java] view plain copy
- package org.liufeng.course.message.req;
- /**
- * 地理位置消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class LocationMessage extends BaseMessage {
- // 地理位置维度
- private String Location_X;
- // 地理位置经度
- private String Location_Y;
- // 地图缩放大小
- private String Scale;
- // 地理位置信息
- private String Label;
- public String getLocation_X() {
- return Location_X;
- }
- public void setLocation_X(String location_X) {
- Location_X = location_X;
- }
- public String getLocation_Y() {
- return Location_Y;
- }
- public void setLocation_Y(String location_Y) {
- Location_Y = location_Y;
- }
- public String getScale() {
- return Scale;
- }
- public void setScale(String scale) {
- Scale = scale;
- }
- public String getLabel() {
- return Label;
- }
- public void setLabel(String label) {
- Label = label;
- }
- }
请求消息之链接消息
[java] view plain copy
- package org.liufeng.course.message.req;
- /**
- * 链接消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class LinkMessage extends BaseMessage {
- // 消息标题
- private String Title;
- // 消息描述
- private String Description;
- // 消息链接
- private String Url;
- public String getTitle() {
- return Title;
- }
- public void setTitle(String title) {
- Title = title;
- }
- public String getDescription() {
- return Description;
- }
- public void setDescription(String description) {
- Description = description;
- }
- public String getUrl() {
- return Url;
- }
- public void setUrl(String url) {
- Url = url;
- }
- }
请求消息之语音消息
[java] view plain copy
- package org.liufeng.course.message.req;
- /**
- * 音频消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class VoiceMessage extends BaseMessage {
- // 媒体ID
- private String MediaId;
- // 语音格式
- private String Format;
- public String getMediaId() {
- return MediaId;
- }
- public void setMediaId(String mediaId) {
- MediaId = mediaId;
- }
- public String getFormat() {
- return Format;
- }
- public void setFormat(String format) {
- Format = format;
- }
- }
响应消息的基类
同样,把消息回复中定义的所有消息都有的字段提取出来,封装成一个基类,这些公有的字段包括:ToUserName(接收方帐号,用户的OPEN_ID)、FromUserName(开发者的微信号)、CreateTime(消息的创建时间)、MsgType(消息类型)、FuncFlag(消息的星标标识),封装后基类org.liufeng.course.message.resp.BaseMessage的代码如下:
[java] view plain copy
- package org.liufeng.course.message.resp;
- /**
- * 消息基类(公众帐号 -> 普通用户)
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class BaseMessage {
- // 接收方帐号(收到的OpenID)
- private String ToUserName;
- // 开发者微信号
- private String FromUserName;
- // 消息创建时间 (整型)
- private long CreateTime;
- // 消息类型(text/music/news)
- private String MsgType;
- // 位0x0001被标志时,星标刚收到的消息
- private int FuncFlag;
- public String getToUserName() {
- return ToUserName;
- }
- public void setToUserName(String toUserName) {
- ToUserName = toUserName;
- }
- public String getFromUserName() {
- return FromUserName;
- }
- public void setFromUserName(String fromUserName) {
- FromUserName = fromUserName;
- }
- public long getCreateTime() {
- return CreateTime;
- }
- public void setCreateTime(long createTime) {
- CreateTime = createTime;
- }
- public String getMsgType() {
- return MsgType;
- }
- public void setMsgType(String msgType) {
- MsgType = msgType;
- }
- public int getFuncFlag() {
- return FuncFlag;
- }
- public void setFuncFlag(int funcFlag) {
- FuncFlag = funcFlag;
- }
- }
响应消息之文本消息
[java] view plain copy
- package org.liufeng.course.message.resp;
- /**
- * 文本消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class TextMessage extends BaseMessage {
- // 回复的消息内容
- private String Content;
- public String getContent() {
- return Content;
- }
- public void setContent(String content) {
- Content = content;
- }
- }
响应消息之音乐消息
[java] view plain copy
- package org.liufeng.course.message.resp;
- /**
- * 音乐消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class MusicMessage extends BaseMessage {
- // 音乐
- private Music Music;
- public Music getMusic() {
- return Music;
- }
- public void setMusic(Music music) {
- Music = music;
- }
- }
音乐消息中Music类的定义
[java] view plain copy
- package org.liufeng.course.message.resp;
- /**
- * 音乐model
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class Music {
- // 音乐名称
- private String Title;
- // 音乐描述
- private String Description;
- // 音乐链接
- private String MusicUrl;
- // 高质量音乐链接,WIFI环境优先使用该链接播放音乐
- private String HQMusicUrl;
- public String getTitle() {
- return Title;
- }
- public void setTitle(String title) {
- Title = title;
- }
- public String getDescription() {
- return Description;
- }
- public void setDescription(String description) {
- Description = description;
- }
- public String getMusicUrl() {
- return MusicUrl;
- }
- public void setMusicUrl(String musicUrl) {
- MusicUrl = musicUrl;
- }
- public String getHQMusicUrl() {
- return HQMusicUrl;
- }
- public void setHQMusicUrl(String musicUrl) {
- HQMusicUrl = musicUrl;
- }
- }
响应消息之图文消息
[java] view plain copy
- package org.liufeng.course.message.resp;
- import java.util.List;
- /**
- * 文本消息
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class NewsMessage extends BaseMessage {
- // 图文消息个数,限制为10条以内
- private int ArticleCount;
- // 多条图文消息信息,默认第一个item为大图
- private List<Article> Articles;
- public int getArticleCount() {
- return ArticleCount;
- }
- public void setArticleCount(int articleCount) {
- ArticleCount = articleCount;
- }
- public List<Article> getArticles() {
- return Articles;
- }
- public void setArticles(List<Article> articles) {
- Articles = articles;
- }
- }
图文消息中Article类的定义
[java] view plain copy
- package org.liufeng.course.message.resp;
- /**
- * 图文model
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class Article {
- // 图文消息名称
- private String Title;
- // 图文消息描述
- private String Description;
- // 图片链接,支持JPG、PNG格式,较好的效果为大图640*320,小图80*80,限制图片链接的域名需要与开发者填写的基本资料中的Url一致
- private String PicUrl;
- // 点击图文消息跳转链接
- private String Url;
- public String getTitle() {
- return Title;
- }
- public void setTitle(String title) {
- Title = title;
- }
- public String getDescription() {
- return null == Description ? "" : Description;
- }
- public void setDescription(String description) {
- Description = description;
- }
- public String getPicUrl() {
- return null == PicUrl ? "" : PicUrl;
- }
- public void setPicUrl(String picUrl) {
- PicUrl = picUrl;
- }
- public String getUrl() {
- return null == Url ? "" : Url;
- }
- public void setUrl(String url) {
- Url = url;
- }
- }
全部消息封装完成后,Eclipse工程中关于消息部分的结构应该与下图保持一致,如果不一致的(类名、属性名称不一致的)请检查后调整一致,因为后面的章节还要介绍如何将微信开发中通用的类方法、与业务无关的工具类封装打成jar包,以后再做微信项目只需要引入该jar包即可,这种工作做一次就可以了。
如何解析请求消息?
接下来解决请求消息的解析问题。微信服务器会将用户的请求通过doPost方法发送给我们,让我们再来回顾下上一章节已经写好的doPost方法的定义:
[java] view plain copy
- /**
- * 处理微信服务器发来的消息
- */
- public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // TODO 消息的接收、处理、响应
- }
doPost方法有两个参数,request中封装了请求相关的所有内容,可以从request中取出微信服务器发来的消息;而通过response我们可以对接收到的消息进行响应,即发送消息。
那么如何解析请求消息的问题也就转化为如何从request中得到微信服务器发送给我们的xml格式的消息了。这里我们借助于开源框架dom4j去解析xml(这里使用的是dom4j-1.6.1.jar),然后将解析得到的结果存入HashMap,解析请求消息的方法如下:
[java] view plain copy
- /**
- * 解析微信发来的请求(XML)
- *
- * @param request
- * @return
- * @throws Exception
- */
- @SuppressWarnings("unchecked")
- public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
- // 将解析结果存储在HashMap中
- Map<String, String> map = new HashMap<String, String>();
- // 从request中取得输入流
- InputStream inputStream = request.getInputStream();
- // 读取输入流
- SAXReader reader = new SAXReader();
- Document document = reader.read(inputStream);
- // 得到xml根元素
- Element root = document.getRootElement();
- // 得到根元素的所有子节点
- List<Element> elementList = root.elements();
- // 遍历所有子节点
- for (Element e : elementList)
- map.put(e.getName(), e.getText());
- // 释放资源
- inputStream.close();
- inputStream = null;
- return map;
- }
如何将响应消息转换成xml返回?
我们先前已经将响应消息封装成了Java类,方便我们在代码中使用。那么,请求接收成功、处理完成后,该如何将消息返回呢?这里就涉及到如何将响应消息转换成xml返回的问题,这里我们将采用开源框架xstream来实现Java类到xml的转换(这里使用的是xstream-1.3.1.jar),代码如下:
[java] view plain copy
- /**
- * 文本消息对象转换成xml
- *
- * @param textMessage 文本消息对象
- * @return xml
- */
- public static String textMessageToXml(TextMessage textMessage) {
- xstream.alias("xml", textMessage.getClass());
- return xstream.toXML(textMessage);
- }
- /**
- * 音乐消息对象转换成xml
- *
- * @param musicMessage 音乐消息对象
- * @return xml
- */
- public static String musicMessageToXml(MusicMessage musicMessage) {
- xstream.alias("xml", musicMessage.getClass());
- return xstream.toXML(musicMessage);
- }
- /**
- * 图文消息对象转换成xml
- *
- * @param newsMessage 图文消息对象
- * @return xml
- */
- public static String newsMessageToXml(NewsMessage newsMessage) {
- xstream.alias("xml", newsMessage.getClass());
- xstream.alias("item", new Article().getClass());
- return xstream.toXML(newsMessage);
- }
- /**
- * 扩展xstream,使其支持CDATA块
- *
- * @date 2013-05-19
- */
- private static XStream xstream = new XStream(new XppDriver() {
- public HierarchicalStreamWriter createWriter(Writer out) {
- return new PrettyPrintWriter(out) {
- // 对所有xml节点的转换都增加CDATA标记
- boolean cdata = true;
- @SuppressWarnings("unchecked")
- public void startNode(String name, Class clazz) {
- super.startNode(name, clazz);
- }
- protected void writeText(QuickWriter writer, String text) {
- if (cdata) {
- writer.write("<![CDATA[");
- writer.write(text);
- writer.write("]]>");
- } else {
- writer.write(text);
- }
- }
- };
- }
- });
说明:由于xstream框架本身并不支持CDATA块的生成,40~62行代码是对xtream做了扩展,使其支持在生成xml各元素值时添加CDATA块。
消息处理工具的封装
知道怎么解析请求消息,也知道如何将响应消息转化成xml了,接下来就是将消息相关的处理方法全部封装到工具类MessageUtil中,该类的完整代码如下:
[java] view plain copy
- package org.liufeng.course.util;
- import java.io.InputStream;
- import java.io.Writer;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.dom4j.Document;
- import org.dom4j.Element;
- import org.dom4j.io.SAXReader;
- import org.liufeng.course.message.resp.Article;
- import org.liufeng.course.message.resp.MusicMessage;
- import org.liufeng.course.message.resp.NewsMessage;
- import org.liufeng.course.message.resp.TextMessage;
- import com.thoughtworks.xstream.XStream;
- import com.thoughtworks.xstream.core.util.QuickWriter;
- import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
- import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
- import com.thoughtworks.xstream.io.xml.XppDriver;
- /**
- * 消息工具类
- *
- * @author liufeng
- * @date 2013-05-19
- */
- public class MessageUtil {
- /**
- * 返回消息类型:文本
- */
- public static final String RESP_MESSAGE_TYPE_TEXT = "text";
- /**
- * 返回消息类型:音乐
- */
- public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
- /**
- * 返回消息类型:图文
- */
- public static final String RESP_MESSAGE_TYPE_NEWS = "news";
- /**
- * 请求消息类型:文本
- */
- public static final String REQ_MESSAGE_TYPE_TEXT = "text";
- /**
- * 请求消息类型:图片
- */
- public static final String REQ_MESSAGE_TYPE_IMAGE = "image";
- /**
- * 请求消息类型:链接
- */
- public static final String REQ_MESSAGE_TYPE_LINK = "link";
- /**
- * 请求消息类型:地理位置
- */
- public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
- /**
- * 请求消息类型:音频
- */
- public static final String REQ_MESSAGE_TYPE_VOICE = "voice";
- /**
- * 请求消息类型:推送
- */
- public static final String REQ_MESSAGE_TYPE_EVENT = "event";
- /**
- * 事件类型:subscribe(订阅)
- */
- public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
- /**
- * 事件类型:unsubscribe(取消订阅)
- */
- public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
- /**
- * 事件类型:CLICK(自定义菜单点击事件)
- */
- public static final String EVENT_TYPE_CLICK = "CLICK";
- /**
- * 解析微信发来的请求(XML)
- *
- * @param request
- * @return
- * @throws Exception
- */
- @SuppressWarnings("unchecked")
- public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
- // 将解析结果存储在HashMap中
- Map<String, String> map = new HashMap<String, String>();
- // 从request中取得输入流
- InputStream inputStream = request.getInputStream();
- // 读取输入流
- SAXReader reader = new SAXReader();
- Document document = reader.read(inputStream);
- // 得到xml根元素
- Element root = document.getRootElement();
- // 得到根元素的所有子节点
- List<Element> elementList = root.elements();
- // 遍历所有子节点
- for (Element e : elementList)
- map.put(e.getName(), e.getText());
- // 释放资源
- inputStream.close();
- inputStream = null;
- return map;
- }
- /**
- * 文本消息对象转换成xml
- *
- * @param textMessage 文本消息对象
- * @return xml
- */
- public static String textMessageToXml(TextMessage textMessage) {
- xstream.alias("xml", textMessage.getClass());
- return xstream.toXML(textMessage);
- }
- /**
- * 音乐消息对象转换成xml
- *
- * @param musicMessage 音乐消息对象
- * @return xml
- */
- public static String musicMessageToXml(MusicMessage musicMessage) {
- xstream.alias("xml", musicMessage.getClass());
- return xstream.toXML(musicMessage);
- }
- /**
- * 图文消息对象转换成xml
- *
- * @param newsMessage 图文消息对象
- * @return xml
- */
- public static String newsMessageToXml(NewsMessage newsMessage) {
- xstream.alias("xml", newsMessage.getClass());
- xstream.alias("item", new Article().getClass());
- return xstream.toXML(newsMessage);
- }
- /**
- * 扩展xstream,使其支持CDATA块
- *
- * @date 2013-05-19
- */
- private static XStream xstream = new XStream(new XppDriver() {
- public HierarchicalStreamWriter createWriter(Writer out) {
- return new PrettyPrintWriter(out) {
- // 对所有xml节点的转换都增加CDATA标记
- boolean cdata = true;
- @SuppressWarnings("unchecked")
- public void startNode(String name, Class clazz) {
- super.startNode(name, clazz);
- }
- protected void writeText(QuickWriter writer, String text) {
- if (cdata) {
- writer.write("<![CDATA[");
- writer.write(text);
- writer.write("]]>");
- } else {
- writer.write(text);
- }
- }
- };
- }
- });
- }
OK,到这里关于消息及消息处理工具的封装就讲到这里,其实就是对请求消息/响应消息建立了与之对应的Java类、对xml消息进行解析、将响应消息的Java对象转换成xml。下一篇讲会介绍如何利用上面封装好的工具识别用户发送的消息类型,并做出正确的响应。
五、第5篇-各种消息的接收与响应
前一篇文章里我们已经把微信公众平台接口中消息及相关操作都进行了封装,本章节将主要介绍如何接收微信服务器发送的消息并做出响应。
明确在哪接收消息
从微信公众平台接口消息指南中可以了解到,当用户向公众帐号发消息时,微信服务器会将消息通过POST方式提交给我们在接口配置信息中填写的URL,而我们就需要在URL所指向的请求处理类CoreServlet的doPost方法中接收消息、处理消息和响应消息。
接收、处理、响应消息
下面先来看我已经写好的CoreServlet的完整代码:
[java] view plain copy
- package org.liufeng.course.servlet;
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.liufeng.course.service.CoreService;
- import org.liufeng.course.util.SignUtil;
- /**
- * 核心请求处理类
- *
- * @author liufeng
- * @date 2013-05-18
- */
- public class CoreServlet extends HttpServlet {
- private static final long serialVersionUID = 4440739483644821986L;
- /**
- * 确认请求来自微信服务器
- */
- public 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");
- PrintWriter out = response.getWriter();
- // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
- if (SignUtil.checkSignature(signature, timestamp, nonce)) {
- out.print(echostr);
- }
- out.close();
- out = null;
- }
- /**
- * 处理微信服务器发来的消息
- */
- public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 将请求、响应的编码均设置为UTF-8(防止中文乱码)
- request.setCharacterEncoding("UTF-8");
- response.setCharacterEncoding("UTF-8");
- // 调用核心业务类接收消息、处理消息
- String respMessage = CoreService.processRequest(request);
- // 响应消息
- PrintWriter out = response.getWriter();
- out.print(respMessage);
- out.close();
- }
- }
代码说明:
1)第51行代码:微信服务器POST消息时用的是UTF-8编码,在接收时也要用同样的编码,否则中文会乱码;
2)第52行代码:在响应消息(回复消息给用户)时,也将编码方式设置为UTF-8,原理同上;
3)第54行代码:调用CoreService类的processRequest方法接收、处理消息,并得到处理结果;
4)第57~59行:调用response.getWriter().write()方法将消息的处理结果返回给用户
从doPost方法的实现可以看到,它是通过调用CoreService类的processRequest方法接收、处理消息的,这样做的目的是为了解耦,即业务相关的操作都不在Servlet里处理,而是完全交由业务核心类CoreService去做。下面来看CoreService类的代码实现:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.Date;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-05-20
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- *
- * @param request
- * @return
- */
- public static String processRequest(HttpServletRequest request) {
- String respMessage = null;
- try {
- // 默认返回的文本消息内容
- String respContent = "请求处理异常,请稍候尝试!";
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 消息类型
- String msgType = requestMap.get("MsgType");
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- // 文本消息
- if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
- respContent = "您发送的是文本消息!";
- }
- // 图片消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {
- respContent = "您发送的是图片消息!";
- }
- // 地理位置消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {
- respContent = "您发送的是地理位置消息!";
- }
- // 链接消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {
- respContent = "您发送的是链接消息!";
- }
- // 音频消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {
- respContent = "您发送的是音频消息!";
- }
- // 事件推送
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {
- // 事件类型
- String eventType = requestMap.get("Event");
- // 订阅
- if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {
- respContent = "谢谢您的关注!";
- }
- // 取消订阅
- else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {
- // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息
- }
- // 自定义菜单点击事件
- else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {
- // TODO 自定义菜单权没有开放,暂不处理该类消息
- }
- }
- textMessage.setContent(respContent);
- respMessage = MessageUtil.textMessageToXml(textMessage);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respMessage;
- }
- }
代码说明:
1)第29行:调用消息工具类MessageUtil解析微信发来的xml格式的消息,解析的结果放在HashMap里;
2)32~36行:从HashMap中取出消息中的字段;
3)39-44、84行:组装要返回的文本消息对象;
4)47~82行:演示了如何接收微信发送的各类型的消息,根据MsgType判断属于哪种类型的消息;
5)85行:调用消息工具类MessageUtil将要返回的文本消息对象TextMessage转化成xml格式的字符串;
关于事件推送(关注、取消关注、菜单点击)
对于消息类型的判断,像文本消息、图片消息、地理位置消息、链接消息和语音消息都比较好理解,有很多刚接触的朋友搞不懂事件推送消息有什么用,或者不清楚该如何判断用户关注的消息。那我们就专门来看下事件推送,下图是官方消息接口文档中关于事件推送的说明:
这里我们只要关心两个参数:MsgType和Event。当MsgType=event时,就表示这是一条事件推送消息;而Event表示事件类型,包括订阅、取消订阅和自定义菜单点击事件。也就是说,无论用户是关注了公众帐号、取消对公众帐号的关注,还是在使用公众帐号的菜单,微信服务器都会发送一条MsgType=event的消息给我们,而至于具体这条消息表示关注、取消关注,还是菜单的点击事件,就需要通过Event的值来判断了。(注意区分Event和event)
连载五篇教程总结
经过5篇的讲解,已经把开发模式启用,接口配置,消息相关工具类的封装,消息的接收与响应全部讲解完了,而且贴上了完整的源代码,相信有一定Java基础的朋友可以看的明白,能够通过系列文章基本掌握微信公众平台开发的相关技术知识。下面我把目前项目的完整结构贴出,方便大家对照:
六、第6篇-文本消息的内容长度限制揭秘
相信不少朋友都遇到过这样的问题:当发送的文本消息内容过长时,微信将不做任何响应。那么到底微信允许的文本消息的最大长度是多少呢?我们又该如何计算文本的长度呢?为什么还有些人反应微信好像支持的文本消息最大长度在1300多呢?这篇文章会彻底解除大家的疑问。
接口文档中对消息长度限制为2048
可以看到,接口文档中写的很明确:回复的消息内容长度不超过2048字节。那为什么很多人测试反应消息内容长度在1300多字节时,微信就不响应了呢?我想这问题应该在这部分人没有搞清楚到底该如何计算文本的字节数。
如何正确计算文本所占字节数
计算文本(字符串)所占字节数,大家第一个想到的应该就是String类的getBytes()方法,该方法返回的是字符串对应的字节数组,再计算数组的length就能够得到字符串所占字节数。例如:
[java] view plain copy
- public static void main(String []args) {
- // 运行结果:4
- System.out.println("柳峰".getBytes().length);
- }
上面的示例中计算了两个中文所占的字节数为4,即一个汉字占2个字节。真的是这样吗?其实我们忽略了一个问题:对于不同的编码方式,中文所占的字节数也不一样!这到底要怎么呢?在上面的例子中,我们并没有指定编码方式,那么会使用操作系统所默认的编码方式。先来看我得出的三条结论:
1)如果上面的例子运行在默认编码方式为ISO8859-1的操作系统平台上,计算结果是2;
2)如果上面的例子运行在默认编码方式为gb2312或gbk的操作系统平台上,计算结果是4;
3)如果上面的例子运行在默认编码方式为utf-8的操作系统平台上,计算结果是6;
如果真的是这样,是不是意味着String.getBytes()方法在我们的系统平台上默认采用的是gb2312或gbk编码方式呢?我们再来看一个例子:
[java] view plain copy
- public static void main(String []args) throws UnsupportedEncodingException {
- // 运行结果:2
- System.out.println("柳峰".getBytes("ISO8859-1").length);
- // 运行结果:4
- System.out.println("柳峰".getBytes("GB2312").length);
- // 运行结果:4
- System.out.println("柳峰".getBytes("GBK").length);
- // 运行结果:6
- System.out.println("柳峰".getBytes("UTF-8").length);
- }
这个例子是不是很好地证明了我上面给出的三条结论呢?也就是说采用ISO8859-1编码方式时,一个中/英文都只占一个字节;采用GB2312或GBK编码方式时,一个中文占两个字节;而采用UTF-8编码方式时,一个中文占三个字节。
微信平台采用的编码方式及字符串所占字节数的计算
那么,在向微信服务器返回消息时,该采用什么编码方式呢?当然是UTF-8,因为我们已经在doPost方法里采用了如下代码来避免中文乱码了:
[java] view plain copy
- // 将请求、响应的编码均设置为UTF-8(防止中文乱码)
- request.setCharacterEncoding("UTF-8");
- response.setCharacterEncoding("UTF-8");
为了验证我所说了,我写了个例子来测试:
[java] view plain copy
- private static String getMsgContent() {
- StringBuffer buffer = new StringBuffer();
- // 每行70个汉字,共682个汉字加1个英文的感叹号
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");
- buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵!");
- return buffer.toString();
- }
- public static void main(String []args) throws Exception {
- // 采用gb2312编码方式时占1365个字节
- System.out.println(getMsgContent().getBytes("gb2312").length);
- // 采用utf-8编码方式时占2047个字节
- System.out.println(getMsgContent().getBytes("utf-8").length);
- }
getMsgContent()方法返回的内容正是微信的文本消息最长能够支持的,即采用UTF-8编码方式时,文本消息内容最多支持2047个字节,也就是微信公众平台接口文档里所说的回复的消息内容长度不超过2048字节,即使是等于2048字节也不行,你可以试着将getMsgContent()方法里的内容多加一个英文符号,这个时候微信就不响应了。
同时,我们也发现,如果采用gb2312编码方式来计算getMsgContent()方法返回的文本所占字节数的结果是1365,这就是为什么很多朋友都说微信的文本消息最大长度好像只支持1300多字节,并不是接口文档中所说的2048字节,其实是忽略了编码方式,只是简单的使用了String类的getBytes()方法而不是getBytes("utf-8")方法去计算所占字节数。
Java中utf-8编码方式时所占字节数的计算方法封装
[java] view plain copy
- /**
- * 计算采用utf-8编码方式时字符串所占字节数
- *
- * @param content
- * @return
- */
- public static int getByteSize(String content) {
- int size = 0;
- if (null != content) {
- try {
- // 汉字采用utf-8编码时占3个字节
- size = content.getBytes("utf-8").length;
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- }
- return size;
- }
好了,本章节的内容就讲到这里,我想大家通过这篇文章所学到的应该不仅仅是2047这个数字,还应该对字符编码方式有一个新的认识。
七、第7篇-文本消息中换行符的使用
本篇文章主要介绍在文本消息中使用换行符的好处以及如何使用换行符。
最近一个月虽然抽不出时间写博客,但却一直在认真答复大家提出的问题。收到这么多的回复、关注和答谢,还是蛮有成就感的,让我觉得做这件事越来越有意义,更加坚定了我继续写下去的决心。经过前面六篇文章的讲解,相信在看文章的你,已经掌握了微信公众帐号的基础开发知识(基于Java),如框架搭建、API封装、消息接收与回复等;接下来的系列文章将专注于讲解公众帐号开发中的技巧及实用功能的开发(如天气查询、周边搜索、人机对话等)。
使用换行的好处及示例
使用换行的好处无非就是让信息的呈现更加整齐、美观和直观,适当的在文本消息中使用换行符,会让人看了之后感觉很舒服、清晰、明了。下面是公众帐号xiaoqrobot的主菜单示例,就是合理地使用了换行符,看上去是不是很直观、清爽呢?(什么?觉得很丑?呃,那就算是我自恋吧...)
你可以试想一下,如果这个文本菜单没有使用一个换行符,那会长什么样?
如何在文本消息中使用换行符?
在微信公众帐号的文本消息中,换行符仍然是“\n”,下面就通过代码来讲解xiaoqrobot的文本菜单是如何实现的?
[java] view plain copy
- /**
- * xiaoqrobot的主菜单
- *
- * @return
- */
- public static String getMainMenu() {
- StringBuffer buffer = new StringBuffer();
- buffer.append("您好,我是小q,请回复数字选择服务:").append("\n\n");
- buffer.append("1 天气预报").append("\n");
- buffer.append("2 公交查询").append("\n");
- buffer.append("3 周边搜索").append("\n");
- buffer.append("4 歌曲点播").append("\n");
- buffer.append("5 经典游戏").append("\n");
- buffer.append("6 美女电台").append("\n");
- buffer.append("7 人脸识别").append("\n");
- buffer.append("8 聊天唠嗑").append("\n\n");
- buffer.append("回复“?”显示此帮助菜单");
- return buffer.toString();
- }
怎么样,实现起来是不是很简单呢?
1)9-16行就是菜单项,菜单项之间都是用一个换行符分隔;
2)第8行、第16号末尾都使用了两个换行符,这样可以把菜单项与其他内容分隔开,更有层次感,看上去也会舒服、直观一点。
可能细心的朋友已经发现了:在截图上,“周边搜索”和“美女电台”后边都有一个“礼物”表情,而代码中并没有看到,这是我专门去掉的,因为我打算后面专门用一篇文章把QQ表情的发送、处理、接收讲清楚。
细节决定成败!
八、第8篇-文本消息中使用网页超链接
本文主要介绍网页超链接的作用以及如何在文本消息中使用网页超链接。
网页超链接的作用
我想但凡是熟悉HTML的朋友,对超链接一定不会陌生。而今天我们要讨论和使用的只是超链接中的其中一种---网页超链接,即使用HTML中的<a>标签将某段文字链接到其他网页上去,示例如下:
[html] view plain copy
- <a href="http://blog.csdn.net/lyq8479">柳峰的博客</a>
上面是一段标准的HTML代码,实现了一个网页超链接,即将“柳峰的博客”5个字链接到了博客主页URL,当“柳峰的博客”5个字时,会打开http://blog.csdn.net/lyq8479所指向的网页。
如何在文本消息中使用网页超链接
其实,不知道如何在文本消息中使用网页超链接的开发者几乎100%都熟悉HTML,特别是对HTML中的<a>标签再熟悉不过了。那到底在微信公众帐号的文本消息中使用超链接有什么特别之处呢?为什么如此多的朋友都曾经在这个问题上栽过跟头?我们先来看在微信中两种错误使用超链接的方法:
错误用法1(a标签的href属性值未被引号引起):
[html] view plain copy
- <a href=http://blog.csdn.net/lyq8479>柳峰的博客</a>
错误用法2(a标签的href属性值被单引号引起):
[html] view plain copy
- <a href=‘http://blog.csdn.net/lyq8479‘>柳峰的博客</a>
在做Web开发时,以上两种写法都是可以的,但是放在微信公众帐号的文本消息中,这两种写法都是错误的,网页超链接并不会起作用,而且在Android手机上还会将HTML代码原样显示出来,如下图所示:
Android手机上的效果:
iPhone手机上的效果:
可以看出,在微信上,HTML的a标签属性值不用引号引起,或者使用单引号引起,都是错误的写法(在iPhone上,a标签属性href的值用单引号是正常的)。正确的用法是将a标签href属性的值用双引号引起,代码如下:
[html] view plain copy
- <a href="http://blog.csdn.net/lyq8479">柳峰的博客</a>
这样在Android和iPhone手机上,都可以正确显示超链接,并且点击该超链接,会使用微信内置浏览器打开http://blog.csdn.net/lyq8479。
提示:在测试微信公众帐号时,不要只是在自己的手机上测试通过就认为完全没问题了,因为目前微信公众帐号上有好几处在Android和iOS平台上表现不一致。
九、第9篇-QQ表情的发送与接收
我想大家对QQ表情一定不会陌生,一个个小头像极大丰富了聊天的乐趣,使得聊天不再是简单的文字叙述,还能够配上喜、怒、哀、乐等表达人物心情的小图片。本文重点要介绍的内容就是如何在微信公众平台使用QQ表情,即在微信公众帐号开发模式下,如何发送QQ表情给用户,以及如何识别用户发来的是QQ表情。
QQ表情代码表
首先需要明确的是:QQ表情虽然呈现为一张张动态的表情图片,但在微信公众平台的消息接口中却是属于文本消息;也就是说当用户向公众帐号发送QQ表情时,公众帐号后台程序接收到的消息类型MsgType的值为text。只要上面这点能理解了,下面的工作就好开展了。
对于QQ表情,发送的是文本消息,而呈现出来却是表情图片,那么每一个QQ表情图片一定会有与之相对应的表情代码。下面是我已经整理好的微信公众帐号中使用的QQ表情代码对照表:
上面一共列出了105个QQ表情,每个表情都给出了与之相对应的文字代码与符号代码(也许这两种叫法并不恰当),至于这两种代码怎么来的以及如何使用,下面马上会讲到。
用户向公众帐号发送QQ表情
在微信上使用公众帐号时,如何发送QQ表情,我想这个很少有人不会的。在输入框旁边有一个笑脸的图片按钮,点击它将会弹出表情选择界面,可选择的表情依次为“QQ表情”、“符号表情”和“动画表情”。当我们点击选择了某个QQ表情后,发现在输入框中会显示该表情的文字代码,这里是用一对中括号引起的,如下图所示:
其实,当我们很熟悉要使用QQ表情的文字代码时,也可以直接在输入框中输入表情的代码,而不需要弹出表情选择框。如下图所示:
从上图可以看出,在输入框中输入“[呲牙]”、“/呲牙”和“/::D”这三种代码的作用一样,都是发送呲牙的QQ表情。这个时候,大家再回过头去看文章最开始的QQ表情代码对照表,就明白是怎么回事了。
公众帐号向用户发送QQ表情
与用户向公众帐号发送QQ表情一样,在开发模式下,公众帐号也可以用同样的表情代码(文字代码或符号代码)向用户回复QQ表情。代码片段如下:
[java] view plain copy
- // 文本消息
- if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- textMessage.setContent("[难过] /难过 /::(");
- // 文本消息对象转换成xml字符串
- respMessage = MessageUtil.textMessageToXml(textMessage);
- }
上面代码片段的作用是:判断发送的消息类型,如果是文本消息(MsgType=text),则回复三个难过的QQ表情给用户。可以看出,不管是用户发给公众帐号,还是公众帐号发给用户,都可以使用QQ表情的文字代码(如:[难过] /难过)和符号代码(如 /::()。
公众帐号识别用户发送的QQ表情
在掌握了如何发送QQ表情后,我们再来看看公众帐号如何识别用户发送的是QQ表情。这是什么意思呢?当用户向公众帐号发送一个QQ表情,在后台程序中接收到的会是什么值,我们又怎么知道这个值就是一个QQ表情。
其实,只要做个简单的测试,比如:将接收到的文本消息输出到日志中(可以用log4j或者System.out.print),不难发现:向公众帐号发送一个QQ表情,在后台程序中接收到的是QQ表情的符号代码。
下面是我简单封装的一个方法,通过正则表达式实现的,用于判断用户发送的是否是单个QQ表情。
[java] view plain copy
- /**
- * 判断是否是QQ表情
- *
- * @param content
- * @return
- */
- public static boolean isQqFace(String content) {
- boolean result = false;
- // 判断QQ表情的正则表达式
- String qqfaceRegex = "/::\\)|/::~|/::B|/::\\||/:8-\\)|/::<|/::$|/::X|/::Z|/::‘\\(|/::-\\||/::@|/::P|/::D|/::O|/::\\(|/::\\+|/:--b|/::Q|/::T|/:,@P|/:,@-D|/::d|/:,@o|/::g|/:\\|-\\)|/::!|/::L|/::>|/::,@|/:,@f|/::-S|/:\\?|/:,@x|/:,@@|/::8|/:,@!|/:!!!|/:xx|/:bye|/:wipe|/:dig|/:handclap|/:&-\\(|/:B-\\)|/:<@|/:@>|/::-O|/:>-\\||/:P-\\(|/::‘\\||/:X-\\)|/::\\*|/:@x|/:8\\*|/:pd|/:<W>|/:beer|/:basketb|/:oo|/:coffee|/:eat|/:pig|/:rose|/:fade|/:showlove|/:heart|/:break|/:cake|/:li|/:bome|/:kn|/:footb|/:ladybug|/:shit|/:moon|/:sun|/:gift|/:hug|/:strong|/:weak|/:share|/:v|/:@\\)|/:jj|/:@@|/:bad|/:lvu|/:no|/:ok|/:love|/:<L>|/:jump|/:shake|/:<O>|/:circle|/:kotow|/:turn|/:skip|/:oY|/:#-0|/:hiphot|/:kiss|/:<&|/:&>";
- Pattern p = Pattern.compile(qqfaceRegex);
- Matcher m = p.matcher(content);
- if (m.matches()) {
- result = true;
- }
- return result;
- }
下面是方法的使用,实现了这样一个简单的功能:用户发什么QQ表情给公众帐号,公众帐号就回复什么QQ表情给用户(xiaoqrobot就是这么做的)。实现代码如下:
[java] view plain copy
- // 文本消息
- if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
- // 文本消息内容
- String content = requestMap.get("Content");
- // 判断用户发送的是否是单个QQ表情
- if(XiaoqUtil.isQqFace(content)) {
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- // 用户发什么QQ表情,就返回什么QQ表情
- textMessage.setContent(content);
- // 将文本消息对象转换成xml字符串
- respMessage = MessageUtil.textMessageToXml(textMessage);
- }
- }
好了,关于微信公众帐号中QQ表情的使用就介绍这么多。其实,我并不希望初学者上来只是简单拷贝我贴出的代码,实现了自己想要的功能就完事了,更希望初学的朋友能够通过此文章学会一种思考问题和解决问题的方法。
十、第10篇-解析接口中的消息创建时间CreateTime
从微信公众平台的消息接口指南中可以看出,每种类型的消息定义中,都包含有CreateTime参数,它表示消息的创建时间,如下图所示:
上图是消息接口指南中4.1-文本消息的定义。注意CreateTime的描述:消息创建时间(整型),重点在于这是一个整型的时间,而不是我们大家所熟悉的类似于"yyyy-MM-dd HH:mm:ss"的标准格式时间。本文主要想介绍的就是微信消息接口中定义的整型消息创建时间CreateTime的含义,以及如何将CreateTime转换成我们所熟悉的时间格式。
整型CreateTime的含义
消息接口中定义的消息创建时间CreateTime,它表示1970年1月1日0时0分0秒至消息创建时所间隔的秒数,注意是间隔的秒数,不是毫秒数!
整型CreateTime的转换
在Java中,我们也经常会通过下面两种方式获取long类型的时间,先上代码:
[java] view plain copy
- /**
- * 演示Java中常用的获取long类型时间的两种方式
- */
- public static void main(String[] args) {
- long longTime1 = System.currentTimeMillis();
- // 1373206143378
- System.out.println(longTime1);
- long longTime2 = new java.util.Date().getTime();
- // 1373206143381
- System.out.println(longTime2);
- }
上面两种获取long类型时间的方法是等价的,获取到的结果表示当时时间距离1970年1月1日0时0分0秒0毫秒的毫秒数,注意这里是毫秒数!那么这里获取到的long类型的时间如何转换成标准格式的时间呢?方法如下:
[java] view plain copy
- /**
- * 演示Java中常用的获取long类型时间的两种方式
- */
- public static void main(String[] args) {
- // 当前时间(距离1970年1月1日0时0分0秒0毫秒的毫秒数)
- long longTime = 1373206143378L;
- String stdFormatTime = formatTime(longTime);
- // 输出:2013-07-07 22:09:03
- System.out.println(stdFormatTime);
- }
- /**
- * 将long类型的时间转换成标准格式(yyyy-MM-dd HH:mm:ss)
- *
- * @param longTime
- * @return
- */
- public static String formatTime(long longTime) {
- DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- return format.format(new Date(longTime));
- }
上面演示了将一个long类型的时间转换成标准格式的时间,只是简单的运用了SimpleDateFormat类,比较好懂的。那么再回到今天的主题上来,如何将CreateTime转换成标准格式的时间。
微信消息接口中的CreateTime表示距离1970年的秒数,而System.currentTimeMillis()表示距离1970年的毫秒数,它们之间的换算就相当于:1秒=1000毫秒,即将CreateTime乘以1000,就变成了距离1970年的毫秒数了,就可以使用上面的formatTime()方法来处理了,是不是很简单呢?
下面,我还是单另封装一个方法,用于将微信消息中的整型的消息创建时间CreateTime转换成标准格式的时间,如下:
[java] view plain copy
- /**
- * 将微信消息中的CreateTime转换成标准格式的时间(yyyy-MM-dd HH:mm:ss)
- *
- * @param createTime 消息创建时间
- * @return
- */
- public static String formatTime(String createTime) {
- // 将微信传入的CreateTime转换成long类型,再乘以1000
- long msgCreateTime = Long.parseLong(createTime) * 1000L;
- DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- return format.format(new Date(msgCreateTime));
- }
十一、第11篇-符号表情的发送(上)
题外话(可以略过)
相信这篇文章已经让大家等的太久了,不是我故弄玄虚、吊大家胃口,而是写一篇文章真的需要花太多的时间。也许一篇文章,你们花3-5分钟就看完了、就学会掌握了,而我却要花2-3个小时的时间来完成,也许只有用心写过文章的人才能体会,希望大家能够相互体谅!
也曾经有人对我说,我写的东西太初级,都是入门级的东西。好吧,我承认众口难调,很难满足所有的读者,再加上我自己也只是个新手,一个4月前才听说微信公众平台这个词的初学者,谢谢你们以不同方式对我的激励,我会更加努力的!
第9篇文章介绍了QQ表情的发送与接收。在此之后,很多朋友问我如何发emoji表情(微信上叫符号表情),也就让我有了写这篇文章的决心。在此之前,我在网上进行了大量的搜索,发现根本没有介绍这方面的文章,并且在微信公众帐号开发官方交流群里提问,也少有人知道该如何发送emoji表情。今天,就让我们一起来揭开它的神秘面纱!
文章概要
本文重点介绍如何在微信公众帐号开发模式下,通过程序代码向用户发送符号表情。至于如何识别用户发送的是符号表情,就不在此讲解了,留给大家一点学习思考的空间。我只是给大家一个提示:用户向公众帐号发送符号表情,其实也是一条文本消息,这与QQ表现是一样的,即然是文本消息,将接收的符号表情内容打印到日志,不就知道每个表情对应的文本了吗?呵呵,当然也没有这么简单,并不是像其他文本消息,这里需要对接收到符号表情消息先做编码的转换。好了,就提示这么多。
认识符号表情
在公众帐号的主交互界面,窗口底部的输入框旁边有一个笑脸的图片按钮,点击它将会弹出表情选择界面,可选择的表情依次为“QQ表情”、“符号表情”和“动画表情”,我们选择“符号表情”,将会看到如下图所示界面:
可以持看出,相比QQ表情,符号表情要更加实用。为什么这么说呢?因为QQ表情大都是脸部表情,而符号表情除了脸部表情外,还有很多与生活息息相关的表情,例如:动物、花朵、树木、电视、电话、电脑、吉它、球类、交通工具等等。如果能在消息中使用符号表情,会不会显得更加生动、有趣呢?
再来看看小q机器人中使用符号表情的效果,先上两张图:
左边截图是小q机器人的主菜单,在Q友圈文字旁边的那个表情就是符号表情,是一女一男两人小朋友,示意着在Q友圈里可以结识到更多的朋友,不要想歪了,^_^。右边截图是人脸识别功能的使用指南,里面的“相机”、“鬼脸”也是符号表情,这样看上去是不是更加有趣味性呢?如果是纯文本,一定会显得太单调、太枯燥了。
Emoji表情的分类
Emoji表情有很多种版本,包括Unified、DoCoMo、KDDI、Softbank和Google,而且不同版本的表情代码也不一样,更可恶的是:不同的手机操作系统、甚至是同一操作系统的不同版本所支持的emoji表情又不一样。所以,完美主义者可以止步了,因为目前emoji表情并不能保证在所有终端上都能正常使用。
庆幸的是,我已经在超过10余部终端上测试过emoji表情的使用,这其中包括iPhone 4S、iPhone 5、Android 2.2、Android 4.0+、Win8、iPad2,只有极个别终端上显示不出来或显示为一个小方格,所以并没有什么太大的影响,也就可以放心使用了!
Emoji表情代码表之Unified版本
上面介绍的几种版本的emoji表情,都是通过unicode编码来表示的。换言之,不同版本的emoji表情对应的unicode编码值也不一样。本篇文章,我先给出Unified版本emoji表情的代码表,如下图所示:
公众帐号如何向用户发送emoji表情
上面已经给出了emoji表情的unified unicode代码对照表,那么这些代码要如何使用,才能发送出对应的emoji表情呢?如果你只是简单的像使用QQ表情代码那样,直接在文本消息的Content里写emoji表情代码,一定是会原样显示的。
这里需要用到一个Java方法做转换处理,方法的代码如下:
[java] view plain copy
- /**
- * emoji表情转换(hex -> utf-16)
- *
- * @param hexEmoji
- * @return
- */
- public static String emoji(int hexEmoji) {
- return String.valueOf(Character.toChars(hexEmoji));
- }
方法说明:例如,“自行车”的unicode编码值为U+1F6B2,如果我们要在程序代码中使用“自行车”这个emoji表情,需要这样使用:
[java] view plain copy
- String bike = String.valueOf(Character.toChars(0x1F6B2));
其实前面那个emoji()方法就是对上面这行代码做了个简单的封装而以。现在知道如何使用emoji表情代码了吧,其实就是将代码表中的U+替换为0x,再调用emoji方法进行转换,将转换后的结果放在文本消息的Content中,返回给用户就会显示emoji表情了。
下面,我给出一个使用emoji表情的完整示例,如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.Date;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-05-20
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- *
- * @param request
- * @return
- */
- public static String processRequest(HttpServletRequest request) {
- String respMessage = null;
- try {
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- textMessage.setContent("自行车" + emoji(0x1F6B2) + " 男性" + emoji(0x1F6B9) + " 钱袋" + emoji(0x1F4B0));
- respMessage = MessageUtil.textMessageToXml(textMessage);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respMessage;
- }
- /**
- * emoji表情转换(hex -> utf-16)
- *
- * @param hexEmoji
- * @return
- */
- public static String emoji(int hexEmoji) {
- return String.valueOf(Character.toChars(hexEmoji));
- }
- }
上面代码的作用是:不管用户发送什么类型的消息,都返回包含三个emoji表情的文本消息。如果不明白CoreService类怎么回事,请查看本系列教程的第5篇,或者你只需要认真看第42行代码,就知道怎么样把emoji表情代码放在文本消息的Content中了。最后再来看下运行效果截图:
本篇文章要讲的内容就至此结束了,但关于emoji表情的讲解还没有结束,为什么这么说呢?请仔细看本篇文章的第二张截图,也就是小q机器人的文本菜单,里面用到的emoji表情在本文给出的emoji代码表里根本找不到(微信上的emoji表情与代码表中完全一致),那这个emoji表情又是如何发送的呢,请听下回分解!
十二、第12篇-符号表情的发送(下)
引言及文章概要
第11篇文章给出了Unified版本的符号表情(emoji表情)代码表,并且介绍了如何在微信公众帐号开发模式下发送emoji表情,还在文章结尾出,卖了个关子:“小q机器人中使用的一些符号表情,在微信的符号表情选择栏里根本找不到,并且在上篇文章给出的符号表情代码表(Unified版)中也没有,那这些表情是如何发送的呢?”如下面两张图所示的符号表情“情侣”和“公共汽车”。
本文主要介绍以下内容:1)如何在微信上使用更多的符号表情(即如何发送在微信符号表情选择栏中不存在的emoji表情);2)给出SoftBank版符号表情的代码对照表;3)介绍及演示如何发送SoftBank版本的符号表情。让大家彻底玩转微信公众帐号的emoji表情!
如何在微信上使用更多的符号表情
我们先来看下,作为一个微信用户,如何向好友或微信公众帐号发送一些微信符号表情选择栏中没有列出的符号表情。例如:小q机器人中使用的“情侣”、“公共汽车”两个符号表情,如果我想在与朋友微信聊天时使用,该怎么办呢?请先看下面的两张截图:
可以看出,当我们在输入框中输入“情侣”的全拼“qinglv”、“公共汽车”的全拼“gonggongqiche”时,输入法的文本提示列表中就会自动显示对应的符号表情,怎么样,是不是很容易呢?这类表情还有很多,例如:马桶、厕所、取款机等。
说明:笔者使用的是iPhone 4S手机系统自带的输入法做的测试,如果你用的是安卓、或者是第三方输入法,那就另当别论了。
Emoji表情代码表之SoftBank版本
上篇文章讲过,emoji表情有很多种版本,其中包括Unified、DoCoMo、KDDI、Softbank和Google,并且不同版本用于表示同一符号表情的Unicode代码也不相同。本篇文章,给出SoftBank(日本软银集团)版本的emoji表情代码表(网上一般称之为SB Unicode,指的就是它),如下图所示:
公众帐号如何向用户发送SoftBank版本的符号表情
在微信公众帐号开发模式下,发送SoftBank版的符号表情要比发送Unified版的符号表情简单的多,直接将符号表情对应的SoftBank Unicode值写在程序代码中返回给用户即可,无需做任何处理。
下面,我给出一个发送SoftBank版符号表情的示例,代码如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.Date;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-07-21
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- *
- * @param request
- * @return
- */
- public static String processRequest(HttpServletRequest request) {
- String respMessage = null;
- try {
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- textMessage.setContent("自行车\ue136 男人\ue138 钱袋\ue12f 情侣\ue428 公共汽车\ue159");
- respMessage = MessageUtil.textMessageToXml(textMessage);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respMessage;
- }
- }
上面代码的作用是:不管用户发送什么类型的消息,都返回包含5个emoji表情的文本消息。如果不明白CoreService类怎么回事,请查看本系列教程的第5篇,或者你只需要认真看第42行代码,就知道怎么样把SoftBank版emoji表情代码放在文本消息的Content中了。最后再来看下运行效果截图:
说明:每一个符号表情都有与之对应的Unified unicode、Softbank unicode代码,并不是说“情侣”、“公共汽车”这类在微信的符号表情栏中找不到的emoji表情只能通过本文的方式发送,只要你拿到与之对应的Unified unicode代码,一样可以使用上篇文章所讲的方法发送这类符号表情。
好了,关于微信公众帐号向用户发送符号表情的讲解就此结束了,相信有些朋友看完教程已经开始在帐号中使用符号表情了。其实,我更希望大家在拷贝我粘出的Unified版、SoftBank版符号表情代码表的同时,也能去了解下符号表情各种版本、Unicode编码及增补码的相关知识,不断拓展自己的知识面,触类旁通,这样才能真正地把我讲解的知识变成你自己的,才能做到以不变应万变。
十三、第13篇-图文消息全攻略
引言及内容概要
已经有几位读者抱怨“柳峰只用到文本消息作为示例,从来不提图文消息,都不知道图文消息该如何使用”,好吧,我错了,原本以为把基础API封装完、框架搭建好,再给出一个文本消息的使用示例,大家就能够照猫画虎的,或许是因为我的绘画功底太差,画出的那只猫本来就不像猫吧……
本篇主要介绍微信公众帐号开发中图文消息的使用,以及图文消息的几种表现形式。标题取名为“图文消息全攻略”,这绝对不是标题党,是想借此机会把大家对图文消息相关的问题、疑虑、障碍全部清除掉。
图文消息的主要参数说明
通过微信官方的消息接口指南,可以看到对图文消息的参数介绍,如下图所示:
从图中可以了解到:
1)图文消息的个数限制为10,也就是图中ArticleCount的值(图文消息的个数,限制在10条以内);
2)对于多图文消息,第一条图文的图片显示为大图,其他图文的图片显示为小图;
3)第一条图文的图片大小建议为640*320,其他图文的图片大小建议为80*80;
好了,了解这些,再结合第4篇文章所讲的消息及消息处理工具的封装,想要回复图文消息给用户也就不是什么难事了。
图文消息的多种表现形式
下面直接通过代码演示图文消息最主要的五种表现形式的用法,源代码如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.ArrayList;
- import java.util.Date;
- import java.util.List;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.Article;
- import org.liufeng.course.message.resp.NewsMessage;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-07-25
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- *
- * @param request
- * @return
- */
- public static String processRequest(HttpServletRequest request) {
- String respMessage = null;
- try {
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 消息类型
- String msgType = requestMap.get("MsgType");
- // 默认回复此文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- // 由于href属性值必须用双引号引起,这与字符串本身的双引号冲突,所以要转义
- textMessage.setContent("欢迎访问<a href=\"http://blog.csdn.net/lyq8479\">柳峰的博客</a>!");
- // 将文本消息对象转换成xml字符串
- respMessage = MessageUtil.textMessageToXml(textMessage);
- // 文本消息
- if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
- // 接收用户发送的文本消息内容
- String content = requestMap.get("Content");
- // 创建图文消息
- NewsMessage newsMessage = new NewsMessage();
- newsMessage.setToUserName(fromUserName);
- newsMessage.setFromUserName(toUserName);
- newsMessage.setCreateTime(new Date().getTime());
- newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);
- newsMessage.setFuncFlag(0);
- List<Article> articleList = new ArrayList<Article>();
- // 单图文消息
- if ("1".equals(content)) {
- Article article = new Article();
- article.setTitle("微信公众帐号开发教程Java版");
- article.setDescription("柳峰,80后,微信公众帐号开发经验4个月。为帮助初学者入门,特推出此系列教程,也希望借此机会认识更多同行!");
- article.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg");
- article.setUrl("http://blog.csdn.net/lyq8479");
- articleList.add(article);
- // 设置图文消息个数
- newsMessage.setArticleCount(articleList.size());
- // 设置图文消息包含的图文集合
- newsMessage.setArticles(articleList);
- // 将图文消息对象转换成xml字符串
- respMessage = MessageUtil.newsMessageToXml(newsMessage);
- }
- // 单图文消息---不含图片
- else if ("2".equals(content)) {
- Article article = new Article();
- article.setTitle("微信公众帐号开发教程Java版");
- // 图文消息中可以使用QQ表情、符号表情
- article.setDescription("柳峰,80后," + emoji(0x1F6B9)
- + ",微信公众帐号开发经验4个月。为帮助初学者入门,特推出此系列连载教程,也希望借此机会认识更多同行!\n\n目前已推出教程共12篇,包括接口配置、消息封装、框架搭建、QQ表情发送、符号表情发送等。\n\n后期还计划推出一些实用功能的开发讲解,例如:天气预报、周边搜索、聊天功能等。");
- // 将图片置为空
- article.setPicUrl("");
- article.setUrl("http://blog.csdn.net/lyq8479");
- articleList.add(article);
- newsMessage.setArticleCount(articleList.size());
- newsMessage.setArticles(articleList);
- respMessage = MessageUtil.newsMessageToXml(newsMessage);
- }
- // 多图文消息
- else if ("3".equals(content)) {
- Article article1 = new Article();
- article1.setTitle("微信公众帐号开发教程\n引言");
- article1.setDescription("");
- article1.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg");
- article1.setUrl("http://blog.csdn.net/lyq8479/article/details/8937622");
- Article article2 = new Article();
- article2.setTitle("第2篇\n微信公众帐号的类型");
- article2.setDescription("");
- article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");
- article2.setUrl("http://blog.csdn.net/lyq8479/article/details/8941577");
- Article article3 = new Article();
- article3.setTitle("第3篇\n开发模式启用及接口配置");
- article3.setDescription("");
- article3.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");
- article3.setUrl("http://blog.csdn.net/lyq8479/article/details/8944988");
- articleList.add(article1);
- articleList.add(article2);
- articleList.add(article3);
- newsMessage.setArticleCount(articleList.size());
- newsMessage.setArticles(articleList);
- respMessage = MessageUtil.newsMessageToXml(newsMessage);
- }
- // 多图文消息---首条消息不含图片
- else if ("4".equals(content)) {
- Article article1 = new Article();
- article1.setTitle("微信公众帐号开发教程Java版");
- article1.setDescription("");
- // 将图片置为空
- article1.setPicUrl("");
- article1.setUrl("http://blog.csdn.net/lyq8479");
- Article article2 = new Article();
- article2.setTitle("第4篇\n消息及消息处理工具的封装");
- article2.setDescription("");
- article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");
- article2.setUrl("http://blog.csdn.net/lyq8479/article/details/8949088");
- Article article3 = new Article();
- article3.setTitle("第5篇\n各种消息的接收与响应");
- article3.setDescription("");
- article3.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");
- article3.setUrl("http://blog.csdn.net/lyq8479/article/details/8952173");
- Article article4 = new Article();
- article4.setTitle("第6篇\n文本消息的内容长度限制揭秘");
- article4.setDescription("");
- article4.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");
- article4.setUrl("http://blog.csdn.net/lyq8479/article/details/8967824");
- articleList.add(article1);
- articleList.add(article2);
- articleList.add(article3);
- articleList.add(article4);
- newsMessage.setArticleCount(articleList.size());
- newsMessage.setArticles(articleList);
- respMessage = MessageUtil.newsMessageToXml(newsMessage);
- }
- // 多图文消息---最后一条消息不含图片
- else if ("5".equals(content)) {
- Article article1 = new Article();
- article1.setTitle("第7篇\n文本消息中换行符的使用");
- article1.setDescription("");
- article1.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg");
- article1.setUrl("http://blog.csdn.net/lyq8479/article/details/9141467");
- Article article2 = new Article();
- article2.setTitle("第8篇\n文本消息中使用网页超链接");
- article2.setDescription("");
- article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");
- article2.setUrl("http://blog.csdn.net/lyq8479/article/details/9157455");
- Article article3 = new Article();
- article3.setTitle("如果觉得文章对你有所帮助,请通过博客留言或关注微信公众帐号xiaoqrobot来支持柳峰!");
- article3.setDescription("");
- // 将图片置为空
- article3.setPicUrl("");
- article3.setUrl("http://blog.csdn.net/lyq8479");
- articleList.add(article1);
- articleList.add(article2);
- articleList.add(article3);
- newsMessage.setArticleCount(articleList.size());
- newsMessage.setArticles(articleList);
- respMessage = MessageUtil.newsMessageToXml(newsMessage);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respMessage;
- }
- /**
- * emoji表情转换(hex -> utf-16)
- *
- * @param hexEmoji
- * @return
- */
- public static String emoji(int hexEmoji) {
- return String.valueOf(Character.toChars(hexEmoji));
- }
- }
如果不明白CoreService类放在什么位置,该如何使用,请查看本系列教程的第5篇。上面代码实现的功能是当用户发送数字1-5时,分别回复五种不同表现形式的图文消息给用户,如下:
a)用户发送1,回复单图文消息。参考代码68~81行,运行效果如下:
b)用户发送2,回复单图文消息---不含图片。参考代码82~96行,运行效果如下:
说明:图文消息的标题、描述是可以包含QQ表情、符号表情的。
c)用户发送3,回复多图文消息。参考代码97~123行,运行效果如下:
说明:对于多图文消息,描述不会被显示,可以在标题使用换行符,使得显示更加美观。
d)用户发送4,回复多图文消息---首条消息不含图片。参考代码124~158行,运行效果如下:
e)用户发送5,回复多图文消息---最后一条消息不含图片。参考代码159~186行,运行效果如下:
可以看出,图文消息有着丰富的内容及多样化的表现形式,希望大家能够根据各自的特点及实际使用需要,合理地运用。
最后,根据实践经验,我对图文消息做一个使用总结:
1)一定要给图文消息的Url属性赋值。不管是单图文,还是多图文,或者是不含图片的图文,都有可能会被用户点击。如果Url为空,用户点击后将会打开一个空白页面,这给用户的体验是非常差的;
2)只有单图文的描述才会显示,多图文的描述不会被显示;
3)图文消息的标题、描述中可以使用QQ表情和符号表情。合理地运用表情符号,会使得消息更加生动;
4)图文消息的标题、描述中可以使用换行符。合理地使用换行符,会使得内容结构更加清晰;
5)图文消息的标题、描述中不支持超文本链接(html的<a>标签)。不只是技术上实现不了,就连逻辑上也说不通,因为一条图文消息的任何位置被点击,都将调用微信内置的浏览器打开Url,如果标题、描述里再放几个超链接,不知道点击该打开哪个页面。真搞不懂为什么有好几个同学都在问这个问题,难道设计成多图文不好吗?
6)图文消息的链接、图片链接可以使用外部域名下的资源,如本例中:柳峰的头像、博文的链接,都是指向CSDN网站的资源。在网上,甚至是微信官方交流群里,认为图文消息的Url、PicUrl不可以使用外链的大有人在,不知道这谣言从哪开始的,实践是检验真理的唯一标准!
7)使用指定大小的图片。第一条图文的图片大小建议为640*320,其他图文的图片大小建议为80*80。如果使用的图片太大,加载慢,而且耗流量;如果使用的图片太小,显示后会被拉伸,失真了很难看。
8)每条图文消息的图文建议控制在1-4条。这样在绝大多数终端上一屏能够显示完,用户扫一眼就能大概了解消息的主要内容,这样最有可能促使用户去点击并阅读。
十四、第14篇-自定义菜单的创建及菜单事件响应
微信5.0发布
2013年8月5日,伴随着微信5.0
iPhone版的发布,公众平台也进行了重要的更新,主要包括:
1)运营主体为组织,可选择成为服务号或者订阅号;
2)服务号可以申请自定义菜单;
3)使用QQ登录的公众号,可以升级为邮箱登录;
4)使用邮箱登录的公众号,可以修改登录邮箱;
5)编辑图文消息可选填作者;
6)群发消息可以同步到腾讯微博。
其中,大家议论最多的当属前两条,就是关于帐号类型和自定义菜单的更新,我这里做几点补充说明:
1)目前公众号类型分为两种:服务号和订阅号,8月5日平台更新后所有的帐号默认为订阅号,有一次转换成服务号的机会;
2)服务号主要面向企业、政府和其他组织,而订阅号主要面向媒体和个人;
3)只有服务号可以申请自定义菜单,订阅号不能申请;
4)服务号每月只能群发一条消息,而订阅号每天能群发一条消息。
平台更新后,让很多人纠结的是自定义菜单和每天群发一条消息不可兼得,对此,我不想过多评论。
引言及内容概要
在微信5.0以前,自定义菜单是作为一种内测资格使用的,只有少数公众帐号拥有菜单,因此出现很多企业为了弄到菜单不惜重金求购。现如今,一大批帐号从订阅号转为服务号,很多都是奔着自定义菜单去的。而且,经测试发现,微信最近的审核放松很多,只要申请服务号、自定义菜单的基本都成功了,根本不管填写的资料真伪。不知道以后微信会不会翻脸,要求补全企业资料,那将会是一种给小孩一颗糖吃再把他打哭的感觉。。。
自定义菜单是申请到了,到底该怎么创建、怎么使用呢?最近几天不管是微信官方交流群,还是在我博客留言里,都能够看到不少开发者都在为这个发愁。本篇文章就为大家解决这个难题。
自定义菜单的创建步骤
1、找到AppId和AppSecret。自定义菜单申请成功后,在“高级功能”-“开发模式”-“接口配置信息”的最后两项就是;
2、根据AppId和AppSecret,以https get方式获取访问特殊接口所必须的凭证access_token;
3、根据access_token,将json格式的菜单数据通过https
post方式提交。
分析创建菜单的难点
原来创建菜单这么简单,三步就能搞定?跟把大象放冰箱差不多。呵呵,当然没有这么简单,那我们一步步来看,到底难在哪里?
首先,第1步肯定都没有问题,只要成功申请了自定义菜单,一定能拿到AppId和AppSecret这两个值。
再来看第2步,由于是get方式获取access_token,很多人直接把拼好的url放在浏览器里执行,access_token就拿到了。抛开是不是用编程方式实现的来说,这真是个好办法,显然大家在第二步上也没有问题。
最后再看第3步,拼装json格式的菜单数据,虽然繁锁一点,但基本上也都没有什么问题的,因为官方给了个例子,照猫画虎就行了。那问题一定就出现在https post提交上了。
结论:不知道如何创建自定义菜单的朋友,大都可以归为以下三种情况:
1)根本不看或者没看懂公众平台API文档中关于“通用接口”、“自定义菜单接口”和“使用限制”部分的说明;
2)不知道如何发起HTTPS请求(平时的http请求,直接使用HttpUrlConnection就可以轻松搞定,但https请求要复杂一点);
3)不知道如何通过POST方式提交json格式的菜单数据。
正在看文章的你,不知道是属于哪一种,或者几种情况都有,不妨留言说出来,也可以做个调查。不管属于哪一种情况,既然看到了这篇文章,相信一定会让你弄明白的。
解读通用接口文档---凭证的获取
我们先来看通用接口文档的简介部分,如下图所示。
通俗点讲,这段简介可以这么理解:公众平台还有很多特殊的接口,像自定义菜单的创建、语音文件的获取、主动发送消息等,如果开发者想通过HTTP请求访问这些特殊接口,就必须要有访问凭证,也就是access_token。
那么,又该如何获取接口访问凭证access_token呢?让我们继续往下看。
图中已经表达的很清楚了,获取access_token是通过GET方式访问如下链接:
[java] view plain copy
- https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
链接中有三个参数,分别是grant_type、appid和secret。根据图中的参数说明,grant_type传固定值client_credential,而appid和secret就是申请完自定义菜单后微信分配给我们的。
请求发送成功后,微信服务器会返回一个json串,包含access_token和expires_in两个元素。其中,access_token就是我们最终需要的凭证,而expires_in是凭证的有效期,单位是秒,7200秒也就是2个小时。这就意味着,不是每次访问特殊接口,都需要重新获取一次access_token,只要access_token还在有效期内,就一直可以使用。
解读自定义菜单接口文档
还是一样,先来看看自定义菜单接口的简介部分,如下图所示。
从图中我们能够获取到以下信息:
1)拿到凭证access_token后,我们能对菜单执行三种操作:创建、查询和删除;
2)自定义菜单目前只支持click一种事件,即用户点击后回复某种类型的消息;不能够实现点击菜单项直接打开页面(type=view未开放,目前只是微生活有);
3)由于微信客户端缓存的原因,菜单创建后并不会立即在微信上显示出来,需要过24小时。在测试菜单创建时,可以通过取消关注后,再关注的方式达到立即看菜单的目的。
继续往下看,就是关于菜单怎么创建的介绍了,如下图所示。
其实就是向地址https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN以POST方式提交一个JSON格式的菜单字符串。
后面,关于参数说明的部分我就不一一贴图说明了,把重点说一下:
1)自定义菜单是一个3x5结构的,即菜单最多只能有二级,一级菜单最多只能有3个,每个一级菜单下最多可以有5个二级菜单项;
2)菜单项都有一个key值。当用户点击某个菜单项时,微信会将该菜单项的key值以事件推送的方式发送给我们的后台处理程序。
关于菜单的查询、创建我就不提了,这两个接口使用的频率非常小,一般都用不上。如果需要,再按照我上面提供的思路也不难理解。
解读API文档之使用限制
很多小伙伴看到这张图就开始疑惑了:怎么菜单还限制使用次数,用户量越来越大的时候,根本不够用啊。看清楚,这个限制是针对接口调用的,也就是针对开发者的,和用户数、使用次数半点关系也没有。
就先拿获取凭证接口来说吧,限制一天只能调用200次。还记得前面提到过access_token是有有效期的,并且有效期为两小时,也就是获取一次access_token后的两小时内,都可以继续使用,那么理想情况一天24小时内,是不是只需要获取12次就够了?难道200次还不够用?
再来看下菜单创建接口限制一天只能调用100次。我就这么解释吧,菜单创建一次后,只要你不切换模式(指的是在编辑模式和开发模式间切换)、不调用删除接口,这个菜单会永远存在的。谁没事干,一天要创建100次菜单,就算是测试,测个10次8次足够了吧?
菜单的查询和删除接口的限制我就不解释了,至今为止这二个接口我都没使用过一次。就算有这样的使用需求,一天这么多次的调用,完全足够了。
封装通用的请求方法
读到这里,就默认大家已经掌握了上面讲到的所有关于自定义菜单的理论知识,下面就进入代码实战讲解的部分。
先前我们了解到,创建菜单需要调用二个接口,并且都是https请求,而非http。如果要封装一个通用的请求方法,该方法至少需要具备以下能力:
1)支持HTTPS请求;
2)支持GET、POST两种方式;
3)支持参数提交,也支持无参数的情况;
对于https请求,我们需要一个证书信任管理器,这个管理器类需要自己定义,但需要实现X509TrustManager接口,代码如下:
[java] view plain copy
- package org.liufeng.weixin.util;
- import java.security.cert.CertificateException;
- import java.security.cert.X509Certificate;
- import javax.net.ssl.X509TrustManager;
- /**
- * 证书信任管理器(用于https请求)
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class MyX509TrustManager implements X509TrustManager {
- public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- }
- public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- }
- public X509Certificate[] getAcceptedIssuers() {
- return null;
- }
- }
这个证书管理器的作用就是让它信任我们指定的证书,上面的代码意味着信任所有证书,不管是否权威机构颁发。
证书有了,通用的https请求方法就不难实现了,实现代码如下:
[java] view plain copy
- package org.liufeng.weixin.util;
- import java.io.BufferedReader;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.io.OutputStream;
- import java.net.ConnectException;
- import java.net.URL;
- import javax.net.ssl.HttpsURLConnection;
- import javax.net.ssl.SSLContext;
- import javax.net.ssl.SSLSocketFactory;
- import javax.net.ssl.TrustManager;
- import net.sf.json.JSONObject;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- /**
- * 公众平台通用接口工具类
- *
- * @author liuyq
- * @date 2013-08-09
- */
- public class WeixinUtil {
- private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);
- /**
- * 发起https请求并获取结果
- *
- * @param requestUrl 请求地址
- * @param requestMethod 请求方式(GET、POST)
- * @param outputStr 提交的数据
- * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
- */
- public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
- JSONObject jsonObject = null;
- StringBuffer buffer = new StringBuffer();
- try {
- // 创建SSLContext对象,并使用我们指定的信任管理器初始化
- TrustManager[] tm = { new MyX509TrustManager() };
- SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
- sslContext.init(null, tm, new java.security.SecureRandom());
- // 从上述SSLContext对象中得到SSLSocketFactory对象
- SSLSocketFactory ssf = sslContext.getSocketFactory();
- URL url = new URL(requestUrl);
- HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
- httpUrlConn.setSSLSocketFactory(ssf);
- httpUrlConn.setDoOutput(true);
- httpUrlConn.setDoInput(true);
- httpUrlConn.setUseCaches(false);
- // 设置请求方式(GET/POST)
- httpUrlConn.setRequestMethod(requestMethod);
- if ("GET".equalsIgnoreCase(requestMethod))
- httpUrlConn.connect();
- // 当有数据需要提交时
- if (null != outputStr) {
- OutputStream outputStream = httpUrlConn.getOutputStream();
- // 注意编码格式,防止中文乱码
- outputStream.write(outputStr.getBytes("UTF-8"));
- outputStream.close();
- }
- // 将返回的输入流转换成字符串
- InputStream inputStream = httpUrlConn.getInputStream();
- InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
- BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
- String str = null;
- while ((str = bufferedReader.readLine()) != null) {
- buffer.append(str);
- }
- bufferedReader.close();
- inputStreamReader.close();
- // 释放资源
- inputStream.close();
- inputStream = null;
- httpUrlConn.disconnect();
- jsonObject = JSONObject.fromObject(buffer.toString());
- } catch (ConnectException ce) {
- log.error("Weixin server connection timed out.");
- } catch (Exception e) {
- log.error("https request error:{}", e);
- }
- return jsonObject;
- }
- }
代码说明:
1)41~50行:解决https请求的问题,很多人问题就出在这里;
2)55~59行:兼容GET、POST两种方式;
3)61~67行:兼容有数据提交、无数据提交两种情况,也有相当一部分人不知道如何POST提交数据;
Pojo类的封装
在获取凭证创建菜单前,我们还需要封装一些pojo,这会让我们的代码更美观,有条理。
首先是调用获取凭证接口后,微信服务器会返回json格式的数据:{"access_token":"ACCESS_TOKEN","expires_in":7200},我们将其封装为一个AccessToken对象,对象有二个属性:token和expiresIn,代码如下:
[java] view plain copy
- package org.liufeng.weixin.pojo;
- /**
- * 微信通用接口凭证
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class AccessToken {
- // 获取到的凭证
- private String token;
- // 凭证有效时间,单位:秒
- private int expiresIn;
- public String getToken() {
- return token;
- }
- public void setToken(String token) {
- this.token = token;
- }
- public int getExpiresIn() {
- return expiresIn;
- }
- public void setExpiresIn(int expiresIn) {
- this.expiresIn = expiresIn;
- }
- }
接下来是对菜单结构的封装。因为我们是采用面向对象的编程方式,最终提交的json格式菜单数据就应该是由对象直接转换得到,而不是在程序代码中拼一大堆json数据。菜单结构封装的依据是公众平台API文档中给出的那一段json格式的菜单结构,如下所示:
[java] view plain copy
- {
- "button":[
- {
- "type":"click",
- "name":"今日歌曲",
- "key":"V1001_TODAY_MUSIC"
- },
- {
- "type":"click",
- "name":"歌手简介",
- "key":"V1001_TODAY_SINGER"
- },
- {
- "name":"菜单",
- "sub_button":[
- {
- "type":"click",
- "name":"hello word",
- "key":"V1001_HELLO_WORLD"
- },
- {
- "type":"click",
- "name":"赞一下我们",
- "key":"V1001_GOOD"
- }]
- }]
- }
首先是菜单项的基类,所有一级菜单、二级菜单都共有一个相同的属性,那就是name。菜单项基类的封装代码如下:
[java] view plain copy
- package org.liufeng.weixin.pojo;
- /**
- * 按钮的基类
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class Button {
- private String name;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
接着是子菜单项的封装。这里对子菜单是这样定义的:没有子菜单的菜单项,有可能是二级菜单项,也有可能是不含二级菜单的一级菜单。这类子菜单项一定会包含三个属性:type、name和key,封装的代码如下:
[java] view plain copy
- package org.liufeng.weixin.pojo;
- /**
- * 普通按钮(子按钮)
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class CommonButton extends Button {
- private String type;
- private String 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;
- }
- }
再往下是父菜单项的封装。对父菜单项的定义:包含有二级菜单项的一级菜单。这类菜单项包含有二个属性:name和sub_button,而sub_button以是一个子菜单项数组。父菜单项的封装代码如下:
[java] view plain copy
- package org.liufeng.weixin.pojo;
- /**
- * 复杂按钮(父按钮)
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class ComplexButton extends Button {
- private Button[] sub_button;
- public Button[] getSub_button() {
- return sub_button;
- }
- public void setSub_button(Button[] sub_button) {
- this.sub_button = sub_button;
- }
- }
最后是整个菜单对象的封装,菜单对象包含多个菜单项(最多只能有3个),这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项),如果能明白上面所讲的,再来看封装后的代码就很容易理解了:
[java] view plain copy
- package org.liufeng.weixin.pojo;
- /**
- * 菜单
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class Menu {
- private Button[] button;
- public Button[] getButton() {
- return button;
- }
- public void setButton(Button[] button) {
- this.button = button;
- }
- }
关于POJO类的封装就介绍完了。
凭证access_token的获取方法
继续在先前通用请求方法的类WeixinUtil.Java中加入以下代码,用于获取接口访问凭证:
[java] view plain copy
- // 获取access_token的接口地址(GET) 限200(次/天)
- public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
- /**
- * 获取access_token
- *
- * @param appid 凭证
- * @param appsecret 密钥
- * @return
- */
- public static AccessToken getAccessToken(String appid, String appsecret) {
- AccessToken accessToken = null;
- String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
- JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
- // 如果请求成功
- if (null != jsonObject) {
- try {
- accessToken = new AccessToken();
- accessToken.setToken(jsonObject.getString("access_token"));
- accessToken.setExpiresIn(jsonObject.getInt("expires_in"));
- } catch (JSONException e) {
- accessToken = null;
- // 获取token失败
- log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
- }
- }
- return accessToken;
- }
自定义菜单的创建方法
继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于创建自定义菜单:
[java] view plain copy
- // 菜单创建(POST) 限100(次/天)
- public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
- /**
- * 创建菜单
- *
- * @param menu 菜单实例
- * @param accessToken 有效的access_token
- * @return 0表示成功,其他值表示失败
- */
- public static int createMenu(Menu menu, String accessToken) {
- int result = 0;
- // 拼装创建菜单的url
- String url = menu_create_url.replace("ACCESS_TOKEN", accessToken);
- // 将菜单对象转换成json字符串
- String jsonMenu = JSONObject.fromObject(menu).toString();
- // 调用接口创建菜单
- JSONObject jsonObject = httpRequest(url, "POST", jsonMenu);
- if (null != jsonObject) {
- if (0 != jsonObject.getInt("errcode")) {
- result = jsonObject.getInt("errcode");
- log.error("创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
- }
- }
- return result;
- }
调用封装的方法创建自定义菜单
[java] view plain copy
- package org.liufeng.weixin.main;
- import org.liufeng.weixin.pojo.AccessToken;
- import org.liufeng.weixin.pojo.Button;
- import org.liufeng.weixin.pojo.CommonButton;
- import org.liufeng.weixin.pojo.ComplexButton;
- import org.liufeng.weixin.pojo.Menu;
- import org.liufeng.weixin.util.WeixinUtil;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- /**
- * 菜单管理器类
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class MenuManager {
- private static Logger log = LoggerFactory.getLogger(MenuManager.class);
- public static void main(String[] args) {
- // 第三方用户唯一凭证
- String appId = "000000000000000000";
- // 第三方用户唯一凭证密钥
- String appSecret = "00000000000000000000000000000000";
- // 调用接口获取access_token
- AccessToken at = WeixinUtil.getAccessToken(appId, appSecret);
- if (null != at) {
- // 调用接口创建菜单
- int result = WeixinUtil.createMenu(getMenu(), at.getToken());
- // 判断菜单创建结果
- if (0 == result)
- log.info("菜单创建成功!");
- else
- log.info("菜单创建失败,错误码:" + result);
- }
- }
- /**
- * 组装菜单数据
- *
- * @return
- */
- private static Menu getMenu() {
- CommonButton btn11 = new CommonButton();
- btn11.setName("天气预报");
- btn11.setType("click");
- btn11.setKey("11");
- CommonButton btn12 = new CommonButton();
- btn12.setName("公交查询");
- btn12.setType("click");
- btn12.setKey("12");
- CommonButton btn13 = new CommonButton();
- btn13.setName("周边搜索");
- btn13.setType("click");
- btn13.setKey("13");
- CommonButton btn14 = new CommonButton();
- btn14.setName("历史上的今天");
- btn14.setType("click");
- btn14.setKey("14");
- CommonButton btn21 = new CommonButton();
- btn21.setName("歌曲点播");
- btn21.setType("click");
- btn21.setKey("21");
- CommonButton btn22 = new CommonButton();
- btn22.setName("经典游戏");
- btn22.setType("click");
- btn22.setKey("22");
- CommonButton btn23 = new CommonButton();
- btn23.setName("美女电台");
- btn23.setType("click");
- btn23.setKey("23");
- CommonButton btn24 = new CommonButton();
- btn24.setName("人脸识别");
- btn24.setType("click");
- btn24.setKey("24");
- CommonButton btn25 = new CommonButton();
- btn25.setName("聊天唠嗑");
- btn25.setType("click");
- btn25.setKey("25");
- CommonButton btn31 = new CommonButton();
- btn31.setName("Q友圈");
- btn31.setType("click");
- btn31.setKey("31");
- CommonButton btn32 = new CommonButton();
- btn32.setName("电影排行榜");
- btn32.setType("click");
- btn32.setKey("32");
- CommonButton btn33 = new CommonButton();
- btn33.setName("幽默笑话");
- btn33.setType("click");
- btn33.setKey("33");
- ComplexButton mainBtn1 = new ComplexButton();
- mainBtn1.setName("生活助手");
- mainBtn1.setSub_button(new CommonButton[] { btn11, btn12, btn13, btn14 });
- ComplexButton mainBtn2 = new ComplexButton();
- mainBtn2.setName("休闲驿站");
- mainBtn2.setSub_button(new CommonButton[] { btn21, btn22, btn23, btn24, btn25 });
- ComplexButton mainBtn3 = new ComplexButton();
- mainBtn3.setName("更多体验");
- mainBtn3.setSub_button(new CommonButton[] { btn31, btn32, btn33 });
- /**
- * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项<br>
- *
- * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢?<br>
- * 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义:<br>
- * menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 });
- */
- Menu menu = new Menu();
- menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 });
- return menu;
- }
- }
注意:在运行以上代码时,需要将appId和appSecret换成你自己公众号的。
整个工程的结构
为了保证文章的完整独立性和可读性,我是新建了一个JavaProject(Java web工程也可以,没有太大关系),没有在前几篇文章所讲到的weixinCourse工程中添加代码。如果需要,读者可以自己实现将菜单创建的代码移到自己已有的工程中去。
图中所有Java文件的源代码都在文章中贴出并进行了说明,图中使用到的jar也是Java开发中通用的jar包,很容易在网上下载到。
工程中引入的jar包主要分为两类:
1)第一类是json开发工具包,用于Java对象和Json字符串之间的转换;json开发工具包一共有3个jar:ezmorph-1.0.6.jar,json-lib-2.2.3-jdk13.jar和morph-1.1.1.jar。
2)第二类是slf4j日志工具包,用于记录系统运行所产生的日志,日志可以输出到控制台或文件中。
整个工程中,唯一没有讲到的是src下的log4j.properties的配置,也把它贴出来,方便大家参考,这样才是一个完整的工程源码。log4j.properties文件的内容如下:
[java] view plain copy
- log4j.rootLogger=info,console,file
- log4j.appender.console=org.apache.log4j.ConsoleAppender
- log4j.appender.console.layout=org.apache.log4j.PatternLayout
- log4j.appender.console.layout.ConversionPattern=[%-5p] %m%n
- log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
- log4j.appender.file.DatePattern=‘-‘yyyy-MM-dd
- log4j.appender.file.File=./logs/weixinmpmenu.log
- log4j.appender.file.Append=true
- log4j.appender.file.layout=org.apache.log4j.PatternLayout
- log4j.appender.file.layout.ConversionPattern=[%-5p] %d %37c %3x - %m%n
如何响应菜单点击事件
自定义菜单的创建工作已经完成,那么该如何接收和响应菜单的点击事件呢,也就是说在公众帐号后台处理程序中,如何识别用户点击的是哪个菜单,以及做出响应。这部分内容其实在教程的第5篇各种消息的接收与响应中已经讲解清楚了。
来看一下第一篇教程weixinCourse项目中的CoreService类要怎么改写,才能接收响应菜单点击事件,该类修改后的完整代码如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.Date;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-05-20
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- *
- * @param request
- * @return
- */
- public static String processRequest(HttpServletRequest request) {
- String respMessage = null;
- try {
- // 默认返回的文本消息内容
- String respContent = "请求处理异常,请稍候尝试!";
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 消息类型
- String msgType = requestMap.get("MsgType");
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- // 文本消息
- if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {
- respContent = "您发送的是文本消息!";
- }
- // 图片消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {
- respContent = "您发送的是图片消息!";
- }
- // 地理位置消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {
- respContent = "您发送的是地理位置消息!";
- }
- // 链接消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {
- respContent = "您发送的是链接消息!";
- }
- // 音频消息
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {
- respContent = "您发送的是音频消息!";
- }
- // 事件推送
- else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {
- // 事件类型
- String eventType = requestMap.get("Event");
- // 订阅
- if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {
- respContent = "谢谢您的关注!";
- }
- // 取消订阅
- else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {
- // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息
- }
- // 自定义菜单点击事件
- else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {
- // 事件KEY值,与创建自定义菜单时指定的KEY值对应
- String eventKey = requestMap.get("EventKey");
- if (eventKey.equals("11")) {
- respContent = "天气预报菜单项被点击!";
- } else if (eventKey.equals("12")) {
- respContent = "公交查询菜单项被点击!";
- } else if (eventKey.equals("13")) {
- respContent = "周边搜索菜单项被点击!";
- } else if (eventKey.equals("14")) {
- respContent = "历史上的今天菜单项被点击!";
- } else if (eventKey.equals("21")) {
- respContent = "歌曲点播菜单项被点击!";
- } else if (eventKey.equals("22")) {
- respContent = "经典游戏菜单项被点击!";
- } else if (eventKey.equals("23")) {
- respContent = "美女电台菜单项被点击!";
- } else if (eventKey.equals("24")) {
- respContent = "人脸识别菜单项被点击!";
- } else if (eventKey.equals("25")) {
- respContent = "聊天唠嗑菜单项被点击!";
- } else if (eventKey.equals("31")) {
- respContent = "Q友圈菜单项被点击!";
- } else if (eventKey.equals("32")) {
- respContent = "电影排行榜菜单项被点击!";
- } else if (eventKey.equals("33")) {
- respContent = "幽默笑话菜单项被点击!";
- }
- }
- }
- textMessage.setContent(respContent);
- respMessage = MessageUtil.textMessageToXml(textMessage);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respMessage;
- }
- }
代码说明:
1)第69行、第81行这两行代码说明了如何判断菜单的点击事件。当消息类型MsgType=event,并且Event=CLICK时,就表示是自定义菜单点击事件;
2)第83行是判断具体点击的是哪个菜单项,根据菜单的key值来判断;
3)第85~109行表示当用户点击某个菜单项后,具体返回什么消息,我只是做个简单示例,统一返回文本消息,读者可以根据实际需要来灵活处理。
总结
到这里关于自定义菜单的创建、菜单事件的判断和处理响应就全部介绍完了。我只希望看过文章的人不要只是拷贝代码,如果是这样,我完全不用花这么多的时间来写这篇文章,直接把工程放在下载区多简单。另外,网上是有很多工具,让你填入appid,appsecret和菜单结构,提交就能创建菜单,请慎用!因为appid和appsecret一旦告诉别人,你的公众号的菜单控制权就在别人手上了,总会有别有用心的人出来搞点事的。
十五、第15篇-自定义菜单的view类型(访问网页)
引言及内容概要
距离写上一篇文章《自定义菜单的创建及菜单事件响应》整整过了两个月的时间,那时公众平台还没有开放view类型的菜单。在不久前,微信公众平台悄悄开放了view类型的菜单,却没有在首页发布任何通知,貌似微信团队很喜欢这么干。一个偶然的机会,我留意到API文档的自定义菜单接口发生了变化,增加了对菜单view类型的说明:
view(访问网页):
用户点击view类型按钮后,会直接跳转到开发者指定的url中。
于是我在第一时间更新了小q机器人(微信号:xiaoqrobot)的菜单,在一级菜单“更多”下增加了二级菜单“使用帮助”,点击该菜单项会直接跳转到网页,如下图所示。
最近也有不少网友问起这种类型的菜单是如何创建的,本篇文章就为大家介绍下view类型的自定义菜单该如何创建。
自定义菜单的两种类型(click和view)
公众平台API文档中给出了自定义菜单的json结构示例,我从中截取两个菜单项的json代码,一个是click类型,另一个是view类型,如下所示。
[html] view plain copy
- {
- "type":"click",
- "name":"今日歌曲",
- "key":"V1001_TODAY_MUSIC"
- },
- {
- "type":"view",
- "name":"歌手简介",
- "url":"http://www.qq.com/"
- }
从上面可以看出,两种类型的菜单除了type值不同之外,属性也有差别。click类型的菜单有key属性,而view类型的菜单没有key属性,与之对应的是url属性。通过上一篇的学习我们知道,key值是用于判断用户点击了哪个click类型的菜单项。而view类型的菜单没有key属性,目前无法在公众账号后台判断是否有用户点击了view类型的菜单项,也就没办法知道哪个用户点击了view类型的菜单项。
建立view类型的菜单对象
View类型的菜单有3个属性:type、name和url。在上一篇文章中,我们创建了菜单项的基类Button,Button类只有一个属性name。View类型的菜单对象也需要继承Button类,代码如下:
[java] view plain copy
- package org.liufeng.weixin.pojo;
- /**
- * view类型的菜单
- *
- * @author liuyq
- * @date 2013-04-10
- */
- public class ViewButton extends Button {
- private String type;
- private String 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;
- }
- }
创建带view类型的菜单示例
我们对前一篇文章中给出的菜单创建代码进行调整,增加view类型的菜单项,完整的菜单创建代码如下:
[java] view plain copy
- package org.liufeng.weixin.main;
- import org.liufeng.weixin.pojo.AccessToken;
- import org.liufeng.weixin.pojo.Button;
- import org.liufeng.weixin.pojo.CommonButton;
- import org.liufeng.weixin.pojo.ComplexButton;
- import org.liufeng.weixin.pojo.Menu;
- import org.liufeng.weixin.pojo.ViewButton;
- import org.liufeng.weixin.util.WeixinUtil;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- /**
- * 菜单管理器类
- *
- * @author liufeng
- * @date 2013-08-08
- */
- public class MenuManager {
- private static Logger log = LoggerFactory.getLogger(MenuManager.class);
- public static void main(String[] args) {
- // 第三方用户唯一凭证
- String appId = "000000000000000000";
- // 第三方用户唯一凭证密钥
- String appSecret = "00000000000000000000000000000000";
- // 调用接口获取access_token
- AccessToken at = WeixinUtil.getAccessToken(appId, appSecret);
- if (null != at) {
- // 调用接口创建菜单
- int result = WeixinUtil.createMenu(getMenu(), at.getToken());
- // 判断菜单创建结果
- if (0 == result)
- log.info("菜单创建成功!");
- else
- log.info("菜单创建失败,错误码:" + result);
- }
- }
- /**
- * 组装菜单数据
- *
- * @return
- */
- private static Menu getMenu() {
- CommonButton btn11 = new CommonButton();
- btn11.setName("天气预报");
- btn11.setType("click");
- btn11.setKey("11");
- CommonButton btn12 = new CommonButton();
- btn12.setName("公交查询");
- btn12.setType("click");
- btn12.setKey("12");
- CommonButton btn13 = new CommonButton();
- btn13.setName("周边搜索");
- btn13.setType("click");
- btn13.setKey("13");
- CommonButton btn14 = new CommonButton();
- btn14.setName("历史上的今天");
- btn14.setType("click");
- btn14.setKey("14");
- CommonButton btn15 = new CommonButton();
- btn15.setName("电影排行榜");
- btn15.setType("click");
- btn15.setKey("32");
- CommonButton btn21 = new CommonButton();
- btn21.setName("歌曲点播");
- btn21.setType("click");
- btn21.setKey("21");
- CommonButton btn22 = new CommonButton();
- btn22.setName("经典游戏");
- btn22.setType("click");
- btn22.setKey("22");
- CommonButton btn23 = new CommonButton();
- btn23.setName("美女电台");
- btn23.setType("click");
- btn23.setKey("23");
- CommonButton btn24 = new CommonButton();
- btn24.setName("人脸识别");
- btn24.setType("click");
- btn24.setKey("24");
- CommonButton btn25 = new CommonButton();
- btn25.setName("聊天唠嗑");
- btn25.setType("click");
- btn25.setKey("25");
- CommonButton btn31 = new CommonButton();
- btn31.setName("Q友圈");
- btn31.setType("click");
- btn31.setKey("31");
- CommonButton btn33 = new CommonButton();
- btn33.setName("幽默笑话");
- btn33.setType("click");
- btn33.setKey("33");
- CommonButton btn34 = new CommonButton();
- btn34.setName("用户反馈");
- btn34.setType("click");
- btn34.setKey("34");
- CommonButton btn35 = new CommonButton();
- btn35.setName("关于我们");
- btn35.setType("click");
- btn35.setKey("35");
- ViewButton btn32 = new ViewButton();
- btn32.setName("使用帮助");
- btn32.setType("view");
- btn32.setUrl("http://liufeng.gotoip2.com/xiaoqrobot/help.jsp");
- ComplexButton mainBtn1 = new ComplexButton();
- mainBtn1.setName("生活助手");
- mainBtn1.setSub_button(new Button[] { btn11, btn12, btn13, btn14, btn15 });
- ComplexButton mainBtn2 = new ComplexButton();
- mainBtn2.setName("休闲驿站");
- mainBtn2.setSub_button(new Button[] { btn21, btn22, btn23, btn24, btn25 });
- ComplexButton mainBtn3 = new ComplexButton();
- mainBtn3.setName("更多");
- mainBtn3.setSub_button(new Button[] { btn31, btn33, btn34, btn35, btn32 });
- /**
- * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项<br>
- *
- * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢?<br>
- * 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义:<br>
- * menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 });
- */
- Menu menu = new Menu();
- menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 });
- return menu;
- }
- }
119~122行代码就是用于创建view类型菜单项的。上面的菜单结构也是小q机器人(微信号:xiaoqrobot)目前在使用的,读者可以对照着理解。
补充:居然还有些网友问我上面的自定义菜单创建代码在哪里调用,问我为什么不把调用的代码也公开?搞的我都有点不好意思了。上面的菜单创建代码是写在main方法中,直接在开发工具中运行一下就可以了,这应该是最最基础的Java知识了。另外,菜单只要创建一次,会一直存在的,不需要每次启动应用程序都去创建菜单。当然,你也可以把菜单的创建代码集成到项目中,并非一定要放在main方法中。程序是死的,人是活的,解决方法往往有很多种,怎么方便实用就怎么来!
十六、第16篇-应用实例之历史上的今天
内容概要
本篇文章主要讲解如何在微信公众帐号上实现“历史上的今天”功能。这个例子本身并不复杂,但希望通过对它的学习,读者能够对正则表达式有一个新的认识,能够学会运用现有的网络资源丰富自己的公众账号。
何谓历史上的今天
回顾历史的长河,历史是生活的一面镜子;以史为鉴,可以知兴衰;历史上的每一天,都是喜忧参半;可以了解历史的这一天发生的事件,借古可以鉴今,历史是不能忘记的。查看历史上每天发生的重大事情,增长知识,开拓眼界,提高人文素养。
寻找接口(数据源)
要实现查询“历史上的今天”,首先我们要找到相关数据源。笔者经过搜索发现,网络上几乎没有现成的“历史上的今天”API可以使用,所以我们只能通过爬取、解析网页源代码的方式得到我们需要的数据。笔者发现网站http://www.rijiben.com/上包含“历史上的今天”功能,就用它做数据源了。
开发步骤
为了便于读者理解,我们需要清楚该应用实例的开发步骤,主要如下:
1)发起HTTP GET请求,获取网页源代码。
2)运用正则表达式从网页源代码中抽取我们需要的数据。
3)对抽取得到的数据进行加工(使内容呈现更加美观)。
4)将以上三步进行封装,供外部调用。
5)在公众账号后台调用封装好的“历史上的今天”查询方法。
代码实现
笔者将上述步骤1)、2)、3)中的代码实现封装成了TodayInHistoryService类,并对外提供了getTodayInHistory()方法来获取“历史上的今天”。实现代码如下:
[java] view plain copy
- import java.io.BufferedReader;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import java.text.DateFormat;
- import java.text.SimpleDateFormat;
- import java.util.Calendar;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
- /**
- * 历史上的今天查询服务
- *
- * @author liufeng
- * @date 2013-10-16
- *
- */
- public class TodayInHistoryService {
- /**
- * 发起http get请求获取网页源代码
- *
- * @param requestUrl
- * @return
- */
- private static String httpRequest(String requestUrl) {
- StringBuffer buffer = null;
- try {
- // 建立连接
- URL url = new URL(requestUrl);
- HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection();
- httpUrlConn.setDoInput(true);
- httpUrlConn.setRequestMethod("GET");
- // 获取输入流
- InputStream inputStream = httpUrlConn.getInputStream();
- InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
- BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
- // 读取返回结果
- buffer = new StringBuffer();
- String str = null;
- while ((str = bufferedReader.readLine()) != null) {
- buffer.append(str);
- }
- // 释放资源
- bufferedReader.close();
- inputStreamReader.close();
- inputStream.close();
- httpUrlConn.disconnect();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return buffer.toString();
- }
- /**
- * 从html中抽取出历史上的今天信息
- *
- * @param html
- * @return
- */
- private static String extract(String html) {
- StringBuffer buffer = null;
- // 日期标签:区分是昨天还是今天
- String dateTag = getMonthDay(0);
- Pattern p = Pattern.compile("(.*)(<div class=\"listren\">)(.*?)(</div>)(.*)");
- Matcher m = p.matcher(html);
- if (m.matches()) {
- buffer = new StringBuffer();
- if (m.group(3).contains(getMonthDay(-1)))
- dateTag = getMonthDay(-1);
- // 拼装标题
- buffer.append("≡≡ ").append("历史上的").append(dateTag).append(" ≡≡").append("\n\n");
- // 抽取需要的数据
- for (String info : m.group(3).split(" ")) {
- info = info.replace(dateTag, "").replace("(图)", "").replaceAll("</?[^>]+>", "").trim();
- // 在每行末尾追加2个换行符
- if (!"".equals(info)) {
- buffer.append(info).append("\n\n");
- }
- }
- }
- // 将buffer最后两个换行符移除并返回
- return (null == buffer) ? null : buffer.substring(0, buffer.lastIndexOf("\n\n"));
- }
- /**
- * 获取前/后n天日期(M月d日)
- *
- * @return
- */
- private static String getMonthDay(int diff) {
- DateFormat df = new SimpleDateFormat("M月d日");
- Calendar c = Calendar.getInstance();
- c.add(Calendar.DAY_OF_YEAR, diff);
- return df.format(c.getTime());
- }
- /**
- * 封装历史上的今天查询方法,供外部调用
- *
- * @return
- */
- public static String getTodayInHistoryInfo() {
- // 获取网页源代码
- String html = httpRequest("http://www.rijiben.com/");
- // 从网页中抽取信息
- String result = extract(html);
- return result;
- }
- /**
- * 通过main在本地测试
- *
- * @param args
- */
- public static void main(String[] args) {
- String info = getTodayInHistoryInfo();
- System.out.println(info);
- }
- }
代码解读:
1)27-58行代码是httpRequest()方法,用于发起http get请求,获取指定url的网页源代码。
2)66-92行代码是extract()方法,运用正则表达式从网页源代码中抽取“历史上的今天”数据。
3)111-118行代码是getTodayInHistory()方法,封装给外部调用查询“历史上的今天”。
4)125-128行代码是main方法,用于在本地的开发工具中测试。
5)75-76行代码的作用是判断获取到的“历史上的今天”数据是当天的还是前一天的(因为不能保证www.rijiben.com上的数据一定在凌晨零点准时更新,所以为了保证数据的准确性必须做此判断)。
6)第71行代码是本文的重点,笔者编写的正则表达式规则是“(.*)(<div class=\"listren\">)(.*?)(</div>)(.*)”。正则表达式规则需要根据网页源代码进行编写的,特别是包含“历史上的今天”数据的那部分HTML标签,所以我们先来查看网页源代码。通过httpRequest("http://www.rijiben.com/")方法获取到的网页源代码,与我们通过浏览器访问http://www.rijiben.com/页面再点击右键选择“查看网页源代码”所得到的结果完全一致。我们通过浏览器查看http://www.rijiben.com/的网页源代码,然后找到“历史上的今天”数据所在位置,如下图所示:
从上面的源代码截图中可以看到,我们需要的数据被包含在<div class="listren">标签内,这样就不难理解为什么正则表达式要这样写:
(.*)(<div class=\"listren\">)(.*?)(</div>)(.*)
我们使用括号()将正则表达式规则分成了5组,下面是这些分组的说明:
第1组:(.*)表示网页源代码中<div class="listren">标签之前还有任意多个字符。
第2组:(<div
class=\"listren\">)中的反斜杠表示转义,所以该规则就是用于匹配<div
class="listren">。
第3组:(.*?)表示在标签<div
class="listren">和</div>之间的所有内容,这才是我们真正需要的数据所在。
第4组:(</div>)就是用于匹配<div class="listren">的结束标签。
第5组:(.*)表示在</div>标签之后还有任意多的字符。
掌握了正则表达式规则的含义,就不难理解为什么在extract()方法中全都是在使用m.group(3),因为m.group(3)就表示匹配到数据的第3个分组。m.group(3)的内容如下:
[html] view plain copy
- <ul> <li><a href="/news6836/" title="0690年10月16日 武则天登上皇位">0690年10月16日 武则天登上皇位</a> (图)</li> <li><a href="/news6837/" title="1854年10月16日 唯美主义运动的倡导者王尔德诞辰">1854年10月16日 唯美主义运动的倡导者王尔德诞辰</a> </li> <li><a href="/news6838/" title="1854年10月16日 德国社会主义活动家考茨基诞生">1854年10月16日 德国社会主义活动家考茨基诞生</a> </li> <li><a href="/news6839/" title="1908年10月16日 阿尔巴尼亚领导人恩维尔·霍查诞辰">1908年10月16日 阿尔巴尼亚领导人恩维尔·霍查诞辰</a> (图)</li> <li><a href="/news6840/" title="1913年10月16日 中国“两弹一星”元勋钱三强诞辰">1913年10月16日 中国“两弹一星”元勋钱三强诞辰</a> (图)</li> <li><a href="/news6841/" title="1922年10月16日 开滦煤矿工人罢工失败">1922年10月16日 开滦煤矿工人罢工失败</a> (图)</li> <li><a href="/news6842/" title="1927年10月16日 德国诺贝尔文学奖得主格拉斯诞生">1927年10月16日 德国诺贝尔文学奖得主格拉斯诞生</a> (图)</li> <li><a href="/news6843/" title="1933年10月16日 抗日同盟军失败">1933年10月16日 抗日同盟军失败</a> (图)</li> <li><a href="/news6844/" title="1950年10月16日 人民解放军进军西藏">1950年10月16日 人民解放军进军西藏</a> (图)</li> <li><a href="/news6845/" title="1954年10月16日 俞平伯《关于红楼梦研究问题的信》发表">1954年10月16日 俞平伯《关于红楼梦研究问题的信》发表</a> (图)</li> <li><a href="/news6846/" title="1959年10月16日 美军将领、国务卿马歇尔去世">1959年10月16日 美军将领、国务卿马歇尔去世</a> (图)</li> <li><a href="/news6847/" title="1964年10月16日 勃列日涅夫取代赫鲁晓夫 成为苏共中央第一书记">1964年10月16日 勃列日涅夫取代赫鲁晓夫 成为苏共中央第一书记</a> </li> <li><a href="/news6848/" title="1964年10月16日 我国第一颗原子弹爆炸成功">1964年10月16日 我国第一颗原子弹爆炸成功</a> (图)</li> <li><a href="/news6849/" title="1973年10月16日 震撼世界的石油危机爆发">1973年10月16日 震撼世界的石油危机爆发</a> (图)</li> <li><a href="/news6850/" title="1978年10月16日 约翰·保罗二世当选新教皇">1978年10月16日 约翰·保罗二世当选新教皇</a> </li> <li><a href="/news6851/" title="1979年10月16日 哈克将军宣布巴基斯坦推迟大选解散政党">1979年10月16日 哈克将军宣布巴基斯坦推迟大选解散政党</a> </li> <li><a href="/news6852/" title="1984年10月16日 图图主教荣获“诺贝尔和平奖”">1984年10月16日 图图主教荣获“诺贝尔和平奖”</a> </li> <li><a href="/news6853/" title="1988年10月16日 北京正负电子对撞机对撞成功">1988年10月16日 北京正负电子对撞机对撞成功</a> (图)</li> <li><a href="/news6854/" title="1991年10月16日 美国小镇枪杀案22人丧生">1991年10月16日 美国小镇枪杀案22人丧生</a> </li> <li><a href="/news6855/" title="1991年10月16日 莫扎特死因有新说">1991年10月16日 莫扎特死因有新说</a> </li> <li><a href="/news6856/" title="1991年10月16日 钱学森获“国家杰出贡献科学家”殊荣">1991年10月16日 钱学森获“国家杰出贡献科学家”殊荣</a> (图)</li> <li><a href="/news6857/" title="1994年10月16日 德国总理科尔四连任">1994年10月16日 德国总理科尔四连任</a> </li> <li><a href="/news6858/" title="1994年10月16日 第十二届广岛亚运会闭幕">1994年10月16日 第十二届广岛亚运会闭幕</a> </li> <li><a href="/news6859/" title="1994年10月16日 修秦陵制秦俑工匠墓葬被发现">1994年10月16日 修秦陵制秦俑工匠墓葬被发现</a> </li> <li><a href="/news6860/" title="1995年10月16日 美国百万黑人男子大游行">1995年10月16日 美国百万黑人男子大游行</a> (图)</li> </ul>
可以看到,通过正则表达式抽取得到的m.group(3)中仍然有大量的html标签、空格、换行、无关字符等。我们要想办法把它们全部过滤掉,第83行代码的作用正是如此。
组装文本消息
[java] view plain copy
- // 组装文本消息(历史上的今天)
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(WeixinUtil.RESP_MESSAGE_TYPE_TEXT);
- textMessage.setFuncFlag(0);
- textMessage.setContent(TodayInHistoryService.getTodayInHistoryInfo());
对于公众帐号的消息回复在本系列教程的第5篇已经讲的很详细了,所以在这里笔者只是简单的组装了文本消息。最后,我们来看一下在微信公众帐号上的演示效果:
说明:与其说这是一篇关于公众帐号应用开发的教程,倒不如说这是一篇关于网页数据爬取的教程。本文旨在为读者开辟思路,介绍一种数据获取方式。当然,这种做法也是有弊端的,当网页改版源代码结构发生变化时,就需要重新改写数据抽取代码。没有做不到,只有想不到!
十七、第17篇-应用实例之智能翻译
内容概要
本篇文章为大家演示如何在微信公众帐号上实现“智能翻译”,本例中翻译功能是通过调用“百度翻译API”实现的。智能翻译是指用户任意输入想要翻译的内容(单词或句子),系统能自动识别用户采用的语言,并将其翻译为其他语言,目前支持的翻译方向:中->英、英->中和日->中。下面我们来看看智能翻译最终做出来的效果:
我们通过输入关键词“翻译”或者点击菜单“翻译”能够看到该功能的使用帮助,然后输入“翻译+内容”就能对内容进行翻译了。
百度翻译API介绍
点击查看百度翻译API使用说明,其实这份文档已经说的很详细了,笔者只是将我们调用该接口时最关心的内容摘取出来,主要如下:
1)通过发送HTTP
GET请求调用百度翻译API。
2)百度翻译API请求地址:
http://openapi.baidu.com/public/2.0/bmt/translate
3)调用API需要传递from、to、client_id和q四个参数,描述如下:
key |
value |
描述 |
from |
源语言语种:语言代码或auto |
仅支持特定的语言组合,下面会单独进行说明 |
to |
目标语言语种:语言代码或auto |
仅支持特定的语言组合,下面会单独进行说明 |
client_id |
开发者在百度连接平台上注册得到的授权API key |
请阅读如何获取api key |
q |
待翻译内容 |
该字段必须为UTF-8编码,并且以GET方式调用API时,需要进行urlencode编码。 |
在调用接口前,我们要先获取到api key。获取方式比较简单,根据提示一步步操作就可以,笔者就不再赘述了。
4)对于智能翻译,参数from和to的传都是auto。
4)参数q的编码方式为UTF-8,传递之前要进行urlencode编码。
5)接口返回结果示例如下:
{"from":"en","to":"zh","trans_result":[{"src":"today","dst":"\u4eca\u5929"}]}
返回结果里的中文是unicode编码,需要通过json_decode进行转换,转换后的示例如下:
{
"from": "en",
"to": "zh",
"trans_result": [
{
"src": "today",
"dst": "今天"
},
{
"src": "tomorrow",
"dst": "明天"
}
]
}
JSON处理工具包Gson介绍
Gson是Google提供的用于在Java对象和JSON数据之间进行转换的Java类库。通过使用Gson类库,我们可以将JSON字符串转成Java对象,反之亦然。下载地址:https://code.google.com/p/google-gson/downloads/list,Gson的使用比较简单,直接调用它的方法toJson()或fromJson()就能完成相应的转换,但需要注意的是:在使用Gson将json字符串转换成Java对象之前,需要先创建好与目标Java对象。读者可以在维基百科上学习它的使用示例http://zh.wikipedia.org/wiki/Gson。
代码实现
1)创建与百度翻译API返回的JSON相对应的Java类
[java] view plain copy
- import java.util.List;
- /**
- * 调用百度翻译api查询结果
- *
- * @author liufeng
- * @date 2013-10-21
- */
- public class TranslateResult {
- // 实际采用的源语言
- private String from;
- // 实际采用的目标语言
- private String to;
- // 结果体
- private List<ResultPair> trans_result;
- public String getFrom() {
- return from;
- }
- public void setFrom(String from) {
- this.from = from;
- }
- public String getTo() {
- return to;
- }
- public void setTo(String to) {
- this.to = to;
- }
- public List<ResultPair> getTrans_result() {
- return trans_result;
- }
- public void setTrans_result(List<ResultPair> trans_result) {
- this.trans_result = trans_result;
- }
- }
注意:这里的类名可以任意取,但是成员变量的名字应于翻译API返回的JSON字符串中的属性名保持一致,否则将JSON转换成TranslateResult对象时会报错。
TranslateResult类中的trans_result属性是一个ResultPair集合,该类的代码如下:
[java] view plain copy
- /**
- * 结果对
- *
- * @author liufeng
- * @date 2013-10-21
- */
- public class ResultPair {
- // 原文
- private String src;
- // 译文
- private String dst;
- public String getSrc() {
- return src;
- }
- public void setSrc(String src) {
- this.src = src;
- }
- public String getDst() {
- return dst;
- }
- public void setDst(String dst) {
- this.dst = dst;
- }
- }
说明:这两个类的封装是Gson类库所要求的,如果读者不是用Gson解析json字符串,而是用JSON-lib,就没有必要封装这两个类。
2)接口调用
[java] view plain copy
- import java.io.BufferedReader;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.io.UnsupportedEncodingException;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import com.google.gson.Gson;
- /**
- *
- * @author liufeng
- * @date 2013-10-21
- */
- public class BaiduTranslateService {
- /**
- * 发起http请求获取返回结果
- *
- * @param requestUrl 请求地址
- * @return
- */
- public static String httpRequest(String requestUrl) {
- StringBuffer buffer = new StringBuffer();
- try {
- URL url = new URL(requestUrl);
- HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection();
- httpUrlConn.setDoOutput(false);
- httpUrlConn.setDoInput(true);
- httpUrlConn.setUseCaches(false);
- httpUrlConn.setRequestMethod("GET");
- httpUrlConn.connect();
- // 将返回的输入流转换成字符串
- InputStream inputStream = httpUrlConn.getInputStream();
- InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
- BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
- String str = null;
- while ((str = bufferedReader.readLine()) != null) {
- buffer.append(str);
- }
- bufferedReader.close();
- inputStreamReader.close();
- // 释放资源
- inputStream.close();
- inputStream = null;
- httpUrlConn.disconnect();
- } catch (Exception e) {
- }
- return buffer.toString();
- }
- /**
- * utf编码
- *
- * @param source
- * @return
- */
- public static String urlEncodeUTF8(String source) {
- String result = source;
- try {
- result = java.net.URLEncoder.encode(source, "utf-8");
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- return result;
- }
- /**
- * 翻译(中->英 英->中 日->中 )
- *
- * @param source
- * @return
- */
- public static String translate(String source) {
- String dst = null;
- // 组装查询地址
- String requestUrl = "http://openapi.baidu.com/public/2.0/bmt/translate?client_id=AAAAAAAAAAAAAAAAAAAAAAAA&q={keyWord}&from=auto&to=auto";
- // 对参数q的值进行urlEncode utf-8编码
- requestUrl = requestUrl.replace("{keyWord}", urlEncodeUTF8(source));
- // 查询并解析结果
- try {
- // 查询并获取返回结果
- String json = httpRequest(requestUrl);
- // 通过Gson工具将json转换成TranslateResult对象
- TranslateResult translateResult = new Gson().fromJson(json, TranslateResult.class);
- // 取出translateResult中的译文
- dst = translateResult.getTrans_result().get(0).getDst();
- } catch (Exception e) {
- e.printStackTrace();
- }
- if (null == dst)
- dst = "翻译系统异常,请稍候尝试!";
- return dst;
- }
- public static void main(String[] args) {
- // 翻译结果:The network really powerful
- System.out.println(translate("网络真强大"));
- }
- }
代码解读:
1)第21-53行封装了一个http请求方法httpRequest(),相信读过之前教程的读者已经很熟悉了。
2)第61-69行封装了一个urlEncodeUTF8()方法,用于对url中的参数进行UTF-8编码。
3)第81行代码中的client_id需要替换成自己申请的api key。
4)第83行代码是对url中的中文进行编码。以后凡是遇到通过url传递中文参数的情况,一定要显示地对中文进行编码,否则很可能出现程序在本机能正常运行,但部署到服务器上却有问题,因为本机与服务器的默认编码方式可能不一样。
5)第88行代码就是调用百度翻译API。
6)第90行代码是使用Gson工具将json字符串转换成TranslateResult对象,是不是发现Gson的使用真的很简单?另外,前面提到过调用百度翻译API返回的json里如果有中文是用unicode表示的,形如“\u4eca\u5929”,那为什么这里没有做任何处理?因为Gson的内部实现已经帮我们搞定了。
公众账号后台调用
在公众账号后台,需要对接收到的文本消息进行判断,如果是以“翻译”两个字开头的,就认为是在使用智能翻译功能,然后将“翻译”两个字之后的内容作为翻译对象,调用API进行翻译;如果输入的只有“翻译”两个字,就提示智能翻译功能的使用指南。关键代码如下:
[java] view plain copy
- // 文本消息
- if (WeixinUtil.REQ_MESSAGE_TYPE_TEXT.equals(msgType)) {
- String content = requestMap.get("Content").trim();
- if (content.startsWith("翻译")) {
- String keyWord = content.replaceAll("^翻译", "").trim();
- if ("".equals(keyWord)) {
- textMessage.setContent(getTranslateUsage());
- } else {
- textMessage.setContent(BaiduTranslateService.translate(keyWord));
- }
- out.print(WeixinUtil.textMessageToXml(textMessage));
- }
- }
第7行getTranslateUsage()方法得到的就是智能翻译功能的使用指南,代码如下:
[java] view plain copy
- /**
- * Q译通使用指南
- *
- * @return
- */
- public static String getTranslateUsage() {
- StringBuffer buffer = new StringBuffer();
- buffer.append(XiaoqUtil.emoji(0xe148)).append("Q译通使用指南").append("\n\n");
- buffer.append("Q译通为用户提供专业的多语言翻译服务,目前支持以下翻译方向:").append("\n");
- buffer.append(" 中 -> 英").append("\n");
- buffer.append(" 英 -> 中").append("\n");
- buffer.append(" 日 -> 中").append("\n\n");
- buffer.append("使用示例:").append("\n");
- buffer.append(" 翻译我是中国人").append("\n");
- buffer.append(" 翻译dream").append("\n");
- buffer.append(" 翻译さようなら").append("\n\n");
- buffer.append("回复“?”显示主菜单");
- return buffer.toString();
- }
说明:希望通过本例的学习,除了掌握百度翻译API的调用之外,读者还能够掌握json字符串的解析方法,这样就能够自己学会调用更多互联网上开放的接口。
十八、第18篇-应用实例之音乐搜索
引言及内容概要
微信公众平台支持向用户回复音乐消息,用户收到音乐消息后,点击即可播放音乐。通过音乐消息,公众账号可以实现音乐搜索(歌曲点播)功能,即用户输入想听的音乐名称,公众账号返回对应的音乐(歌曲)。读者可以关注xiaoqrobot体验该功能,操作指南及使用如下所示。
考虑到歌曲名称有重复的情况,用户还可以同时指定歌曲名称、演唱者搜索歌曲。下面就为读者详细介绍歌曲点播功能的实现过程。
音乐消息说明
在微信公众平台开发者文档中提到,向用户回复音乐消息需要构造如下格式的XML数据。
[html] view plain copy
- <xml>
- <ToUserName><![CDATA[toUser]]></ToUserName>
- <FromUserName><![CDATA[fromUser]]></FromUserName>
- <CreateTime>12345678</CreateTime>
- <MsgType><![CDATA[music]]></MsgType>
- <Music>
- <Title><![CDATA[TITLE]]></Title>
- <Description><![CDATA[DESCRIPTION]]></Description>
- <MusicUrl><![CDATA[MUSIC_Url]]></MusicUrl>
- <HQMusicUrl><![CDATA[HQ_MUSIC_Url]]></HQMusicUrl>
- <ThumbMediaId><![CDATA[media_id]]></ThumbMediaId>
- </Music>
- </xml>
上面XML中,需要注意的是<Music>节点中的参数,说明如下:
1)参数Title:标题,本例中可以设置为歌曲名称;
2)参数Description:描述,本例中可以设置为歌曲的演唱者;
3)参数MusicUrl:普通品质的音乐链接;
4)参数HQMusicUrl:高品质的音乐链接,在WIFI环境下会优先使用该链接播放音乐;
5)参数ThumbMediaId:缩略图的媒体ID,通过上传多媒体文件获得;它指向的是一张图片,最终会作为音乐消息左侧绿色方形区域的背景图片。
上述5个参数中,最为重要的是MusicUrl和HQMusicUrl,这也是本文的重点,如何根据歌曲名称获得歌曲的链接。如果读者只能得到歌曲的一个链接,可以将MusicUrl和HQMusicUrl设置成一样的。至于ThumbMediaId参数,必须是通过微信认证的服务号才能得到,普通的服务号与订阅号可以忽略该参数,也就是说,在回复给微信服务器的XML中可以不包含ThumbMediaId参数。
百度音乐搜索API介绍
上面提到,给用户回复音乐消息最关键在于如何根据歌曲名称获得歌曲的链接,我们必须找一个现成的音乐搜索API,除非读者自己有音乐服务器,或者只向用户回复固定的几首音乐。百度有一个非公开的音乐搜索API,之所以说非公开,是因为笔者没有在百度官网的任何地方看到有关该API的介绍,但这并不影响读者对本例的学习,我们仍然可以调用它。百度音乐搜索API的请求地址如下:
[html] view plain copy
- http://box.zhangmen.baidu.com/x?op=12&count=1&title=TITLE
AUTHOR
- $$
http://box.zhangmen.baidu.com为百度音乐盒的首页地址,上面的链接中不用管参数op和count,重点关注TITLE和AUTHOR,TITLE表示歌曲名称,AUTHOR表示演唱者,AUTHOR可以为空,参数TITLE和AUTHOR需要进行URL编码(UTF-8或GB2312均可)。例如,要搜索歌曲零点乐队的“相信自己”,可以像下面这样:
[html] view plain copy
- // GB2312编码的音乐搜索链接
- http://box.zhangmen.baidu.com/x?op=12&count=1&title=%CF%E0%D0%C5%D7%D4%BC%BA$$
- // UTF-8编码的音乐搜索链接
- http://box.zhangmen.baidu.com/x?op=12&count=1&title=%E7%9B%B8%E4%BF%A1%E8%87%AA%E5%B7%B1$$
通过浏览器访问上面的地址,返回的是如下格式的XML数据:
[html] view plain copy
- <result>
- <count>1</count>
- <url>
- <encode>
- <![CDATA[http://zhangmenshiting.baidu.com/data2/music/44277542/ZWZla2xra2pfn6NndK6ap5WXcJVob5puZ2trbWprmnBjZ2xolpeZa2drZmWZmZmdl2hjZWhvnWlpYmRtZmltcGplZFqin5t1YWBobW5qcGxia2NmZ2twbzE$]]>
- </encode>
- <decode>
- <![CDATA[44277542.mp3?xcode=a39c6698955c82594aab36931dcbef60139f180191368931&mid=0.59949419022597]]>
- </decode>
- <type>8</type>
- <lrcid>64644</lrcid>
- <flag>1</flag>
- </url>
- <durl>
- <encode>
- <![CDATA[http://zhangmenshiting2.baidu.com/data2/music/44277530/ZWZla2xramhfn6NndK6ap5WXcJVob5puZ2trbWprmnBjZ2xolpeZa2drZmWZmZmdl2hjaGhvnZ5qlGRpbpedamJla1qin5t1YWBobW5qcGxia2NmZ2twbzE$]]>
- </encode>
- <decode>
- <![CDATA[44277530.mp3?xcode=a39c6698955c82594aab36931dcbef60439ff9b159af2138&mid=0.59949419022597]]>
- </decode>
- <type>8</type>
- <lrcid>64644</lrcid>
- <flag>1</flag>
- </durl>
- <p2p>
- <hash>022bc0fbf66cd19bea96db49634419dc2600393f</hash>
- <url>
- <![CDATA[ ]]>
- </url>
- <type>mp3</type>
- <size>5236902</size>
- <bitrate>192</bitrate>
- </p2p>
- </result>
返回结果中的主要参数说明如下:
1)<count>
表示搜索到的音乐数;
2)<url>中包含了普通品质的音乐链接,<durl>中包含了高品质音乐的链接;
3)<encode>中包含了加密后的音乐链接,实际上只是对音乐名称进行了加密,<decode>中包含了解密后的音乐名称。因此,要获取音乐的链接就需要重点分析<encode>和<decode>中的内容,下面会专门为读者进行介绍。
4)<type>表示音乐文件的类型,如rm、wma、mp3等;
5)<lrcid>是歌词的ID,<url>中的歌词ID为64644,那么如何得到歌词呢?本例并不关心歌词,只是附带提一下。歌词的地址如下:
[html] view plain copy
- http://box.zhangmen.baidu.com/bdlrc/646/64644.lrc
其中,http://box.zhangmen.baidu.com/bdlrc/是固定值;646为歌词所在目录名,计算方法为歌词ID(64644)除以100,取整数部分;64644.lrc是歌词文件名。
下面来看如何从<encode>和<decode>中得到音乐链接。为了便于说明,笔者将上面搜索结果中的<url>和<durl>部分抽取出来,并进行了标注,如下图所示。
上图中,1和2拼接起来是普通品质音乐的链接,3和4拼接起来是高品质音乐的链接。也就是说,普通品质和高品质的音乐链接如下:
[html] view plain copy
- // 普通品质音乐链接
- http://zhangmenshiting.baidu.com/data2/music/44277542/44277542.mp3?xcode=a39c6698955c82594aab36931dcbef60139f180191368931
- // 高品质音乐链接
- http://zhangmenshiting2.baidu.com/data2/music/44277530/44277530.mp3?xcode=a39c6698955c82594aab36931dcbef60439ff9b159af2138
参数xcode可以理解为随机验证码,每次搜索得到的值都不一样,如果不带该参数会报未授权异常“401 Authorization Required”。需要注意的是,xcode是有时间限制的,超过限制再访问链接会报异常:{"Error":{"code":"2","Message":"object
not exists","LogId":"3456414897"}}。在xcode有效的前提下,通过浏览器访问上面的音乐链接,会提示下载音乐。
编程调用百度音乐搜索API
知道如何从API返回结果中得到音乐链接后,就可以编写程序来实现了。笔者将发送HTTP请求、URL编码、解析XML等操作全部封装在BaiduMusicService类中,该类的代码如下:
[java] view plain copy
- import java.io.InputStream;
- import java.io.UnsupportedEncodingException;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import java.util.List;
- import org.dom4j.Document;
- import org.dom4j.Element;
- import org.dom4j.io.SAXReader;
- import org.liufeng.course.message.resp.Music;
- /**
- * 百度音乐搜索API操作类
- *
- * @author liufeng
- * @date 2013-12-09
- */
- public class BaiduMusicService {
- /**
- * 根据名称和作者搜索音乐
- *
- * @param musicTitle 音乐名称
- * @param musicAuthor 音乐作者
- * @return Music
- */
- public static Music searchMusic(String musicTitle, String musicAuthor) {
- // 百度音乐搜索地址
- String requestUrl = "http://box.zhangmen.baidu.com/x?op=12&count=1&title={TITLE}
AUTHOR
- $$";
- // 对音乐名称、作者进URL编码
- requestUrl = requestUrl.replace("{TITLE}", urlEncodeUTF8(musicTitle));
- requestUrl = requestUrl.replace("{AUTHOR}", urlEncodeUTF8(musicAuthor));
- // 处理名称、作者中间的空格
- requestUrl = requestUrl.replaceAll("\\+", "%20");
- // 查询并获取返回结果
- InputStream inputStream = httpRequest(requestUrl);
- // 从返回结果中解析出Music
- Music music = parseMusic(inputStream);
- // 如果music不为null,设置标题和描述
- if (null != music) {
- music.setTitle(musicTitle);
- // 如果作者不为"",将描述设置为作者
- if (!"".equals(musicAuthor))
- music.setDescription(musicAuthor);
- else
- music.setDescription("来自百度音乐");
- }
- return music;
- }
- /**
- * UTF-8编码
- *
- * @param source
- * @return
- */
- private static String urlEncodeUTF8(String source) {
- String result = source;
- try {
- result = java.net.URLEncoder.encode(source, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- return result;
- }
- /**
- * 发送http请求取得返回的输入流
- *
- * @param requestUrl 请求地址
- * @return InputStream
- */
- private static InputStream httpRequest(String requestUrl) {
- InputStream inputStream = null;
- try {
- URL url = new URL(requestUrl);
- HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection();
- httpUrlConn.setDoInput(true);
- httpUrlConn.setRequestMethod("GET");
- httpUrlConn.connect();
- // 获得返回的输入流
- inputStream = httpUrlConn.getInputStream();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return inputStream;
- }
- /**
- * 解析音乐参数
- *
- * @param inputStream 百度音乐搜索API返回的输入流
- * @return Music
- */
- @SuppressWarnings("unchecked")
- private static Music parseMusic(InputStream inputStream) {
- Music music = null;
- try {
- // 使用dom4j解析xml字符串
- SAXReader reader = new SAXReader();
- Document document = reader.read(inputStream);
- // 得到xml根元素
- Element root = document.getRootElement();
- // count表示搜索到的歌曲数
- String count = root.element("count").getText();
- // 当搜索到的歌曲数大于0时
- if (!"0".equals(count)) {
- // 普通品质
- List<Element> urlList = root.elements("url");
- // 高品质
- List<Element> durlList = root.elements("durl");
- // 普通品质的encode、decode
- String urlEncode = urlList.get(0).element("encode").getText();
- String urlDecode = urlList.get(0).element("decode").getText();
- // 普通品质音乐的URL
- String url = urlEncode.substring(0, urlEncode.lastIndexOf("/") + 1) + urlDecode;
- if (-1 != urlDecode.lastIndexOf("&"))
- url = urlEncode.substring(0, urlEncode.lastIndexOf("/") + 1) + urlDecode.substring(0, urlDecode.lastIndexOf("&"));
- // 默认情况下,高音质音乐的URL 等于 普通品质音乐的URL
- String durl = url;
- // 判断高品质节点是否存在
- Element durlElement = durlList.get(0).element("encode");
- if (null != durlElement) {
- // 高品质的encode、decode
- String durlEncode = durlList.get(0).element("encode").getText();
- String durlDecode = durlList.get(0).element("decode").getText();
- // 高品质音乐的URL
- durl = durlEncode.substring(0, durlEncode.lastIndexOf("/") + 1) + durlDecode;
- if (-1 != durlDecode.lastIndexOf("&"))
- durl = durlEncode.substring(0, durlEncode.lastIndexOf("/") + 1) + durlDecode.substring(0, durlDecode.lastIndexOf("&"));
- }
- music = new Music();
- // 设置普通品质音乐链接
- music.setMusicUrl(url);
- // 设置高品质音乐链接
- music.setHQMusicUrl(durl);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return music;
- }
- // 测试方法
- public static void main(String[] args) {
- Music music = searchMusic("相信自己", "零点乐队");
- System.out.println("音乐名称:" + music.getTitle());
- System.out.println("音乐描述:" + music.getDescription());
- System.out.println("普通品质链接:" + music.getMusicUrl());
- System.out.println("高品质链接:" + music.getHQMusicUrl());
- }
- }
下面对代码进行简单的说明:
1)代码中的Music类是对音乐消息的封装(不包括ThumbMediaId参数),读者可以在本系列教程的第4篇中找到该类的定义;
2)运行上述代码需要引入dom4j的JAR包,笔者使用的是dom4j-1.6.1.jar;
3)searchMusic()方法是提供给外部调用的,在CoreService类中会调用该方法获得音乐消息需要的Music相关的4个参数(Title、Description、MusicUrl和HQMusicUrl);
4)parseMusic()方法用于解析XML,读者可以结合代码中的注释和之前对XML的分析进行理解,这里就不再赘述了。
5)116行、127行中的get(0)表示返回多条音乐结果时默认取第一条。
公众账号后台的实现
在公众账号后台的CoreService类中,需要对用户发送的文本消息进行判断,如果是以“歌曲”两个字开头,就认为用户是在使用“歌曲点播”功能,此时需要对“歌曲”两个字之后的内容进行判断,如果包含“@”符号,就表示需要按演唱者搜索,否则不指定演唱者。CoreService类的完整代码如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.Date;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.Music;
- import org.liufeng.course.message.resp.MusicMessage;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-12-10
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- *
- * @param request
- * @return
- */
- public static String processRequest(HttpServletRequest request) {
- // 返回给微信服务器的xml消息
- String respXml = null;
- // 文本消息内容
- String respContent = null;
- try {
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 消息类型
- String msgType = requestMap.get("MsgType");
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- // 文本消息
- if (MessageUtil.REQ_MESSAGE_TYPE_TEXT.equals(msgType)) {
- // 文本消息内容
- String content = requestMap.get("Content").trim();
- // 如果以“歌曲”2个字开头
- if (content.startsWith("歌曲")) {
- // 将歌曲2个字及歌曲后面的+、空格、-等特殊符号去掉
- String keyWord = content.replaceAll("^歌曲[\\+ [email protected]#%^-_=]?", "");
- // 如果歌曲名称为空
- if ("".equals(keyWord)) {
- respContent = getUsage();
- } else {
- String[] kwArr = keyWord.split("@");
- // 歌曲名称
- String musicTitle = kwArr[0];
- // 演唱者默认为空
- String musicAuthor = "";
- if (2 == kwArr.length)
- musicAuthor = kwArr[1];
- // 搜索音乐
- Music music = BaiduMusicService.searchMusic(musicTitle, musicAuthor);
- // 未搜索到音乐
- if (null == music) {
- respContent = "对不起,没有找到你想听的歌曲<" + musicTitle + ">。";
- } else {
- // 音乐消息
- MusicMessage musicMessage = new MusicMessage();
- musicMessage.setToUserName(fromUserName);
- musicMessage.setFromUserName(toUserName);
- musicMessage.setCreateTime(new Date().getTime());
- musicMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_MUSIC);
- musicMessage.setMusic(music);
- respXml = MessageUtil.musicMessageToXml(musicMessage);
- }
- }
- }
- }
- // 未搜索到音乐时返回使用指南
- if (null == respXml) {
- if (null == respContent)
- respContent = getUsage();
- textMessage.setContent(respContent);
- respXml = MessageUtil.textMessageToXml(textMessage);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respXml;
- }
- /**
- * 歌曲点播使用指南
- *
- * @return
- */
- public static String getUsage() {
- StringBuffer buffer = new StringBuffer();
- buffer.append("歌曲点播操作指南").append("\n\n");
- buffer.append("回复:歌曲+歌名").append("\n");
- buffer.append("例如:歌曲存在").append("\n");
- buffer.append("或者:歌曲存在@汪峰").append("\n\n");
- buffer.append("回复“?”显示主菜单");
- return buffer.toString();
- }
- }
上述代码的逻辑比较简单,用户发送“歌曲+名称”或者“歌曲+名称@演唱者”就能搜索歌曲,搜索不到时会提示用户,如果发送其他内容回复歌曲点播功能的用法。
十九、第19篇-应用实例之人脸检测
在笔者的公众账号小q机器人(微信号:xiaoqrobot)中有一个非常好玩的功能"人脸检测",它能够检测出用户发送的图片中有多少张人脸,并且还能分析出每张脸所对应的人种、性别和年龄。几乎每天都有一些用户在使用“人脸检测”,该功能的趣味性和娱乐性在于能够让用户知道自己的长相与真实年龄是否相符,是否男(女)性化。本文将为读者介绍人脸检测应用的完整实现过程。
人脸检测属于人脸识别的范畴,它是一个复杂的具有挑战性的模式匹配问题,国内外许多组织、科研机构都在专门研究该问题。国内的Face++团队专注于研发人脸检测、识别、分析和重建技术,并且向广大开发者开放了人脸识别API,本文介绍的“人脸检测”应用正是基于Face++ API进行开发。
Face++简介
Face++是北京旷视科技有限公司旗下的人脸识别云服务平台,Face++平台通过提供云端API、离线SDK、以及面向用户的自主研发产品等形式,将人脸识别技术广泛应用到互联网及移动应用场景中。Face++为广大开发者提供了简单易用的API,开发者可以轻松搭建属于自己的云端身份认证、用户兴趣挖掘、移动体感交互、社交娱乐分享等多种类型的应用。
Face++提供的技术服务包括人脸检测、人脸分析和人脸识别,主要说明如下:
1)人脸检测:可以从图片中快速、准确的定位面部的关键区域位置,包括眉毛、眼睛、鼻子、嘴巴等。
2)人脸分析:可以从图片或实时视频流中分析出人脸的性别(准确度达96%)、年龄、种族等多种属性。
3)人脸识别:可以快速判定两张照片是否为同一个人,或者快速判定视频中的人像是否为某一位特定的人。
Face++的中文网址为http://cn.faceplusplus.com/,要使用Face++ API,需要注册成为Face++开发者,也就是要注册一个Face++账号。注册完成后,先创建应用,创建应用时需要填写“应用名称”、“应用描述”、“API服务器”、“应用类型”和“应用平台”,读者可以根据实际情况填写。应用创建完成后,可以看到应用的详细信息,如下图所示。
上图中,最重要的是API KEY和API SECRET,在调用Face++提供的API时,需要传入这两个参数。
人脸检测API介绍
在Face++网站的“API文档”中,能够看到Face++提供的所有API,我们要使用的人脸检测接口是detect分类下的“/detection/detect”,它能够检测出给定图片(Image)中的所有人脸(Face)的位置和相应的面部属性,目前面部属性包括性别(gender)、年龄(age)、种族(race)、微笑程度(smiling)、眼镜(glass)和姿势(pose)。
读者可以在http://cn.faceplusplus.com/uc/doc/home?id=69中了解到人脸检测接口的详细信息,该接口的请求地址如下:
[html] view plain copy
- http://apicn.faceplusplus.com/v2/detection/detect?url=URL&api_secret=API_SECRET&api_key=API_KEY
调用上述接口,必须要传入参数api_key、api_secret和待检测的图片。其中,待检测的图片可以是URL,也可以是POST方式提交的二进制数据。在微信公众账号后台,接收用户发送的图片,得到的是图片的访问路径(PicUrl),因此,在本例中,直接使用待检测图片的URL是最方便的。调用人脸检测接口返回的是JSON格式数据如下:
[html] view plain copy
- {
- "face": [
- {
- "attribute": {
- "age": {
- "range": 5,
- "value": 23
- },
- "gender": {
- "confidence": 99.9999,
- "value": "Female"
- },
- "glass": {
- "confidence": 99.945,
- "value": "None"
- },
- "pose": {
- "pitch_angle": {
- "value": 17
- },
- "roll_angle": {
- "value": 0.735735
- },
- "yaw_angle": {
- "value": -2
- }
- },
- "race": {
- "confidence": 99.6121,
- "value": "Asian"
- },
- "smiling": {
- "value": 4.86501
- }
- },
- "face_id": "17233b4b1b51ac91e391e5afe130eb78",
- "position": {
- "center": {
- "x": 49.4,
- "y": 37.6
- },
- "eye_left": {
- "x": 43.3692,
- "y": 30.8192
- },
- "eye_right": {
- "x": 56.5606,
- "y": 30.9886
- },
- "height": 26.8,
- "mouth_left": {
- "x": 46.1326,
- "y": 44.9468
- },
- "mouth_right": {
- "x": 54.2592,
- "y": 44.6282
- },
- "nose": {
- "x": 49.9404,
- "y": 38.8484
- },
- "width": 26.8
- },
- "tag": ""
- }
- ],
- "img_height": 500,
- "img_id": "22fd9efc64c87e00224c33dd8718eec7",
- "img_width": 500,
- "session_id": "38047ad0f0b34c7e8c6efb6ba39ed355",
- "url": "http://cn.faceplusplus.com/wp-content/themes/faceplusplus.zh/assets/img/demo/1.jpg?v=4"
- }
这里只对本文将要实现的“人脸检测”功能中主要用到的参数进行说明,参数说明如下:
1)face是一个数组,当一张图片中包含多张人脸时,所有识别出的人脸信息都在face数组中。
2)age中的value表示估计年龄,range表示误差范围。例如,上述结果中value=23,range=5,表示人的真实年龄在18岁至28岁左右。
3)gender中的value表示性别,男性为Male,女性为Female;gender中的confidence表示检测结果的可信度。
4)race中的value表示人种,黄色人种为Asian,白色人种为White,黑色人种为Black;race中的confidence表示检测结果的可信度。
5)center表示人脸框中心点坐标,可以将x用于计算人脸的左右顺序,即x坐标的值越小,人脸的位置越靠近图片的左侧。
人脸检测API的使用方法
为了方便开发者调用人脸识别API,Face++团队提供了基于Objective-C、Java(Android)、Matlab、Ruby、C#等多种语言的开发工具包,读者可以在Face++网站的“工具下载”版块下载相关的SDK。在本例中,笔者并不打算使用官方提供的SDK进行开发,主要原因如下:1)人脸检测API的调用比较简单,自己写代码实现也并不复杂;2)如果使用SDK进行开发,笔者还要花费大量篇幅介绍SDK的使用,这些并不是本文的重点;3)自己写代码实现比较灵活。当图片中有多张人脸时,人脸检测接口返回的数据是无序的,开发者可以按照实际使用需求进行排序,例如,将图片中的人脸按照从左至右的顺序进行排序。
编程调用人脸检测API
首先,要对人脸检测接口返回的结构进行封装,建立与之对应的Java对象。由于人脸检测接口返回的参数较多,笔者只是将本例中需要用到的参数抽取出来,封装成Face对象,对应的代码如下:
[java] view plain copy
- package org.liufeng.course.pojo;
- /**
- * Face Model
- *
- * @author liufeng
- * @date 2013-12-18
- */
- public class Face implements Comparable<Face> {
- // 被检测出的每一张人脸都在Face++系统中的标识符
- private String faceId;
- // 年龄估计值
- private int ageValue;
- // 年龄估计值的正负区间
- private int ageRange;
- // 性别:Male/Female
- private String genderValue;
- // 性别分析的可信度
- private double genderConfidence;
- // 人种:Asian/White/Black
- private String raceValue;
- // 人种分析的可信度
- private double raceConfidence;
- // 微笑程度
- private double smilingValue;
- // 人脸框的中心点坐标
- private double centerX;
- private double centerY;
- public String getFaceId() {
- return faceId;
- }
- public void setFaceId(String faceId) {
- this.faceId = faceId;
- }
- public int getAgeValue() {
- return ageValue;
- }
- public void setAgeValue(int ageValue) {
- this.ageValue = ageValue;
- }
- public int getAgeRange() {
- return ageRange;
- }
- public void setAgeRange(int ageRange) {
- this.ageRange = ageRange;
- }
- public String getGenderValue() {
- return genderValue;
- }
- public void setGenderValue(String genderValue) {
- this.genderValue = genderValue;
- }
- public double getGenderConfidence() {
- return genderConfidence;
- }
- public void setGenderConfidence(double genderConfidence) {
- this.genderConfidence = genderConfidence;
- }
- public String getRaceValue() {
- return raceValue;
- }
- public void setRaceValue(String raceValue) {
- this.raceValue = raceValue;
- }
- public double getRaceConfidence() {
- return raceConfidence;
- }
- public void setRaceConfidence(double raceConfidence) {
- this.raceConfidence = raceConfidence;
- }
- public double getSmilingValue() {
- return smilingValue;
- }
- public void setSmilingValue(double smilingValue) {
- this.smilingValue = smilingValue;
- }
- public double getCenterX() {
- return centerX;
- }
- public void setCenterX(double centerX) {
- this.centerX = centerX;
- }
- public double getCenterY() {
- return centerY;
- }
- public void setCenterY(double centerY) {
- this.centerY = centerY;
- }
- // 根据人脸中心点坐标从左至右排序
- @Override
- public int compareTo(Face face) {
- int result = 0;
- if (this.getCenterX() > face.getCenterX())
- result = 1;
- else
- result = -1;
- return result;
- }
- }
与普通Java类不同的是,Face类实现了Comparable接口,并实现了该接口的compareTo()方法,这正是Java中对象排序的关键所在。112-119行代码是通过比较每个Face的脸部中心点的横坐标来决定对象的排序方式,这样能够实现检测出的多个Face按从左至右的先后顺序进行排序。
接下来,是人脸检测API的调用及相关处理逻辑,笔者将这些实现全部封装在FaceService类中,该类的完整实现如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.io.BufferedReader;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
- import org.liufeng.course.pojo.Face;
- import net.sf.json.JSONArray;
- import net.sf.json.JSONObject;
- /**
- * 人脸检测服务
- *
- * @author liufeng
- * @date 2013-12-18
- */
- public class FaceService {
- /**
- * 发送http请求
- *
- * @param requestUrl 请求地址
- * @return String
- */
- private static String httpRequest(String requestUrl) {
- StringBuffer buffer = new StringBuffer();
- try {
- URL url = new URL(requestUrl);
- HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection();
- httpUrlConn.setDoInput(true);
- httpUrlConn.setRequestMethod("GET");
- httpUrlConn.connect();
- // 将返回的输入流转换成字符串
- InputStream inputStream = httpUrlConn.getInputStream();
- InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
- BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
- String str = null;
- while ((str = bufferedReader.readLine()) != null) {
- buffer.append(str);
- }
- bufferedReader.close();
- inputStreamReader.close();
- // 释放资源
- inputStream.close();
- inputStream = null;
- httpUrlConn.disconnect();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return buffer.toString();
- }
- /**
- * 调用Face++ API实现人脸检测
- *
- * @param picUrl 待检测图片的访问地址
- * @return List<Face> 人脸列表
- */
- private static List<Face> faceDetect(String picUrl) {
- List<Face> faceList = new ArrayList<Face>();
- try {
- // 拼接Face++人脸检测的请求地址
- String queryUrl = "http://apicn.faceplusplus.com/v2/detection/detect?url=URL&api_secret=API_SECRET&api_key=API_KEY";
- // 对URL进行编码
- queryUrl = queryUrl.replace("URL", java.net.URLEncoder.encode(picUrl, "UTF-8"));
- queryUrl = queryUrl.replace("API_KEY", "替换成自己的API Key");
- queryUrl = queryUrl.replace("API_SECRET", "替换成自己的API Secret");
- // 调用人脸检测接口
- String json = httpRequest(queryUrl);
- // 解析返回json中的Face列表
- JSONArray jsonArray = JSONObject.fromObject(json).getJSONArray("face");
- // 遍历检测到的人脸
- for (int i = 0; i < jsonArray.size(); i++) {
- // face
- JSONObject faceObject = (JSONObject) jsonArray.get(i);
- // attribute
- JSONObject attrObject = faceObject.getJSONObject("attribute");
- // position
- JSONObject posObject = faceObject.getJSONObject("position");
- Face face = new Face();
- face.setFaceId(faceObject.getString("face_id"));
- face.setAgeValue(attrObject.getJSONObject("age").getInt("value"));
- face.setAgeRange(attrObject.getJSONObject("age").getInt("range"));
- face.setGenderValue(genderConvert(attrObject.getJSONObject("gender").getString("value")));
- face.setGenderConfidence(attrObject.getJSONObject("gender").getDouble("confidence"));
- face.setRaceValue(raceConvert(attrObject.getJSONObject("race").getString("value")));
- face.setRaceConfidence(attrObject.getJSONObject("race").getDouble("confidence"));
- face.setSmilingValue(attrObject.getJSONObject("smiling").getDouble("value"));
- face.setCenterX(posObject.getJSONObject("center").getDouble("x"));
- face.setCenterY(posObject.getJSONObject("center").getDouble("y"));
- faceList.add(face);
- }
- // 将检测出的Face按从左至右的顺序排序
- Collections.sort(faceList);
- } catch (Exception e) {
- faceList = null;
- e.printStackTrace();
- }
- return faceList;
- }
- /**
- * 性别转换(英文->中文)
- *
- * @param gender
- * @return
- */
- private static String genderConvert(String gender) {
- String result = "男性";
- if ("Male".equals(gender))
- result = "男性";
- else if ("Female".equals(gender))
- result = "女性";
- return result;
- }
- /**
- * 人种转换(英文->中文)
- *
- * @param race
- * @return
- */
- private static String raceConvert(String race) {
- String result = "黄色";
- if ("Asian".equals(race))
- result = "黄色";
- else if ("White".equals(race))
- result = "白色";
- else if ("Black".equals(race))
- result = "黑色";
- return result;
- }
- /**
- * 根据人脸识别结果组装消息
- *
- * @param faceList 人脸列表
- * @return
- */
- private static String makeMessage(List<Face> faceList) {
- StringBuffer buffer = new StringBuffer();
- // 检测到1张脸
- if (1 == faceList.size()) {
- buffer.append("共检测到 ").append(faceList.size()).append(" 张人脸").append("\n");
- for (Face face : faceList) {
- buffer.append(face.getRaceValue()).append("人种,");
- buffer.append(face.getGenderValue()).append(",");
- buffer.append(face.getAgeValue()).append("岁左右").append("\n");
- }
- }
- // 检测到2-10张脸
- else if (faceList.size() > 1 && faceList.size() <= 10) {
- buffer.append("共检测到 ").append(faceList.size()).append(" 张人脸,按脸部中心位置从左至右依次为:").append("\n");
- for (Face face : faceList) {
- buffer.append(face.getRaceValue()).append("人种,");
- buffer.append(face.getGenderValue()).append(",");
- buffer.append(face.getAgeValue()).append("岁左右").append("\n");
- }
- }
- // 检测到10张脸以上
- else if (faceList.size() > 10) {
- buffer.append("共检测到 ").append(faceList.size()).append(" 张人脸").append("\n");
- // 统计各人种、性别的人数
- int asiaMale = 0;
- int asiaFemale = 0;
- int whiteMale = 0;
- int whiteFemale = 0;
- int blackMale = 0;
- int blackFemale = 0;
- for (Face face : faceList) {
- if ("黄色".equals(face.getRaceValue()))
- if ("男性".equals(face.getGenderValue()))
- asiaMale++;
- else
- asiaFemale++;
- else if ("白色".equals(face.getRaceValue()))
- if ("男性".equals(face.getGenderValue()))
- whiteMale++;
- else
- whiteFemale++;
- else if ("黑色".equals(face.getRaceValue()))
- if ("男性".equals(face.getGenderValue()))
- blackMale++;
- else
- blackFemale++;
- }
- if (0 != asiaMale || 0 != asiaFemale)
- buffer.append("黄色人种:").append(asiaMale).append("男").append(asiaFemale).append("女").append("\n");
- if (0 != whiteMale || 0 != whiteFemale)
- buffer.append("白色人种:").append(whiteMale).append("男").append(whiteFemale).append("女").append("\n");
- if (0 != blackMale || 0 != blackFemale)
- buffer.append("黑色人种:").append(blackMale).append("男").append(blackFemale).append("女").append("\n");
- }
- // 移除末尾空格
- buffer = new StringBuffer(buffer.substring(0, buffer.lastIndexOf("\n")));
- return buffer.toString();
- }
- /**
- * 提供给外部调用的人脸检测方法
- *
- * @param picUrl 待检测图片的访问地址
- * @return String
- */
- public static String detect(String picUrl) {
- // 默认回复信息
- String result = "未识别到人脸,请换一张清晰的照片再试!";
- List<Face> faceList = faceDetect(picUrl);
- if (null != faceList) {
- result = makeMessage(faceList);
- }
- return result;
- }
- public static void main(String[] args) {
- String picUrl = "http://pic11.nipic.com/20101111/6153002_002722872554_2.jpg";
- System.out.println(detect(picUrl));
- }
- }
上述代码虽然多,但条理很清晰,并不难理解,所以笔者只挑重点的进行讲解,主要说明如下:
1)70行:参数url表示图片的链接,由于链接中存在特殊字符,作为参数传递时必须进行URL编码。请读者记住:不管是什么应用,调用什么接口,凡是通过GET传递的参数中可能会包含特殊字符,都必须进行URL编码,除了中文以外,特殊字符还包括等号“=”、与“&”、空格“ ”等。
2)76-97行:使用JSON-lib解析人脸检测接口返回的JSON数据,并将解析结果存入List中。
3)99行:对集合中的对象进行排序,使用Collections.sort()方法排序的前提是集合中的Face对象实现了Comparable接口。
4)146-203行:组装返回给用户的消息内容。考虑到公众平台的文本消息内容长度有限制,当一张图片中识别出的人脸过多,则只返回一些汇总信息给用户。
5)211-219行:detect()方法是public的,提供给其他类调用。笔者可以在本地的开发工具中运行上面的main()方法,测试detect()方法的输出。
公众账号后台的实现
在公众账号后台的CoreService类中,需要对用户发送的消息类型进行判断,如果是图片消息,则调用人脸检测方法进行分析,如果是其他消息,则返回人脸检测的使用指南。CoreService类的完整代码如下:
[java] view plain copy
- package org.liufeng.course.service;
- import java.util.Date;
- import java.util.Map;
- import javax.servlet.http.HttpServletRequest;
- import org.liufeng.course.message.resp.TextMessage;
- import org.liufeng.course.util.MessageUtil;
- /**
- * 核心服务类
- *
- * @author liufeng
- * @date 2013-12-19
- */
- public class CoreService {
- /**
- * 处理微信发来的请求
- */
- public static String processRequest(HttpServletRequest request) {
- // 返回给微信服务器的xml消息
- String respXml = null;
- try {
- // xml请求解析
- Map<String, String> requestMap = MessageUtil.parseXml(request);
- // 发送方帐号(open_id)
- String fromUserName = requestMap.get("FromUserName");
- // 公众帐号
- String toUserName = requestMap.get("ToUserName");
- // 消息类型
- String msgType = requestMap.get("MsgType");
- // 回复文本消息
- TextMessage textMessage = new TextMessage();
- textMessage.setToUserName(fromUserName);
- textMessage.setFromUserName(toUserName);
- textMessage.setCreateTime(new Date().getTime());
- textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
- // 图片消息
- if (MessageUtil.REQ_MESSAGE_TYPE_IMAGE.equals(msgType)) {
- // 取得图片地址
- String picUrl = requestMap.get("PicUrl");
- // 人脸检测
- String detectResult = FaceService.detect(picUrl);
- textMessage.setContent(detectResult);
- }
- // 其它类型的消息
- else
- textMessage.setContent(getUsage());
- respXml = MessageUtil.textMessageToXml(textMessage);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return respXml;
- }
- /**
- * 人脸检测帮助菜单
- */
- public static String getUsage() {
- StringBuffer buffer = new StringBuffer();
- buffer.append("人脸检测使用指南").append("\n\n");
- buffer.append("发送一张清晰的照片,就能帮你分析出种族、年龄、性别等信息").append("\n");
- buffer.append("快来试试你是不是长得太着急");
- return buffer.toString();
- }
- }
到这里,人脸检测应用就全部开发完成了,整个项目的完整结构如下:
运行结果如下:
笔者用自己的相片测试了两次,测试结果分别是26岁、30岁,这与笔者的实际年龄相差不大,可见,Face++的人脸检测准确度还是比较高的。为了增加人脸检测应用的趣味性和娱乐性,笔者忽略了年龄估计值的正负区间。读者可以充分发挥自己的想像力和创造力,使用Face++ API实现更多实用、有趣的功能。应用开发不是简单的接口调用!
二十、第20篇-新手解惑40则
笔者在CSDN博客频道推出微信公众平台开发教程之后,接触了许多公众平台开发爱好者,也帮助他们解决了许多实际的问题,当然这其中有很多问题都是重复的,因此,笔者将这些问题及解答整理出来,以帮助更多初学者少走弯路。
1、订阅号与服务号的主要区别是什么?
订阅号每天能群发一条消息,没有自定义菜单及高级接口权限;服务号有自定义菜单及高级接口权限,但每月只能群发一条消息。
2、到底该申请订阅号还是服务号?
申请哪种类型的公众账号,主要取决于账号的用途。服务号主要面向企业和组织,旨在为用户提供服务;订阅号主要面向媒体和个人,旨在为用户提供信息和资讯。
3、订阅号是否支持编程开发?
不管是订阅号,还是服务号,在高级功能中都有编辑模式和开发模式,订阅号也支持编程开发,同样也能与企业系统对接。
4、为什么申请的公众账号没有高级功能?
公众账号注册后,要经过微信团队的审核,在审核未完成之前不显示高级功能。一般审核会在15个工作日内完成,如果一两周没审核通过均属正常现象,还请耐心等待。
5、现在订阅号能否申请自定义菜单?
不能。那为什么有些订阅号有自定义菜单?这是历史遗留问题。2013年8月5日,随着微信5.0的发布,公众账号被划分为订阅号和服务号,所有的公众账号都被默认为订阅号,并且有一次转服务号的机会,许多在此之前申请到自定义菜单的账号并没有转为服务号,所以就存在一些订阅号有自定义菜单,例如:36氪、蓉城先锋、天府之光等。
补充:2013年12月24日,公众平台针对订阅号做了重要更新。政府、传统媒体、明星等非企业性质的订阅号可以申请微信认证,通过微信认证的订阅号可获得自定义菜单接口权限。
6、现在申请的订阅号能否转服务号?
不能。只有2013年8月5日微信5.0发布以前申请的订阅号才有一次转服务号的机会,在此之后申请的订阅号不能转服务号。
那如果真的有转服务号的需求怎么解决?只能重新申请一个服务号。
7、目前一个身份证号能申请几个公众账号?
2个。
8、使用一个公司的材料能申请多少个公众账号?
没有限制。
9、在注册公众账号时,提示“你注册的公众号名称存在侵权风险,请先完成微博验证”,这是什么意思?
公众平台对一些可能存在侵权的关键词进行了保护,例如:“微信”、“移动”、“搜狐”等,如果注册的公众账号名称中包含这类关键词,提交时就会提示存在侵权风险。
遇到这种情况时,要么避开这些关键词换个名称注册,要么就根据提示先完成微博验证再继续注册。
10、个人能否申请服务号?
不能,个人只能申请订阅号。服务号的运营主体必须为组织,例如:企业、政府、其他组织等。
11、公众账号的名称可以重复吗?
公众账号的名称可以重复,不用担心被他人抢注。
12、公众账号的名称可以修改吗?
公众账号名称一经设置无法修改,公众平台没有提供账号改名的功能,因此在注册账号时取名应谨慎。
13、微信认证与微博认证有什么区别?
微信认证是针对于服务号,微博认证是针对于订阅号。也就是说,订阅号只能申请微博认证,服务号只能申请微信认证。
14、是否需要粉丝数达到500才能申请微信认证?
只要是服务号都可以申请微信认证,与粉丝数无关。只有订阅号申请微博认证才要求粉丝数必须达到500。
15、编辑模式与开发模式能够同时使用吗?
不能,这两种模式是互斥的,开启编辑模式就必须关闭开发模式,开启开发模式就必须关闭编辑模式。
16、现在用的是编辑模式,以后还可以选择使用开发模式吗?
可以,任何时候都可以根据需要切换到另外一种模式。
17、编辑模式切换到开发模式之后,在编辑模式中设置的内容还在吗?还有效吗?
在编辑模式中设置的内容,只要自己不手动删除,会永远存在的,但这些设置在开发模式下无效。
18、开发模式的菜单为什么突然消失了?
菜单不会无缘无故的消失,如果开发人员没有手动删除,那一定是有人开启过编辑模式引起的。请注意:开启编辑模式后,在开发模式下创建的菜单会被删除。
19、使用开发模式需要具备哪些条件?
1)至少掌握一门编程语言;2)具备公网服务器资源。
20、微信公众平台支持哪些编程语言?应该如何选择?
凡是支持动态Web开发的编程语言都能够用于微信公众平台开发,例如:Java、PHP、ASP.NET、Ruby、Python、Note.js等。
开者人员应该选择自己最擅长的编程语言进行开发,如果都不擅长怎么办?如果都不擅长,建议选择Java或PHP,原因在于网上关于微信公众平台开发的资料大都是基于Java和PHP的,开发起来要相对容易。
21、没有公网服务器资源怎么办?
1)免费:可以考虑使用云环境,例如,BAE(Baidu App Engine,百度应用引擎)和SAE(Sina App Engine,新浪应用引擎)。
2)付费:可以考虑租用VPS(Virtual
Private Server,虚拟专用服务器)或阿里云的云服务器。
如果仅是为了学习微信公众平台开发,个人建议使用BAE。
22、如果想使用Java进行微信公众平台开发至少需要掌握哪些内容?
至少需要掌握Java基础知识、JSP、Servlet、Javabean和JDBC(操作数据库)。
23、公司的项目大都是基于SSH框架进行开发,能使用SSH开发微信公众账号吗?
当然可以,前面说过,凡是支持动态Web开发的编程语言都能用于微信公众平台开发。其实,Struts本质上也是Servlet。
24、柳峰老师,可以给我一份微信公众平台项目的源码吗?
想要源码的朋友请您免开尊口,我认为这不是一种很好的学习方式和态度,而是一种浮躁的表现。博客中的教程已经讲的很详细了,并且贴出了所有代码(一行也不少),如果这样还不愿意花点时间去理解、消化和动动手,我也无能为力!
PS:曾经也有一些开发者、创业团队和公司提出要买小q机器人(xiaoqrobot)的源码,有的开价是5位数,但都被我拒绝了。相比之下,我更愿意把小q机器人的完整实现过程写成一篇篇技术文章免费分享出来,带动更多的开发者加入到微信公众平台开发阵营!
25、公众账号能够通过程序主动向关注用户发消息吗?
截止目前,公众平台还没有开放主动向用户发消息的接口。为什么招行可以?我前面说的是没有“开放”主动发消息的接口,并不代表没有该接口。如果贵公司也有招行的实力,我相信你也有办法申请到;如果没有这样的实力,那就不要费事了。
26、订阅号使用开发模式能够向用户回复图片、语音和视频消息吗?
可以,虽然订阅号没有多媒体文件上传接口权限,无法通过上传多媒体文件到微信服务器获取MediaId,但仍可以变相得到MediaId,同样可以实现回复多媒体消息。变相的实现方法是将用户发送给公众账号的多媒体消息的MediaId记录下来,给用户回复多媒体消息时可以使用。
27、订阅号使用开发模式能够向用户回复音乐消息吗?
可以。
28、音乐消息包含参数ThumbMediaId,没有高级接口权限的公众账号无法获得ThumbMediaId,怎么回复音乐消息?
ThumbMediaId不是音乐消息的必须参数,给用户回复音乐消息时可以不传ThumbMediaId参数,类似下面这种示例格式也能正确回复音乐消息:
[html] view plain copy
- <xml>
- <ToUserName><![CDATA[toUser]]></ToUserName>
- <FromUserName><![CDATA[fromUser]]></FromUserName>
- <CreateTime>12345678</CreateTime>
- <MsgType><![CDATA[music]]></MsgType>
- <Music>
- <Title><![CDATA[TITLE]]></Title>
- <Description><![CDATA[DESCRIPTION]]></Description>
- <MusicUrl><![CDATA[MUSIC_Url]]></MusicUrl>
- <HQMusicUrl><![CDATA[HQ_MUSIC_Url]]></HQMusicUrl>
- </Music>
- </xml>
29、订阅号与非微信认证的服务号能够向回复哪些类型的消息?
在开发模式下,订阅号与非微信认证的服务号只能向用户回复文本消息、音乐消息和图文消息。
30、为什么项目代码与柳峰老师教程中的一样,发消息给公众账号却没有任何响应?
这是我写微信公众平台开发教程以来,初学者给我反馈最多的问题。可以肯定的是,至今为止,我博客中贴出的所有代码全部都能正常运行,没有任何问题。遇到上面这种问题大都是由以下三种情况引起:
1)在公众平台开发模式下,成为开发者却忘记开启开发模式,即开发模式的开关是关闭状态。
2)通过上传WAR包的方式部署应用时,导出的WAR包中没有包含JAR。建议初学者直接将项目需要的JAR拷贝到项目中,这样通过开发工具导出的WAR包就会包含JAR。
3)项目中引入的第三方JAR包与笔者教程中使用的JAR包版本不一致。
31、为什么自定义菜单创建成功了,在微信客户端的公众账号上却不显示?
由于微信客户端缓存的原因,自定义菜单创建成功后,需要24小时以后才能显示出来。开发者在测试时,可以尝试取消关注公众账号后再次关注,这样能立即看到最新的菜单效果。
PS:菜单更新、菜单删除也会有缓存。
32、如果要更新公众账号的自定义菜单,需要先将原有菜单删除吗?
不需要,直接执行菜单创建方法即可,每次创建菜单会自动覆盖以前的菜单。
33、什么是微网站?
微网站是新瓶装老酒,被一些搞营销的人给神化了,以至于很多开发者都在问什么是微网站,如何开发微网站。微网站本质上就是以微信浏览器为入口的手机网站(Web APP),能够兼容Android、iOS、WP等操作系统。开发微网站用到的技术与开发普通网站一样,都是基于HTML(HTML5)、CSS、JavaScript等,所以有普通网站开发经验的开发者,完全有能力开发微网站。
PS:初学者以后再看到什么以“微”开头的新名词,例如:微商城、微客服、微统计,直接把“微”字去掉或者把“微”当作是“基于微信的”就不难理解了。
34、什么是模拟登录?模拟登录微信公众平台能够干什么?
模拟登录指的是通过程序模拟用户在浏览器上的操作。例如,我们通过浏览器访问微信公众平台,先要登录,登录成功后能够查看用户信息、给用户回复消息、群发消息等,其实通过程序也能够实现这些操作。
PS:对于模拟登录,官方并没有明确表态是允许还是禁止,请谨慎使用,万一哪天被封号就不划算了,也没法向关注你公众账号的用户交待。
35、微信认证是如何收费的?
服务号申请微信认证需要支付300元/次的审核服务费用,无论最终的认证审核通过与否,都需要支付这笔费用。微信认证成功后,认证的有效期是一年,在有效期快结束时还要再次申请微信认证。
36、微信支付如何申请?
截止目前,微信公众平台仍未开放微信支付权限的申请。为什么广东联通、小米手机这些账号有微信支付权限?这些公司大都与微信有着合作关系,提前享受这些权限一点也不奇怪。
37、临时带参二维码有哪些应用场景?
通过微信扫描二维码登录微信网页版,就是临时带参二维码的典型应用场景。
38、微信公众平台开发一般如何调试?
微信公众平台提供的在线接口调试工具旨在帮助开发者检测调用公众平台接口时传入的参数是否正确,这款工具对开发者的帮助其实并不大。对于调试本地运行的公众账号后台程序,这里给读者推荐两种方法:
1)使用“微信开发调试小工具”,该工具支持在本地调试,工具的用法及下载请访问:http://www.cnblogs.com/linkbiz/。
2)使用花生壳动态域名解析软件,通过路由器端口映射,可以将自己的电脑变成一台外网服务器,这样本机运行的公众账号后台程序就能直接与微信服务器进行交互了。
39、为什么项目在本地运行正常,也能获取到数据,部署到服务器上之后公众账号没有任何响应?
遇到这类情况,请读者尝试从以下几个方面排查问题:
1)检查项目在服务器上是否部署成功,可以尝试方法以前能够正常运行的功能模块,看能否正确响应,以便缩小问题范围。
2)检查项目中通过URL传递参数时,如果传递特殊字符(例如:中文、+、&等),是否对特殊字符进行了编码。
3)检查程序的处理是否超时,如果超过5秒,公众账号不响应。
4)检查返回的文本消息、图文消息是否超过限制(文本消息长度<=2048字节,图文消息条数<=10条),若超过限制,公众账号不响应。
5)公众账号不响应也有可能是微信公众平台自身故障导致。
40、为什么URL在浏览器能访问,放到微信上却不能访问?
请检查URL中是否包含特殊字符,例如:中文、+、&等,PC上的浏览器通常都会对URL中包含的特殊字符自动编码,但有些浏览器不会。为了保证所有的浏览器都能正常访问URL,请务必对URL中包含特殊字符显示编码,显示编码的意思是代码中能够明确看出编码方式是UTF8、GB2312或者其它。例如像下面这样:
[java] view plain copy
- // 采用操作系统默认的字符集进行编码,在不同的操作系统上表现不一致,不推荐
- java.net.URLEncoder.encode(chinese);
[java] view plain copy
- // 显示编码,推荐用法
- java.net.URLEncoder.encode(chinese, "UTF-8");
PS:很多初学者都认为只有URL中包含中文时才需要编码,结果导致OAuth2.0授权接口、通过ticket换取二维码接口总是调用不成功。OAuth2.0授权接口中的回调地址redirect_uri中包含大量特殊字符必须进行编码,通过ticket换取二维码接口中的ticket中可能包含+号也要进行编码。
送给初学者一条中肯的建议:不要总是怀疑微信公众平台的接口或者有经验的开发者分享的程序代码有问题,最先应该怀疑自己写的程序有问题,这样才有助于发现问题,从而解决问题。请相信:一套久经考验的平台、程序被初学者发现BUG的情况并不多见。
二十一、第21篇-“可信网址”白名单
防欺诈警告
不知道读者是否留意过这种情况:通过微信内置浏览器打开带有表单的页面,点击其中任何一个表单项都会在窗口顶部显示红色背景的防欺诈警告信息“防欺诈盗号,请勿支付或输入qq密码”,如下图所示。
防欺诈警告是腾讯微信团队基于安全考虑而设计的,但这种设计会严重影响用户体验。微信公众平台有一个“可信网址”白名单,它是由微信团队负责管理的。如果微信公众账号使用的网址在“可信网址”白名单之列,用户填写表单时就不会弹出防欺诈警告。例如,使用“招商银行信用卡中心”、“中国南方航空”、“广东联通”等公众账号的表单页面就不会出现防欺诈警告,这样的用户体验会好很多。
“可信网址”的申请
如果读者需要将所拥有或合法管理的网址加入“可信网址”白名单,需要向微信团队提供相关材料(申请书、申请人主体材料、申请网址及权利证明相关材料和申请人保证)进行申请,这些材料的说明如下。
1)申请书下载地址:https://mp.weixin.qq.com/htmledition/res/urlrequest.doc。
2)申请人主体材料包括:申请人的姓名(名称)、联系方式、地址及营业执照(单位)、身份证(个人)等证明权利人主体资格的材料。
3)申请网址及权利证明相关材料:申请成为可信网址的域名及域名登记备案资料,如ICP备案证明、相关授权证明等证明材料。
4)申请人保证:申请人承诺在申请书中的陈述和提供的相关材料皆是真实、有效和合法的,并保证对此独立承担完全责任,并就可能因此造成的损害进行赔偿,包括因腾讯根据申请人申请网址或相关网站内容而给用户或腾讯公司造成的任何损失,包括但不限于用户或腾讯因此而产生的财产损失及腾讯名誉、商誉损害等。
PS:申请“可信网址”的具体细节读者可以拨打微信客服电话或发邮件至[email protected]进行咨询。
二十二、第22篇-如何保证access_token长期有效
为了使第三方开发者能够为用户提供更多更有价值的个性化服务,微信公众平台开放了许多接口,包括自定义菜单接口、客服接口、获取用户信息接口、用户分组接口、群发接口等,开发者在调用这些接口时,都需要传入一个相同的参数access_token,它是公众账号的全局唯一票据,它是接口访问凭证。
access_token的有效期是7200秒(两小时),在有效期内,可以一直使用,只有当access_token过期时,才需要再次调用接口获取access_token。在理想情况下,一个7x24小时运行的系统,每天只需要获取12次access_token,即每2小时获取一次。如果在有效期内,再次获取access_token,那么上一次获取的access_token将失效。
目前,获取access_token接口的调用频率限制为2000次/天,如果每次发送客服消息、获取用户信息、群发消息之前都要先调用获取access_token接口得到接口访问凭证,这显然是不合理的,一方面会更耗时(多了一次接口调用操作),另一方面2000次/天的调用限制恐怕也不够用。因此,在实际应用中,我们需要将获取到的access_token存储起来,然后定期调用access_token接口更新它,以保证随时取出的access_token都是有效的。
下面将为大家介绍如何定时获取并存储access_token。请注意:这不是一篇讲解如何调用接口获取access_token的文章,关于access_token的获取,请参考文章《微信公众帐号开发教程第14篇-自定义菜单的创建及菜单事件响应》。
在动手前先来简单分析一下,我们要解决的无非是如下两个问题:
1、如何定时获取access_token?
在Java中,如果要定时执行某项任务,需要用到java.util.Timer类,对于喜欢使用框架的朋友,可以采用开源的任务调度框架quartz,spring框架也支持quartz。除此这外,还有一种方法就是启动一个线程,在线程的run()方法中写一个死循环,然后使用Thread.sleep()来保证线程定时执行某项任务。
2、将access_token保存在哪?
对于access_token的存储,可以考虑存储在文件、数据库或内存中。具体采用哪种存储方式,需要根据项目实际情况而定。如果只有一台服务器,直接将access_token存储在内存中是最简便有效的方式。
在本文中,笔者将演示的定期获取并存储access_token的流程为:Web服务器启动时就加载一个Servlet,在Servlet的init()方法中启动一个线程,在线程的run()方法中通过死循环+Thread.sleep()的方式定期获取access_token,然后将获取到的access_token保存在public static修饰的变量中。
在工程中创建一个InitServlet类,该类的代码如下:
[java] view plain copy
- package org.liufeng.weixin.servlet;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import org.liufeng.weixin.thread.TokenThread;
- import org.liufeng.weixin.util.WeixinUtil;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- /**
- * 初始化servlet
- *
- * @author liuyq
- * @date 2013-05-02
- */
- public class InitServlet extends HttpServlet {
- private static final long serialVersionUID = 1L;
- private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);
- public void init() throws ServletException {
- // 获取web.xml中配置的参数
- TokenThread.appid = getInitParameter("appid");
- TokenThread.appsecret = getInitParameter("appsecret");
- log.info("weixin api appid:{}", TokenThread.appid);
- log.info("weixin api appsecret:{}", TokenThread.appsecret);
- // 未配置appid、appsecret时给出提示
- if ("".equals(TokenThread.appid) || "".equals(TokenThread.appsecret)) {
- log.error("appid and appsecret configuration error, please check carefully.");
- } else {
- // 启动定时获取access_token的线程
- new Thread(new TokenThread()).start();
- }
- }
- }
从上面的代码可以看出,InitServlet类只重写了init()方法,并没有重写doGet()和doPost()两个方法,因为我们并不打算让InitServlet来处理访问请求。init()方法的实现也比较简单,先获取在web.xml中配置的参数appid和appsecret,再启动线程TokenThread定时获取access_token。
InitServlet在web.xml中的配置如下:
[html] view plain copy
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
- http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
- <servlet>
- <servlet-name>initServlet</servlet-name>
- <servlet-class>
- org.liufeng.weixin.servlet.InitServlet
- </servlet-class>
- <!-- 配置获取access_token所需参数appid和appsecret -->
- <init-param>
- <param-name>appid</param-name>
- <param-value>wx617a123bb8bc99cd</param-value>
- </init-param>
- <init-param>
- <param-name>appsecret</param-name>
- <param-value>4d82cbbbb08714c12345b62d7hn3dcb8</param-value>
- </init-param>
- <load-on-startup>0</load-on-startup>
- </servlet>
- <welcome-file-list>
- <welcome-file>index.jsp</welcome-file>
- </welcome-file-list>
- </web-app>
InitServlet在web.xml中的配置与普通Servlet的配置有几点区别:1)通过配置<init-param>向Servlet中传入参数;2)通过配置<load-on-startup>使得Web服务器启动时就加载该Servlet;3)没有配置<servlet-mapping>,因为InitServlet并不对外提供访问。
TokenThread的源代码如下:
[java] view plain copy
- package org.liufeng.weixin.thread;
- import org.liufeng.weixin.pojo.AccessToken;
- import org.liufeng.weixin.util.WeixinUtil;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- /**
- * 定时获取微信access_token的线程
- *
- * @author liuyq
- * @date 2013-05-02
- */
- public class TokenThread implements Runnable {
- private static Logger log = LoggerFactory.getLogger(TokenThread.class);
- // 第三方用户唯一凭证
- public static String appid = "";
- // 第三方用户唯一凭证密钥
- public static String appsecret = "";
- public static AccessToken accessToken = null;
- public void run() {
- while (true) {
- try {
- accessToken = WeixinUtil.getAccessToken(appid, appsecret);
- if (null != accessToken) {
- log.info("获取access_token成功,有效时长{}秒 token:{}", accessToken.getExpiresIn(), accessToken.getToken());
- // 休眠7000秒
- Thread.sleep((accessToken.getExpiresIn() - 200) * 1000);
- } else {
- // 如果access_token为null,60秒后再获取
- Thread.sleep(60 * 1000);
- }
- } catch (InterruptedException e) {
- try {
- Thread.sleep(60 * 1000);
- } catch (InterruptedException e1) {
- log.error("{}", e1);
- }
- log.error("{}", e);
- }
- }
- }
- }
代码中的第23行通过while(true){}构造了一个死循环(永久执行);第25行调用公众平台接口获取access_token;第29行让线程休眠7000秒再运行,即每隔7000秒获取一次access_token,保证access_token永不失效。在项目中的其他类,可以通过调用 TokenThread.accessToken.getToken() 来得到接口访问凭证access_token。在本地部署运行该程序,Tomcat启动完成后就会在控制台显示如下日志:
[html] view plain copy
- [INFO ] weixin api appid:wx617a123bb8bc99cd
- [INFO ] weixin api appsecret:4d82cbbbb08714c12345b62d7hn3dcb8
- [INFO ] 获取access_token成功,有效时长7200秒 token:sFopJ9lMmLl4u-ad61ojKpS0TolhN2s3SnHoI2Mh5GgdiYb35i-7DG2T2CDyQKMe
为了能够直观看到定期获取access_token的效果,可以试着将TokenThread里的线程休眠时间修改为30秒或60秒。最后,附上本文所涉及的项目源代码,下载地址:http://download.csdn.net/detail/lyq8479/7300501
PS:2014年4月25日微信团队发布了修改access_token长度的通知,很多开发者问这次修改会对我们的程序产生什么影响,这里顺便回答一下:如果开发者将获取到的access_token存入数据库,就必须保证对应的字段长度足够大,至少能存储512个字符;如果开发者是将access_token存储在内存中,那什么都不需要修改。