微信服务器与应用程序交互-客户输入及关注、取关等事件

 

一、交互的基本原理
微信服务器就相当于一个转发服务器,终端(手机、Pad等)发起请求至微信服务器,微信服务器,然后将请求转发给自定义服务(这就里就是我们的具体实现)。
服务处理完毕,然后挥发给微信服务器,微信服务器再将具体响应回复到终端。
通信协议为:HTTP
数据格式为:XML
具体的流程如下图所示:
微信服务器与应用程序交互-客户输入及关注、取关等事件
1.交互流程
(1)微信服务器可以把xml参数通过http转发到我们开发的应用服务
(2)自定义服务接收参数(xml)并处理
 (3) 如果有消息要推送给微信终端用户,则通过xml文件格式传递给微信服务器
 
2.微信用户交互方式
(1)发送内容:文本、音频、视频、图片(能处理的主要是文本,如用户发送1)
 (2)交互事件:关注、取消关注、扫码、地址定位
 
3.辅助技术
 (1)request获取xml参数
(2)xml和map、vo对象互相转化
(3)签名认证
 
二、微信配置
 
基本配置
 
微信服务器与应用程序交互-客户输入及关注、取关等事件
 
 
URL
微信服务器转化给我们地址,传递参数给我们,并获得参数返回,java一般试用servlet来接收,也可以springmvc的action
注意:必须是80或者443端口
token
大家加解密要用到的参数
EncodingAESKey
加***
消息加解密方式
初期选择明文,不加密
 
二、servlet代码
 
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * Servlet implementation class WxCommServlet
 */
@WebServlet("/WxCommServlet")
public abstract class WxCommServlet extends HttpServlet {
      
      private static final long serialVersionUID = 1L;
      private static final Logger log=LoggerFactory.getLogger(WxCommServlet.class);
      
    /**
     * @see HttpServlet#HttpServlet()
     */
    public WxCommServlet() {
        super();
        // TODO Auto-generated constructor stub
    }
      /**
       * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
       */
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          // 微信加密签名信息
              
             String signature = request.getParameter("signature");
             // 时间戮
             String timestamp = request.getParameter("timestamp");
             // 随机数
             String nonce = request.getParameter("nonce");
             // 随机字符串
             String echostr = request.getParameter("echostr");
            
             
             PrintWriter out = response.getWriter();
             // 通过检验 signature 对请求进行校验,若校验成功则原样返回 echostr,表示接入成功,否则接入失败
            if(SignUtil.checkSignature(signature, timestamp, nonce,getToken())){
                out.print(echostr);
            }
            out.close();
      }
      /**
       * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
       */
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            //编码设置,和微信保持一致
            request.setCharacterEncoding("utf-8");
            response.setCharacterEncoding("utf-8");
            
            //请求校验
             String signature = request.getParameter("signature");
             // 时间戮
             String timestamp = request.getParameter("timestamp");
             // 随机数
             String nonce = request.getParameter("nonce");
            
             //加解密类型
             String encrypt_type=request.getParameter("encrypt_type");
             try{
               
                if(!SignUtil.checkSignature(signature, timestamp, nonce,getToken())){//校验不成功
                      return;
                }
              PrintWriter out = response.getWriter();
               //校验成功。进行业务处理
                   Map<String,String> map=null;
                 String respXml="";//返回参数
                if("aes".equals(encrypt_type)){//加密模式
             
                      //由于获取不到多机构微信id的**,这里不做加密处理。如果有机构使用了安全模式名,这里推送异常
                      XBUtil.exceptionDeal(log, new Exception("有机构使用了加密传递"), "微信端");
                }
                else{//明文模式
                      map= MessageUtil.parseXML(request);//生成,把request里面的xml转化成java map键值对
                   
                      respXml= msgDeal(map);//调用业务逻辑层进行处理,获得反馈,没有返回""字符串
                }
             
              out.println(respXml);//返回给微信服务器    
            out.close();
             }
             catch(Exception e){
                e.printStackTrace();
                throw new ServletException(e);
             }
      }
      /**
       * 获得在微信公众号设置的token
       * @return
       */
      protected abstract String getToken();
      /**
       * 互动消息处理
       *@param userMsgMap :用户的行为参数
       * @return如果没有互动反馈,则返回空字符串,否则则是xml的交互格式
       */
      protected abstract String msgDeal(Map<String,String> userMsgMap);
      
}
 
三、消息xml格式
 
(1)微信用户推送消息
 
ToUserName:是公众号ID
fromUser:微信用户的OPEN_ID
文本消息:
用户输入的是图片
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<!-- 消息类型:text表示文本-->
<MsgType><![CDATA[text]]></MsgType>
<!-- 文本消息内容-->
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
图片消息:
用户输入的是图片
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<!-- 消息类型:image表示文本-->
<MsgType><![CDATA[image]]></MsgType>
<!--图片链接-->
<PicUrl><![CDATA[this is a url]]></PicUrl>
<MsgId>1234567890123456</MsgId>
</xml>
地理位置消息:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1351776360</CreateTime>
<!-- 消息类型:location表示文本-->
<MsgType>event</MsgType>
<Event>LOCATION</Event>
<Latitude>23.134521</Latitude>
<Longitude>113.358803</Longitude>
<Precision>30.000000</Precision>
</xml>
 
 
关注公众号消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event></xml>
取消订阅号
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[unsubscribe]]></Event></xml>
扫码事件
 
扫描带参数二维码事件
用户扫描带场景值二维码时,可能推送以下两种事件:
  1. 如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
  2. 如果用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者。
1. 用户未关注时,进行关注后的事件推送
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_123123]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>
 
 
参数    描述
ToUserName    开发者微信号
FromUserName    发送方帐号(一个OpenID)
CreateTime    消息创建时间 (整型)
MsgType    消息类型,event
Event    事件类型,subscribe(订阅)、unsubscribe(取消订阅)
 
2. 用户已关注时的事件推送
 
推送XML数据包示例:
 
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[SCAN]]></Event>
<EventKey><![CDATA[SCENE_VALUE]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket></xml>
 
参数    描述
ToUserName           开发者微信号
FromUserName     发送方帐号(一个OpenID)
CreateTime          消息创建时间 (整型)
MsgType            消息类型,event
Event                 事件类型,SCAN
EventKey           事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
Ticket                 二维码的ticket,可用来换取二维码图片
 
(2)反馈给用户信息
 
 
反馈给微信用户的是一个xml格式的文件,如果没有,则返回“”
 
             PrintWriter out = response.getWriter();         
             respXml= msgDeal(map);//调用业务逻辑层进行处理,获得反馈,没有返回""字符串
             out.println(respXml);//返回给微信服务器   
 
 
1.文本消息
 
示例
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content></xml>
说明
微信服务器与应用程序交互-客户输入及关注、取关等事件
注意
文本可
2.图片消息
 
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[media_id]]></MediaId>
</Image></xml>
微信服务器与应用程序交互-客户输入及关注、取关等事件
3.图文消息
 
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>1</ArticleCount>
<Articles>
<item>
<Title><![CDATA[title1]]></Title>
<Description><![CDATA[description1]]></Description>
<PicUrl><![CDATA[picurl]]></PicUrl>
<Url><![CDATA[url]]></Url>
</item>
</Articles></xml>
 
参数                是否必须           说明
ToUserName         是    接收方帐号(收到的OpenID)
FromUserName     是    开发者微信号
CreateTime            是    消息创建时间 (整型)
MsgType               是    消息类型,图文为news
ArticleCount         是    图文消息个数;当用户发送文本、图片、视频、图文、地理位置这五种消息时,开发者只能回复1条                                         图文消息;其余场景最多可回复8条图文消息
Articles                 是    图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数
Title                      是    图文消息标题
Description          是    图文消息描述
PicUrl                   是    图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
Url                       是    点击图文消息跳转链接
微信服务器与应用程序交互-客户输入及关注、取关等事件
多个图文,是不会显示描述信息,第一个将是大图模式,只有一个才有描述
 
4.语音信息
 
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[voice]]></MsgType>
<Voice>
<MediaId><![CDATA[media_id]]></MediaId>
</Voice></xml>
 
参数    是否必须    说明
ToUserName        是    接收方帐号(收到的OpenID)
FromUserName    是    开发者微信号
CreateTime        是    消息创建时间戳 (整型)
MsgType            是    消息类型,语音为voice
MediaId            是    通过素材管理中的接口上传多媒体文件,得到的id
 
5.视频消息
 
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[video]]></MsgType>
<Video>
<MediaId><![CDATA[media_id]]></MediaId>
<Title><![CDATA[title]]></Title>
<Description><![CDATA[description]]></Description>
</Video></xml>
参数    是否必须    说明
ToUserName        是    接收方帐号(收到的OpenID)
FromUserName    是    开发者微信号
CreateTime        是    消息创建时间 (整型)
MsgType            是    消息类型,视频为video
MediaId            是    通过素材管理中的接口上传多媒体文件,得到的id
Title            否    视频消息的标题
Description        否    视频消息的描述
 
6.音频消息
 
<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>
 
参数    是否必须    说明
ToUserName        是    接收方帐号(收到的OpenID)
FromUserName    是    开发者微信号
CreateTime        是    消息创建时间 (整型)
MsgType            是    消息类型,音乐为music
Title                   否    音乐标题
Description        否    音乐描述
MusicURL        否    音乐链接
HQMusicUrl        否    高质量音乐链接,WIFI环境优先使用该链接播放音乐
ThumbMediaId    是    缩略图的媒体id,通过素材管理中的接口上传多媒体文件,得到的id
 
 
四、帮助类
 
1.xml解析成map
jdom解析
<dependency>
    <groupId>org.jdom</groupId>
    <artifactId>jdom</artifactId>
    <version>2.0.2</version>
</dependency>
 
实现方法
      public static Map<String,String>  parseXML(HttpServletRequest request) throws Exception{
            Map<String,String>  map=new HashMap<String,String> ();
            SAXReader reader = new SAXReader();
            Document doc=reader.read(request.getInputStream());
            Element root = doc.getRootElement(); 
            digui(root,map);
            return map;
      }
 
/**
       * 递归循环函数
       * @param e
       * @throws Exception
       */
      private  static void digui(Element e,Map<String,String> map)throws Exception{
            List<Element> list=e.elements();
            if(list.size()==0){
                  System.out.println(e.getName()+"=>"+e.getTextTrim());
                  map.put(e.getName(), e.getTextTrim());
            }
            else{
                  for(Element ee:list){
                        digui(ee,map);
                  }
            }
            
      }
 
2.vo与xml的相互转化
 
使用xstream
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.11.1</version></dependency>
实现方法
private static XStream xStream=new  XStream(new MyXppDriver());//xstream解析类
 
      /**
       * 把文本消息类映射成xml
       * @param textMessage
       * @return
       */
      public static String messageToXml(TextMessage textMessage){
            xStream.alias("xml", TextMessage.class);//把把默认类转换成xml
            return xStream.toXML(textMessage) ;
      }
      
      
      
      /**
       * 把图像消息类映射成xml
       * @param textMessage
       * @return
       */
      public static String messageToXml(ImageMessage imageMessage){
      
            xStream.alias("xml", ImageMessage.class);//把把默认类转换成xml
            return xStream.toXML(imageMessage) ;
      }
      /**
       * 把语音消息类映射成xml
       * @param textMessage
       * @return
       */
      public static String messageToXml(VoiceMessage voiceMessage){
      
            xStream.alias("xml", VoiceMessage.class);//把把默认类转换成xml
            return xStream.toXML(voiceMessage) ;
      }
      
      /**
       * 把语音消息类映射成xml
       * @param textMessage
       * @return
       */
      public static String messageToXml(MusicMessage musicMessage){
      
            xStream.alias("xml", MusicMessage.class);//把把默认类转换成xml
            return xStream.toXML(musicMessage) ;
      }
      
      /**
       * 把图文消息类映射成xml
       * @param textMessage
       * @return
       */
      public static String messageToXml(NewsMessage newsMessage){
      
            xStream.alias("xml", NewsMessage.class);//把把默认类转换成xml
            xStream.alias("item", Article.class);
            return xStream.toXML(newsMessage) ;
      }
      
      /**
       * 把视频消息类映射成xml
       * @param textMessage
       * @return
       */
      public static String messageToXml(VideoMessage videoMessage){
      
            xStream.alias("xml", VideoMessage.class);//把把默认类转换成xml
            return xStream.toXML(videoMessage) ;
      }
      
 
 

3.签名算法

  /**

     * 验证签名

     * @param signature

     * @param timestamp

     * @param nonce

     * @return

     */

    public static boolean checkSignature(String signature, String timestamp, String nonce,String tonken){

        String[] arr = new String[]{tonken, timestamp, nonce};

        // 1.将 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 {

                // 3.将三个参数字符串拼接成一个字符串进行 shal 加密

            md = MessageDigest.getInstance("SHA-1");

         

            byte[] digest = md.digest(content.toString().getBytes());

            tmpStr = byteToStr(digest);

        } catch (NoSuchAlgorithmException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        }

        content = null;

        // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信

        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()): false;

    }

    

    /**

     * 将字节数组转换为十六进制字符串

     * @param digest

     * @return

     */

    private static String byteToStr(byte[] digest) {

        // TODO Auto-generated method stub

        String strDigest = "";

        for(int i = 0; i < digest.length; i++){

            strDigest += byteToHexStr(digest[i]);

        }

        return strDigest;

    }

    

    /**

     * 将字节转换为十六进制字符串

     * @param b

     * @return

     */

    private static String byteToHexStr(byte b) {

        // TODO Auto-generated method stub

        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[(b >>> 4) & 0X0F];

        tempArr[1] = Digit[b & 0X0F];

        

        String s = new String(tempArr);

        return s;

    }

五、典型应用场景:
 
1.用户发消息自动回复
不依赖公众号配置文本,比如回复1,自动反馈需要去操作的应用程序功能
2.关注、取关用户
获取关注和取关信息,存储在数据库里分析
3.带参数二维码扫码及用户关系
因为现在微信禁止在功能里做强制关注公众号,所以通过带参数的公众号二维码进行公众号关注,常见场景:
1.推广海报,生成带分享用户ID的关注二维码,能记录分享者与浏览者的关系
2.功能,如投票功能,也可生成二维码,携带功能ID及用户ID参数,通过文本或者图文消息引导用户到相关功能,并能记录用户关系
 
六.框架封装构思
 
1.一个微信号可以对应多个公众号
2.接收和推送消息和封装成通用类
3.哪种事件(关注、取关、扫码、地址等)的判断封装成一个通用封装类
4.消息可封装成一个基类对象vo和各种具体消息子类,开发人员填充vo对象,系统自动转化成xml
 
微信服务器与应用程序交互-客户输入及关注、取关等事件