微信公众号开发_微信公众号开发和微信公众平台 - CSDN
精华内容
参与话题
  • 手把手教你如何微信公众号开发

    万次阅读 多人点赞 2018-10-27 14:30:21
    最近的话,发现微信开发其实也有很多挺有意思的地方,比如最近很火的一款游戏“跳一跳”,也让我如此着迷。。但是,今天我所要讲的并不是对于小程序的开发,而是要说...而我主要讲解一下关于微信公众号开发中,一...

         最近的话,发现微信开发其实也有很多挺有意思的地方,比如最近很火的一款游戏“跳一跳”,也让我如此着迷。。但是,今天我所要讲的并不是对于小程序的开发,而是要说一下,关于微信开发的另外一个内容,那就是微信公众号。。

        关于,什么是微信公众号,微信公众号怎么申请,这个我就不多说,这些基本的概念不在这里进行讲解,自己可以直接百度就可以找到很多的资源。而我主要讲解一下关于微信公众号开发中,一些比较重要和常见的知识点,所以,这个也并不是基础篇的文章哦~!好歹也要对微信公众号有一点了解才行。~!

    一:实现外网域名访问本地服务项目

    描述:我们在刚开始接触微信公众号开发的时候,我想,一般情况下,很多人都是没有服务器的,简单点说,就是没有服务器IP,或者是公网访问的域名。然而,要想能够实现微信公众能能够与我们后台代码进行交互,那么肯定就需要有一个公网能够访问的地址,那么这怎么办好呢?

    解法一:很简单呀,直接去新浪云,阿里云,华为云,买一个服务器呗,而且是学生的话,还有优惠,多好。。但是,这个又比较繁琐,里面又有配置内容啥的,一堆(比如,tomcat,mysql,jdk等等)。那么,看看第二种方法。。。。(但是,请记住,这个是想做服务端开发必须会的,如果你连部署项目都不会,你觉得公司对你的感觉如何?多一个技能就是一个优势)

    解法二:反向代理。。如果,你是第一次听说这个名词,那么就赶紧恶补一下,这个名词的含义。我简单点说,就是通过反向代理的模式,来代理你本地的ip,以便能够在公网地址能够访问到本地的项目。。具体,请看下面,如何进行~!~!

    步骤:(1)下载ogrok客户端---------也就是反向代理的客户端,当然还有很多类似的,我这里就使用这个而已。

    这个可以通过该链接进行下载ogrok下载链接

    (2)解压刚下载好的客户端文件到自己的一个目录下

    (3)通过cmd命令,进入DOS,并且进入到刚刚解压的ogrok目录下

    (4)执行 ngrok -config=ngrok.cfg -subdomain xxx 8080 //(xxx 是你自定义的域名前缀)

    比如,我这里就是xxx就是用myjavttest

    结果:

    (5)这样的话,我们就可以通过上面的地址进行访问本地的项目了。(原来都是用的localhost:8080/login.jsp或者127.0.0.1:8080/login.jsp),当然,前提是本地有一个开启的项目,这样才可以,别本地都没有项目开启,就用公网去访问,你觉得,它能够访问么?所以,请别忘记这一点。

    (二)微信公众号客户端与后台进行验证身份

    首先,我们通过第一步,我们就能够得到一个以公网地址访问的一个IP(域名),那么既然要使得微信公众号能够与后台进行关联,那么肯定需要配置微信公众号的具体对应的服务器地址了。

    步骤:(1)首先,进入微信公众号开发官网,并且进行登陆

    (2)

    (3)

    (4)

    OK,配置到这个的话,就简单的,将基本的配置进行设置好了。。那么,下面才是关键,进入真正的开发阶段。。

    如果微信端,要与后台进行关联,那么当用户进行与后台交互的时候,后台就需要采取,身份验证,而这个是通过GET方式的请求,而只有通过的才可以进行后续的处理。所以,如何进行,那么就看下面的代码:这里讲解两种形式~~~~

    第一种:(采取Servlet)

    @WebServlet(name = "ConnectWeChatServlet")
    public class ConnectWeChatServlet extends HttpServlet {
    
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        }
    
        /**
         * 进行验证是否身份匹配
         * @param request
         * @param response
         * @throws ServletException
         * @throws IOException
         */
        @Override
        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");
            System.out.println(""+signature +"@"+timestamp +"$"+nonce +"^"+echostr);
            PrintWriter out = response.getWriter();
            if(CheckConnectUtils.checkConncetWithWeChat(signature,timestamp,nonce)){
                out.print(echostr);
            }
        }

    验证的代码:

    package com.hnu.scw.utils;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.Arrays;
    /**
     * @author scw
     * @create 2018-01-17 9:28
     * @desc 检查微信和服务器是否链接成功
     **/
    public class CheckConnectUtils {
        private static final String token = "wechat"; //这个就是微信公众号之前配置的token,必须保持一致
        /**
         * 判断是否链接匹配
         * @param signature
         * @param timestamp
         * @param nonce
         * @return
         */
        public static boolean checkConncetWithWeChat(String signature,String timestamp,String nonce){
            String[] arr = new String[]{token,timestamp,nonce};
            //排序
            Arrays.sort(arr);
            //生成字符串
            StringBuilder stringBuilder = new StringBuilder();
            for (String str:arr) {
                stringBuilder.append(str);
            }
            //进行SHA1加密
            String encodeString = passSha1Encode(stringBuilder.toString());
            if(signature.equals(encodeString)){
                return true;
            }else{
                return false;
            }
        }
    
        /**
         * 字符串进行SHA1加密
         * @param str
         * @return
         */
        public static String passSha1Encode(String str){
            if(str == null || str.length() == 0){
                return null;
            }
            char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9'
            ,'a','b','c','d','e','f'};
            try{
                MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
                mdTemp.update(str.getBytes());
                byte[] md = mdTemp.digest();
                int j = md.length;
                char[] buf = new char[j*2];
                int k = 0;
                for(int i=0 ; i <j ; i++){
                    byte byte0 = md[i];
                    buf[k++] = hexDigits[byte0 >>>4 & 0xf];
                    buf[k++] = hexDigits[byte0 & 0xf];
                }
                return new String(buf);
            } catch (NoSuchAlgorithmException e) {
               return null;
            }
        }
    }

    第二种:(采取框架,比如SpringMVC+Spring+Hibernate)

    /**
     * @author scw
     * @create 2018-01-18 11:38
     * @desc 微信前端连接的主要控制类
     **/
    @Controller
    public class WeChatDogPrimaryController {
        /**
         * 进行微信用户验证,只能是Get方法
         * @param request
         * @param response
         */
        @RequestMapping(value = "/wechat" ,method = RequestMethod.GET)
        public void connectValidate(HttpServletRequest request , HttpServletResponse response) throws IOException {
            String signature = request.getParameter("signature");
            String timestamp = request.getParameter("timestamp");
            String nonce = request.getParameter("nonce");
            String echostr = request.getParameter("echostr");
            System.out.println(""+signature +"@"+timestamp +"$"+nonce +"^"+echostr);
            PrintWriter out = response.getWriter();
            if(CheckConnectUtils.checkConncetWithWeChat(signature,timestamp,nonce)){
                out.print(echostr);
            }
        }
    
    
        /**
         * 客户端进行的消息处理
         * @param request
         * @param response
         */
        @RequestMapping(value = "/wechat" ,method = RequestMethod.POST)
        public void disposeClientMessage(HttpServletRequest request , HttpServletResponse response ) throws IOException {
          
        }

    (三)微信客户端与后台进行消息交互(比如,文本,图片,视频,音频)

    消息的实体类:

    package com.hnu.scw.model;
    
    /**
     * @author scw
     * @create 2018-01-17 13:37
     * @desc 对于所有消息的基本父类
     **/
    public class BaseMessage {
        private String ToUserName;
        private String FromUserName;
        private String CreateTime;
        private String MsgType;
    
        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 String getCreateTime() {
            return CreateTime;
        }
    
        public void setCreateTime(String createTime) {
            CreateTime = createTime;
        }
    
        public String getMsgType() {
            return MsgType;
        }
    
        public void setMsgType(String msgType) {
            MsgType = msgType;
        }
    }
    
    package com.hnu.scw.model;
    
    /**
     * @author SCW
     * @create 2018-01-17 15:08
     * @desc 图片的基本类
     **/
    public class ImageBase {
        private String MediaId;
    
        public String getMediaId() {
            return MediaId;
        }
    
        public void setMediaId(String mediaId) {
            MediaId = mediaId;
        }
    }
    
    package com.hnu.scw.model;
    
    /**
     * @author scw
     * @create 2018-01-17 15:09
     * @desc 图片消息
     **/
    public class ImageMessage extends BaseMessage {
        private ImageBase Image ;
    
        public ImageBase getImageBase() {
            return Image;
        }
    
        public void setImageBase(ImageBase Image) {
            this.Image = Image;
        }
    }
    
    package com.hnu.scw.model;
    
    /**
     * @author Administrator
     * @create 2018-01-17 16:45
     * @desc 音乐类型的基本类
     **/
    public class MusicBase {
        private String Title;
        private String Description;
        private String MusicUrl;
        private String HQMusicUrl;
        private String ThumbMediaId;
    
        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 HQMusicUrl) {
            this.HQMusicUrl = HQMusicUrl;
        }
    
        public String getThumbMediaId() {
            return ThumbMediaId;
        }
    
        public void setThumbMediaId(String thumbMediaId) {
            ThumbMediaId = thumbMediaId;
        }
    }
    
    package com.hnu.scw.model;
    
    /**
     * @author Administrator
     * @create 2018-01-17 16:46
     * @desc 用于包装音乐的实体
     **/
    public class MusicMessage extends BaseMessage {
        private MusicBase Music;
    
        public MusicBase getMusic() {
            return Music;
        }
    
        public void setMusic(MusicBase music) {
            Music = music;
        }
    }
    
    package com.hnu.scw.model;
    
    /**
     * @author scw
     * @create 2018-01-17 10:03
     * @desc 文本消息的内容
     **/
    public class MyTestMessage  extends BaseMessage{
        private String Content;
        private String  MsgId;
    
        public String getContent() {
            return Content;
        }
    
        public void setContent(String content) {
            Content = content;
        }
    
        public String getMsgId() {
            return MsgId;
        }
    
        public void setMsgId(String msgId) {
            MsgId = msgId;
        }
    }
    
    package com.hnu.scw.model;
    
    /**
     * @author scw
     * @create 2018-01-17 13:38
     * @desc 对于图文消息最内层结构的实体类
     **/
    public class NewsBase {
        private String Title;
        private String Description;
        private String PicUrl;
        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 getPicUrl() {
            return PicUrl;
        }
    
        public void setPicUrl(String picUrl) {
            PicUrl = picUrl;
        }
    
        public String getUrl() {
            return Url;
        }
    
        public void setUrl(String url) {
            Url = url;
        }
    }
    
    package com.hnu.scw.model;
    
    import java.util.List;
    
    /**
     * @author scw
     * @create 2018-01-17 13:35
     * @desc 对于图文消息的实体类
     **/
    public class NewsMessage extends BaseMessage{
        private int ArticleCount;
        private List<NewsBase> Articles;
    
        public int getArticleCount() {
            return ArticleCount;
        }
    
        public void setArticleCount(int articleCount) {
            ArticleCount = articleCount;
        }
    
        public List<NewsBase> getArticles() {
            return Articles;
        }
    
        public void setArticles(List<NewsBase> articles) {
            Articles = articles;
        }
    }

    消息封装工具类:

    package com.hnu.scw.utils;
    import com.hnu.scw.model.*;
    import com.thoughtworks.xstream.XStream;
    import org.dom4j.Document;
    import org.dom4j.Element;
    import org.dom4j.io.SAXReader;
    import javax.servlet.http.HttpServletRequest;
    import java.io.InputStream;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    /**
     * @author scw
     * @create 2018-01-17 9:52
     * @desc 用户转换消息的格式
     **/
    public class MessageUtils {
        /**
         * 定义多种消息类型
         */
        public static final String MESSAGE_TEXT = "text";
        public static final String MESSAGE_IMAGE = "image";
        public static final String MESSAGE_VOICE = "voice";
        public static final String MESSAGE_MUSIC = "music";
        public static final String MESSAGE_VIDEO = "video";
        public static final String MESSAGE_LINK = "link";
        public static final String MESSAGE_LOCATION = "location";
        public static final String MESSAGE_EVENT = "event";
        public static final String MESSAGE_SUBSCRIBE = "subscribe";
        public static final String MESSAGE_UNSUBSCRIBE = "unsubscribe";
        public static final String MESSAGE_CLICK = "CLICK";
        public static final String MESSAGE_VIEW = "VIEW";
        //扫码事件
        public static final String MESSAGE_SCANCODE = "scancode_push";
    
        /**
         * XML格式转为map格式
         * @param request
         * @return
         */
        public static Map<String , String> xmlToMap(HttpServletRequest request){
            Map<String ,String> map = new HashMap<String , String>();
            try {
                InputStream inputStream =null;
                inputStream = request.getInputStream();
                SAXReader reader = new SAXReader();
                Document doc = reader.read(inputStream);
                Element rootElement = doc.getRootElement();
                List<Element> elements = rootElement.elements();
                for (Element el:elements) {
                    map.put(el.getName() , el.getText());
                }
                inputStream.close();
                return map ;
            } catch (Exception e) {
                e.printStackTrace();
                return null ;
            }
        }
        /**
         * 文本消息对象转为xml格式
         * @param myTestMessage
         * @return
         */
        public static String textMessage2Xml(MyTestMessage myTestMessage){
            XStream xStream = new XStream();
            xStream.alias("xml" , myTestMessage.getClass());
            return xStream.toXML(myTestMessage);
        }
        /**
         * 将图文消息对象转化为图文格式的XML
         * @return
         */
        public static String newsMessage2XML(NewsMessage newsMessage){
            XStream xStream = new XStream();
            //将需要修改的一些标签进行替换
            xStream.alias("xml" , newsMessage.getClass());
            xStream.alias("item" , new NewsBase().getClass());
            return xStream.toXML(newsMessage);
        }
        /**
         * 设置需要返回的图文信息
         * @param fromUserName
         * @param toUserName
         * @return
         */
        public static String initNewSMessage(String fromUserName , String toUserName ){
            String message = null;
            List<NewsBase> newList = new ArrayList<NewsBase>();
            NewsMessage newsMessage = new NewsMessage();
            NewsBase  newsBase = new NewsBase();
            newsBase.setTitle("我是图文消息");
            newsBase.setDescription("测试测试测试测试测试测试测试测试测试");
        newsBase.setPicUrl("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=677717294,4155848424&fm=27&gp=0.jpg")
            newsBase.setUrl("www.baidu.com");
            //添加图文消息的内容
            newList.add(newsBase);
    
            //注意接受消息和发送消息的顺序要反过来,因为现在是从服务器进行发送了,而客户端是接收端了
            newsMessage.setFromUserName(toUserName);
            newsMessage.setToUserName(fromUserName);
            newsMessage.setCreateTime(String.valueOf(System.currentTimeMillis()));
            newsMessage.setMsgType("news");
            newsMessage.setArticleCount(newList.size());
            newsMessage.setArticles(newList);
            //调用转为图文的XML
            return MessageUtils.newsMessage2XML(newsMessage);
        }
        /**
         * 设置需要返回的文本信息
         * @param fromUserName
         * @param toUserName
         * @param content
         * @return
         */
        public static String initText(String fromUserName , String toUserName , String content){
            MyTestMessage text = new MyTestMessage();
            //注意接受消息和发送消息的顺序要烦过来
            text.setFromUserName(toUserName);
            text.setToUserName(fromUserName);
            text.setMsgType(MessageUtils.MESSAGE_TEXT);
            long time = System.currentTimeMillis();
            text.setCreateTime(String.valueOf(time));
            text.setContent(content);
            return MessageUtils.textMessage2Xml(text);
        }
        /**
         * 设置订阅时,返回菜单的内容
         * @return
         */
        public static String menuText(){
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("菜单1:回复数字1,有惊喜\n");
            stringBuilder.append("菜单2:回复数字2,有惊喜\n");
            stringBuilder.append("菜单3:回复数字3,有惊喜\n");
            stringBuilder.append("菜单4:回复数字3,有惊喜\n");
            stringBuilder.append("菜单5:回复数字3,有惊喜\n");
            stringBuilder.append("菜单6:回复数字未知的东东,也还有有惊喜哦\n");
            return stringBuilder.toString();
        }
        /**
         * 回复关键字"1"的时候的内容
         * @return
         */
        public static String inputNumber1(){
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("我是惊喜111,哈哈,惊喜不惊喜!");
            return stringBuilder.toString();
        }
        /**
         * 回复关键字"2"的时候的内容
         * @return
         */
        public static String inputNumber2(){
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("我是惊喜2222,哈哈,惊喜不惊喜!");
            return stringBuilder.toString();
        }
        /**
         * 返回图片消息(对于视频和音频都是一样的方式,只需要更改类型即可,即将Image修改为video,voice)
         * @param fromUserName
         * @param toUserName
         * @return
         */
        public static String initImageMessage(String fromUserName , String toUserName ){
            ImageBase imageBase = new ImageBase();
            //这个media_Id是在之前执行过上传图片返回得到的信息
            imageBase.setMediaId("HK17wQmCupESK4B9u14PqI4w3gtteXhUtGgriJW6G5c8O-Y0OsjGbYfQYhGDbYDx");
            ImageMessage imageMessage = new ImageMessage();
            imageMessage.setFromUserName(toUserName);
            imageMessage.setToUserName(fromUserName);
            imageMessage.setMsgType(MessageUtils.MESSAGE_IMAGE);
            long time = System.currentTimeMillis();
            imageMessage.setCreateTime(String.valueOf(time));
            imageMessage.setImageBase(imageBase);
            return imageMessage2XML(imageMessage);
        }
        /**
         * 将图片消息对象转化为图片格式的XML
         * @return
         */
        public static String imageMessage2XML(ImageMessage imageMessage){
            XStream xStream = new XStream();
            //将需要修改的一些标签进行替换
            xStream.alias("xml" , imageMessage.getClass());
            xStream.alias("Image" , new ImageBase().getClass());
            return xStream.toXML(imageMessage);
        }
        /**
         * 将音乐消息对象转化为图片格式的XML
         * @return
         */
        public static String musicMessage2XML(MusicMessage musicMessage){
            XStream xStream = new XStream();
            //将需要修改的一些标签进行替换
            xStream.alias("xml" , musicMessage.getClass());
            xStream.alias("Music" , new MusicBase().getClass());
            return xStream.toXML(musicMessage);
        }
        /**
         * 返回音乐消息
         * @param fromUserName
         * @param toUserName
         * @return
         */
        public static String initMusicMessage(String fromUserName , String toUserName ){
            MusicBase musicBase = new MusicBase();
            //这个ThumbMediaId是在之前执行过上传缩略图返回得到的信息(这个和上传图片的方法是一样的,都是调用pictureUtils中的上传方法)
            musicBase.setThumbMediaId("vJOi5E4_U91onQnsayPdkzxted6ZctEAEzcoLd3BJ8a00gJLuhEmTckF6S2XkS5R");
            musicBase.setTitle("来一首音乐");
            musicBase.setDescription("静静的听首歌吧!");
            //设置高质量音质的歌曲路径,可以和一般音质音乐的路径一样
            musicBase.setHQMusicUrl("http://myjava.ngrok.xiaomiqiu.cn/resource/mymusic.mp3");
            //设置音乐的路径
            musicBase.setMusicUrl("http://myjava.ngrok.xiaomiqiu.cn/resource/mymusic.mp3");
            MusicMessage musicMessage = new MusicMessage();
            musicMessage.setFromUserName(toUserName);
            musicMessage.setToUserName(fromUserName);
            //设置类型为音乐
            musicMessage.setMsgType(MessageUtils.MESSAGE_MUSIC);
            long time = System.currentTimeMillis();
            musicMessage.setCreateTime(String.valueOf(time));
            musicMessage.setMusic(musicBase);
            return musicMessage2XML(musicMessage);
        }
    }

    交互主类:

    package com.hnu.scw.controller;
    import com.hnu.scw.utils.CheckConnectUtils;
    import com.hnu.scw.utils.MessageUtils;
    import com.hnu.scw.utils.WeiXinUserInfoUtiols;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.Map;
    /**
     * @author scw
     * @create 2018-01-18 11:38
     * @desc 微信前端连接的主要控制类
     **/
    @Controller
    public class WeChatDogPrimaryController {
        /**
         * 进行微信用户验证,只能是Get方法
         * @param request
         * @param response
         */
        @RequestMapping(value = "/wechat" ,method = RequestMethod.GET)
        public void connectValidate(HttpServletRequest request , HttpServletResponse response) throws IOException {
            String signature = request.getParameter("signature");
            String timestamp = request.getParameter("timestamp");
            String nonce = request.getParameter("nonce");
            String echostr = request.getParameter("echostr");
            System.out.println(""+signature +"@"+timestamp +"$"+nonce +"^"+echostr);
            PrintWriter out = response.getWriter();
            if(CheckConnectUtils.checkConncetWithWeChat(signature,timestamp,nonce)){
                out.print(echostr);
            }
        }
        /**
         * 客户端进行的消息处理
         * @param request
         * @param response
         */
        @RequestMapping(value = "/wechat" ,method = RequestMethod.POST)
        public void disposeClientMessage(HttpServletRequest request , HttpServletResponse response ) throws IOException {
            backTestFunction(request , response );
        }
        /**
         * 文字回复功能开发
         * @param request
         * @param response
         * @throws ServletException
         * @throws IOException
         */
        public void backTestFunction(HttpServletRequest request , HttpServletResponse response ) throws IOException {
            //防止进行post提交和响应的消息乱码
            request.setCharacterEncoding("UTF-8");
            response.setHeader("Content-type", "text/html;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            PrintWriter out = response.getWriter();
            try{
                //将发送过来的消息XML形式转为map内容
                Map<String , String> map = MessageUtils.xmlToMap(request);
                String fromUserName = map.get("FromUserName");
                String toUserName = map.get("ToUserName");
                String msgType = map.get("MsgType");
                String content = map.get("Content");
                String message = null ;
                if(MessageUtils.MESSAGE_TEXT.equals(msgType)){
                    //进行关键字回复功能
                    if("1".equals(content)){
                        message = MessageUtils.initText(fromUserName,toUserName,MessageUtils.inputNumber1());
                    }else if("2".equals(content)){
                        message = MessageUtils.initText(fromUserName,toUserName,MessageUtils.inputNumber2());
                    }
                    else if("3".equals(content)){
                        //客户端输入“3”,返回一条图文消息
                        message = MessageUtils.initNewSMessage(fromUserName,toUserName);
                    }else if("4".equals(content)){
                        //客户端输入“4”,返回一条图片消息
                        message = MessageUtils.initImageMessage(fromUserName,toUserName);
                    }else if("5".equals(content)){
                        //客户端输入“5”,返回一首音乐消息
                        message = MessageUtils.initMusicMessage(fromUserName,toUserName);
                    }else if("6".equals(content)){
                        //测试是否能够获取用户的信息
                        message = MessageUtils.initText(fromUserName,toUserName, WeiXinUserInfoUtiols.getUserInfo(fromUserName));
                    }else if(content.startsWith("翻译")){
                        //客户端输入“以翻译开头”,返回对应的翻译信息
                        /*String translateResult = TranslationUtils.translate(content.substring(2,content.length()));
                        message = MessageUtils.initText(fromUserName,toUserName , translateResult);*/
                    }else {
                        message = MessageUtils.initText(fromUserName,toUserName,"你发送的消息是:" + content);
                    }
                }else if(MessageUtils.MESSAGE_EVENT.equals(msgType)){
                    String eventType = map.get("Event");
                    //完成订阅时候返回的内容
                    if(MessageUtils.MESSAGE_SUBSCRIBE .equals(eventType)){
                        message = MessageUtils.initText(fromUserName,toUserName , MessageUtils.menuText());
                    }else if(MessageUtils.MESSAGE_CLICK .equals(eventType)){
                        //进行的是click按钮的点击事件(这里就推送一下主菜单内容)
                        message = MessageUtils.initText(fromUserName,toUserName , MessageUtils.menuText());
                    }else if(MessageUtils.MESSAGE_VIEW .equals(eventType)){
                        //进行的是view按钮的点击事件(这里就推送一下主菜单内容)
                        String viewUrl = map.get("EventKey");
                        message = MessageUtils.initText(fromUserName,toUserName , viewUrl);
                    }else if(MessageUtils.MESSAGE_SCANCODE .equals(eventType)) {
                        //进行的是扫码事件
                        String key = map.get("EventKey");
                        message = MessageUtils.initText(fromUserName,toUserName , key);
                    }
                }else if(MessageUtils.MESSAGE_LOCATION .equals(msgType)) {
                    //进行的是地理位置信息
                    String label = map.get("Label");
                    message = MessageUtils.initText(fromUserName,toUserName , label);
                }
                //打印输出的xml格式内容,方便进行调试
                System.out.println(message);
                out.print(message);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                out.close();
            }
        }
    }

    (四)获取Access_Token

    Access_Token是一个全局的票据,调用任何的高级接口,都需要这个,所以这个非常非常的重要

    具体代码:

    package com.hnu.scw.utils;
    import com.hnu.scw.menu.BaseButton;
    import com.hnu.scw.menu.ClickButton;
    import com.hnu.scw.menu.CustomeMenu;
    import com.hnu.scw.menu.ViewButton;
    import com.hnu.scw.model.AccessToken;
    import com.hnu.scw.projectconst.ProjectConst;
    import net.sf.json.JSONObject;
    import org.apache.http.HttpEntity;
    import org.apache.http.HttpResponse;
    import org.apache.http.client.ClientProtocolException;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.DefaultHttpClient;
    import org.apache.http.util.EntityUtils;
    import org.springframework.web.context.ContextLoader;
    import org.springframework.web.context.WebApplicationContext;
    
    import javax.servlet.ServletContext;
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    
    /**
     * @author scw
     * @create 2018-01-17 14:13
     * @desc 用户获取access_token,众号调用各接口时都需使用access_token
     **/
    public class WeiXinUtils {
        /**
         * 微信公众号的APPID和Appsecret,这个是每个微信公众号都唯一的,以后配置不同的公众号配置这里即可
         */
        private static final String APPID = "自己公众号对应的内容";
        private static final String APPSECRET = "自己公众号对应的内容";
        //获取access_token的URL
        private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
    
        //进行创建菜单的接口URL
        private static final String CREATE_MENU_URL ="https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
    
        //菜单查询的接口URL
        private static final String QUERY_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";
    
        //菜单删除的接口URL
        private static final String DELETE_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";
    
        /**
         * Get请求,方便到一个url接口来获取结果
         * @param url
         * @return
         */
        public static JSONObject doGetStr(String url){
            DefaultHttpClient defaultHttpClient = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            JSONObject jsonObject = null;
            try{
                HttpResponse response = defaultHttpClient.execute(httpGet);
                HttpEntity entity = response.getEntity();
                if(entity != null){
                    String result = EntityUtils.toString(entity, "UTF-8");
                    jsonObject = JSONObject.fromObject(result);
                }
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return jsonObject;
        }
    
        /**
         * 带参数的post请求,方便到一个url接口来获取结果
         * @param url
         * @param outStr
         * @return
         */
        public static JSONObject doPostStr(String url , String outStr)  {
            DefaultHttpClient defaultHttpClient = new DefaultHttpClient();
            HttpPost httpPost = new HttpPost(url);
            JSONObject jsonObject = null;
            try {
                httpPost.setEntity(new StringEntity(outStr , "UTF-8"));
                HttpResponse response = defaultHttpClient.execute(httpPost);
                HttpEntity entity = response.getEntity();
                if(entity != null){
                    String result = EntityUtils.toString(entity, "UTF-8");
                    jsonObject = JSONObject.fromObject(result);
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return jsonObject;
        }
    
        /**
         * 获取access_token
         * @return
         */
        public static AccessToken getAccessToken(){
            AccessToken accessToken = new AccessToken();
            String url = ACCESS_TOKEN_URL.replace("APPID" ,APPID).replace("APPSECRET",APPSECRET);
            JSONObject jsonObject = doGetStr(url);
            if(jsonObject !=null){
                accessToken.setToken(jsonObject.getString("access_token"));
                accessToken.setExpireIn(jsonObject.getLong("expires_in"));
            }
            return accessToken;
        }

    测试是否成功获取:

    package com.hnu.scw.test;
    import com.hnu.scw.model.AccessToken;
    import com.hnu.scw.utils.WeiXinAccessTokenKeepAlive;
    import com.hnu.scw.utils.WeiXinUtils;
    import org.junit.Test;
    /**
     * @author scw
     * @create 2018-01-18 15:21
     * @desc 用于对Access_token内容相关的测试
     **/
    public class AccessTokenTest {
        /**
         * 获取到Access_Token,这个对于要想使用其他的微信接口,就必须要有这个进行验证
         */
        @Test
        public void getAccssTokenTest(){
            AccessToken accessToken = WeiXinUtils.getAccessToken();
            System.out.println("token:" +accessToken.getToken());
            System.out.println("有效时间:" +accessToken.getExpireIn());
        }
    }

    (五)自定义菜单

    下面的代码,就接着(四)中的类写就可以了,因为都属于微信开发的工具类

    步骤:

    (1)创建菜单按钮的实体对象类

    BaseButton:

    package com.hnu.scw.menu;
    /**
     * @author scw
     * @create 2018-01-17 17:20
     * @desc 最基础的Button
     **/
    public class BaseButton {
        private String type;
        private String name;
        //子按钮(也可以理解为二级菜单)
        private BaseButton[] sub_button;
    
        public String getType() {
            return type;
        }
    
        public void setType(String type) {
            this.type = type;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public BaseButton[] getSub_button() {
            return sub_button;
        }
    
        public void setSub_button(BaseButton[] sub_button) {
            this.sub_button = sub_button;
        }
    }
    

    clickButton:

    package com.hnu.scw.menu;
    /**
     * @author Administrator
     * @create 2018-01-17 17:21
     * @desc Click类型的Button实体
     **/
    public class ClickButton extends BaseButton {
        private String key;
    
        public String getKey() {
            return key;
        }
    
        public void setKey(String key) {
            this.key = key;
        }
    }

    viewButton:

    package com.hnu.scw.menu;
    /**
     * @author scw
     * @create 2018-01-17 17:22
     * @desc 类型是View的按钮实体
     **/
    public class ViewButton extends BaseButton{
        private String url;
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    }
    

    CustomerMenu菜单定义:

    package com.hnu.scw.menu;
    /**
     * @author scw
     * @create 2018-01-17 17:23
     * @desc 自定义菜单的实体
     **/
    public class CustomeMenu {
        //对菜单按钮进行封装
        private BaseButton[] button;
    
        public BaseButton[] getButton() {
            return button;
        }
    
        public void setButton(BaseButton[] button) {
            this.button = button;
        }
    }

    (2)调用接口,进行菜单的创建

     /**
         * 设置菜单的形式
         * @return
         */
        public static CustomeMenu initMenu(){
            CustomeMenu customeMenu = new CustomeMenu();
            ClickButton clickButton = new ClickButton();
            clickButton.setName("click菜单");
            clickButton.setType("click");
            clickButton.setKey("01");
    
            ViewButton viewButton = new ViewButton();
            viewButton.setName("view菜单");
            viewButton.setType("view");
            viewButton.setUrl("需要访问的地址");
    
            ClickButton clickButton2 = new ClickButton();
            clickButton2.setName("扫码事件的click菜单");
            clickButton2.setType("scancode_push");
            clickButton2.setKey("02");
    
            ClickButton clickButton3 = new ClickButton();
            clickButton3.setName("地理位置的click菜单");
            clickButton3.setType("location_select");
            clickButton3.setKey("03");
    
            BaseButton baseButton = new BaseButton();
            baseButton.setName("菜单");
            //将clickButton2,clickButton3作为一个子菜单中的按钮
            baseButton.setSub_button(new BaseButton[]{clickButton2,clickButton3});
            //把按钮分别放入到菜单中
            customeMenu.setButton(new BaseButton[]{clickButton,viewButton,baseButton});
    
            return customeMenu;
        }
    
        /**
         * 创建自定义菜单
         * @param token
         * @param menu
         * @return
         */
        public static int createMenu(String token , String menu){
            int result = 0;
            String url = CREATE_MENU_URL.replace("ACCESS_TOKEN" ,token);
            JSONObject jsonObject = doPostStr(url, menu);
            if(jsonObject != null){
                //接受返回回来的参数,如果是0,就是创建成功
                result = jsonObject.getInt("errcode");
            }
            return result;
        }
    
        /**
         * 对菜单进行查询
         * @param token
         * @return
         */
        public static JSONObject queryMenu(String token){
            String url = QUERY_MENU_URL.replace("ACCESS_TOKEN" ,token);
            JSONObject jsonObject = doGetStr(url);
            return jsonObject;
        }
    
        /**
         * 对菜单进行删除
         * @param token
         * @return
         */
        public static JSONObject deleteMenu(String token){
            String url = DELETE_MENU_URL.replace("ACCESS_TOKEN" ,token);
            JSONObject jsonObject = doGetStr(url);
            return jsonObject;
        }

    (3)生成菜单

    package com.hnu.scw.test;
    import com.hnu.scw.model.AccessToken;
    import com.hnu.scw.utils.WeiXinUtils;
    import net.sf.json.JSONObject;
    import org.junit.Test;
    /**
     * @author scw
     * @create 2018-01-18 15:21
     * @desc 菜单相关的测试
     **/
    public class MenuTest {
        /**
         * 创建菜单
         */
        @Test
        public void creatMenuTest(){
            //获取到access_token
            AccessToken accessToken = WeiXinUtils.getAccessToken();
            //获取到自定义菜单的格式(JSONObject将对象转为json,然后再需要转为字符串型)
            String menu = JSONObject.fromObject(WeiXinUtils.initMenu()).toString();
            //调用创建菜单
            int result = WeiXinUtils.createMenu(accessToken.getToken(), menu);
            if(result == 0){
                //如果调用方法之后,返回的是0,那么就表示创建成功。
                System.out.println("创建成功");
            }else{
                System.out.println("创建失败");
            }
        }
        /**
         * 查询菜单
         */
        @Test
        public void queryMenuTest(){
            //获取到access_token
            AccessToken accessToken = WeiXinUtils.getAccessToken();
            //调用菜单查询的方法,返回是的一个Json格式
            JSONObject jsonObject = WeiXinUtils.queryMenu(accessToken.getToken());
            System.out.println(jsonObject);
        }
        /**
         * 删除菜单
         */
        @Test
        public void deleteMenuTest(){
            //获取到access_token
            AccessToken accessToken = WeiXinUtils.getAccessToken();
            //调用菜单查询的方法,返回是的一个Json格式
            JSONObject jsonObject = WeiXinUtils.deleteMenu(accessToken.getToken());
            if(jsonObject.getInt("errcode") == 0){
                //返回0,表示的是删除成功
                System.out.println("删除成功");
            }else{
                System.out.println("删除失败");
            }
    
        }
    }

           上面就是一些基本的微信公众号的交互了,刚刚接触可能不是很熟,慢慢的就了解了之后,其实也很简单的,当然,对于其中的一些高级接口的使用,可以看看我其他的文章,都有提到。。如果有问题,不明白的地方,欢迎进行交流和留言~!另外,推荐了比较好的学习资源,就是慕课网,这里面对于比较基础的微信公众号开发还是讲解的比较好,再结合我的文章,肯定是没有任何问题的。。。。

    Github的地址:

    https:https://github.com/qq496616246/WeChatCode.git

    git地址:git@github.com:qq496616246/WeChatCode.git

    展开全文
  • 微信公众号开发详细教程

    千次阅读 2019-05-24 17:53:17
    微信公众号开发之配置开发服务器 微信公众号开发之获取access_token 微信公众号开发之关键词回复 微信公众号开发之模板消息 微信公众号开发之授权回调 ...
    展开全文
  • 微信公众号主要有以下几个步骤 微信公众号的通讯机制 微信公众号简介 1.注册微信公众号 2.注册测试公众号 3.搭建微信本地调试环境 1)下载客户端natapp: 2)安装natapp: 4.微信公众号接入(校验签名) 第1步中...

    由于图片图床问题,文章部分图片无法预览,暂时把该文章迁移至简书,给大家带来麻烦,抱歉了。
    感兴趣的小伙伴可参考:https://www.jianshu.com/p/cc1b1050b5b4

    Author xiuhong.chen

    Date 2017/11/23

    Desc 微信公众号公注册、开发环境搭建、access_token管理、Demo实现不同类型消息发送、实现天气预报和翻译功能、natapp外网穿透

    微信公众号的通讯机制

    image.png

    微信公众号简介

    微信公众号分为服务号、订阅号、企业号,订阅号可以个人申请,服务号和企业号要有企业资质才可以。

    我们所说的微信公众号开发指的是订阅号和服务号。关于订阅号和服务器的区别,官方是这样解释的

    • 服务号**:主要偏向于服务交互(功能类似12315,114,银行,提供绑定信息,服务交互),每月可群发4条消息;服务号**适用人群:**媒体、企业、政府或其他组织。

    • 订阅号**:主要偏向于为用户传达资讯,(功能类似报纸杂志,为用户提供新闻信息或娱乐趣事),每天可群发1条消息;订阅号适用人群:个人、媒体、企业、政府或其他组织。

    1.注册微信公众号

    进入微信公众号注册页面https://mp.weixin.qq.com/点击公众号右上方的注册按钮,进入注册界面,填写基本信息,选择订阅号, 完成身份认证, 此处我选择的是个人订阅号,如下完善即可:

    image.png

    image.png

    然后注册成功之后进入微信公众平台后台,然后完善微信号名称和微信号ID:

    image.png

    image.png

    微信号名称默认是新注册公众号,还需要修改微信号名称, 修改的时候需要经过微信认证,并且审核通过之后才可以使用该公众号.

    image.png

    2.注册测试公众号

    个人订阅号有一些接口是没有权限的,也就是说个人订阅号无法调用一些高级的权限接口,如生成二维码、网页授权、自定义菜单、微信支付这样的接口权限个人订阅号是没有调用权限的, 幸运的是,微信公众平台提供了测试公众账号,测试公众号有很多个人订阅号不具备的权限, 测试公众号的注册地址为:

    http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

    用微信扫描页面中的二维码进行登录,登录成功后,就可以看到腾讯分配给我们的测试公众号的信息了,如下图所示, 接下来我们就可以搭建环境,进行开发测试了

    image.png

    测试公众号的所拥有的接口权限如下:

    image.png
    image.png
    image.png
    image.png

    3.搭建微信本地调试环境

    开发基于微信公众号的应用最大的痛苦之处就是调试问题,每次实现一个功能后都需要部署到一个公网服务器进行测试,因为微信用户每次向公众号发起请求时,微信服务器会先接收到用户的请求,然后再转发到我们的服务器上,也就是说,微信服务器是要和我们的服务器进行网络交互,所以我们必须保证我们的服务器外网可以访问到,这种部署到公网服务器进行测试的做法对于我们开发者来说简直是噩梦。所以我们要想一个办法可以做到本地部署,本地调试代码,而要做到这一点,那么我们要解决的问题就是将内网的部署服务器映射到外网,让微信服务器可以正常访问到,幸运的是,借助于第三方软件Ngrok,我们就可以做得到。Ngrok是一个免费的软件Ngrok,使用Ngrok后,我们就可以实现内网穿透,也就是说我们可以将内网的服务器映射到外网给别人访问,这对于我们在本地开发环境中调试微信代码是以及给用户演示一些东西非常快速和有帮助的,因为可以直接使用我们自己的内网的电脑作为服务器。不过需要翻墙访问.

    国内提供Ngrok服务比较好的网站是:http://natapp.cn/,如下图所示:

    image.png

    1)下载客户端natapp:

    image.png

    2)安装natapp:

    具体参考http://blog.csdn.net/xunxianren007/article/details/54954520, 这个网址里详细介绍了win/Mac/Linux下安装步骤

    • 解压缩到目录D:\Program Files\natapp

    • 直接双击打开失败,需要配置环境变量,编辑环境变量Path,新建natapp目录

      image.png

    • 打开cmd, 执行命令 natapp, 显示认证错误

      image.png

    • 这个时候是需要token认证的, 所以我们的主要工作就是如何获得authtoken

      进入https://natapp.cn/,根据提示注册并创建免费隧道, 注册的时候需要用支付宝实名认证的手机号注册

      根据《中华人民共和国网络安全法》,以及防止隧道被非法使用,Natapp实行实名认证制度.
      本站承诺身份信息仅用于身份验证识别,不做任何其他用途,且严格加密存储杜绝泄漏风险
      本实名认证系统基于阿里大数据的强个人识别验证.手机,身份证信息须匹配,比如手机号是你的支付宝实名认证的手机号,日常正常使用的.如其他小号可能无法通过验证
      目前创建免费隧道强制要求实名认证.付费隧道可通过支付宝支付来实名认证
      

      image.png

      image.png

    • 复制authtoken, cmd进入natapp目录执行 natapp -authtoken yourauthtoken 出现下图即为成功

      image.png

    • 此时外网的用户可以直接使用http://rzxjzk.natappfree.cc这个域名访问到我内网的127.0.0.1:8080服务器了,如下图所示:

      image.png

      image.png

      使用了ngrok之后,我们就可以把内网的服务器当成公网服务器来使用了.访问的速度也还在可以接受的范围内吧,截止到目前为止ngrok是可用的,微信公众号服务器是可以访问的,这样一来也就不妨碍我们做本地调式了。到此,我们的微信本地调试开发环境就算是搭建好了。

    4.微信公众号接入(校验签名)

    微信公众平台开发者文档上,关于公众号接入这一节内容在接入指南上写的比较详细的,文档中说接入公众号需要3个步骤,分别是:

    1、填写服务器配置
      2、验证服务器地址的有效性
      3、依据接口文档实现业务逻辑

    其实,第3步已经不能算做公众号接入的步骤,而是接入之后,开发人员可以根据微信公众号提供的接口所能做的一些开发。

    第1步中服务器配置包含服务器地址(URL)、令牌(Token) 和 消息加解密密钥(EncodingAESKey)。

    ​ 可在开发–>基本配置–>服务器配置中配置

    ​ 服务器地址即公众号后台提供业务逻辑的入口地址,目前只支持80端口,之后包括接入验证以及任何其它的操作的请求(例如消息的发送、菜单管理、素材管理等)都要从这个地址进入。接入验证和其它请求的区别就是,接入验证时是get请求,其它时候是post请求;

    Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性);

    EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。本例中全部以未加密的明文消息方式,不涉及此配置项。

    第2步,验证服务器地址的有效性,当点击“提交”按钮后,微信服务器将发送一个http的get请求到刚刚填写的服务器地址,并且携带四个参数:

    image.png

    接到请求后,我们需要做如下三步,若确认此次GET请求来自微信服务器,原样返回echostr参数内容,则接入生效,否则接入失败。

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

    下面我们用Java代码来演示一下这个验证过程

    使用IDE(Eclipse或者IntelliJ IDEA)创建一个JavaWeb项目,新建servlet weChatAccounts,代码如下:

    package weChatServlet;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.activation.DataHandler;
    import javax.activation.FileDataSource;
    import javax.mail.*;
    import javax.mail.internet.*;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.Arrays;
    import java.util.Calendar;
    import java.util.Date;
    import java.util.Properties;
    
    public class weChatAccounts extends HttpServlet {
        static Logger logger = LoggerFactory.getLogger(weChatAccounts.class);
    
        /*
        * 自定义token, 用作生成签名,从而验证安全性
        * */
        private final String TOKEN = "cherry";
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doGet(req,resp);
        }
        @Override
        public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("-----开始校验签名-----");
    
            /**
             * 接收微信服务器发送请求时传递过来的参数
             */
            String signature = req.getParameter("signature");
            String timestamp = req.getParameter("timestamp");
            String nonce = req.getParameter("nonce"); //随机数
            String echostr = req.getParameter("echostr");//随机字符串
    
            /**
             * 将token、timestamp、nonce三个参数进行字典序排序
             * 并拼接为一个字符串
             */
            String sortStr = sort(TOKEN,timestamp,nonce);
            /**
             * 字符串进行shal加密
             */
            String mySignature = shal(sortStr);
            /**
             * 校验微信服务器传递过来的签名 和  加密后的字符串是否一致, 若一致则签名通过
             */
            if(!"".equals(signature) && !"".equals(mySignature) && signature.equals(mySignature)){
                System.out.println("-----签名校验通过-----");
                resp.getWriter().write(echostr);
            }else {
                System.out.println("-----校验签名失败-----");
            }
        }
    
        /**
         * 参数排序
         * @param token
         * @param timestamp
         * @param nonce
         * @return
         */
        public String sort(String token, String timestamp, String nonce) {
            String[] strArray = {token, timestamp, nonce};
            Arrays.sort(strArray);
            StringBuilder sb = new StringBuilder();
            for (String str : strArray) {
                sb.append(str);
            }
            return sb.toString();
        }
    
        /**
         * 字符串进行shal加密
         * @param str
         * @return
         */
        public String shal(String str){
            try {
                MessageDigest digest = MessageDigest.getInstance("SHA-1");
                digest.update(str.getBytes());
                byte messageDigest[] = digest.digest();
    
                StringBuffer hexString = new StringBuffer();
                // 字节数组转换为 十六进制 数
                for (int i = 0; i < messageDigest.length; i++) {
                    String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
                    if (shaHex.length() < 2) {
                        hexString.append(0);
                    }
                    hexString.append(shaHex);
                }
                return hexString.toString();
    
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
    
    

    在web.xml中配置 servlet:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app 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_3_1.xsd"
               version="3.1">
        <welcome-file-list>
            <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    
        <servlet>
            <servlet-name>weChatServlet</servlet-name>
            <servlet-class>weChatServlet.weChatAccounts</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>weChatServlet</servlet-name>
            <url-pattern>/weChatServlet</url-pattern> <!--url-pattern必须与servlet-name一致-->
        </servlet-mapping>
    </web-app>
    
    

    然后在index.jsp中写hello world 测试:

    <%-- Created by IntelliJ IDEA. --%>
    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <html>
      <head>
        <title></title>
      </head>
      <body>
          <h3>微信公众号测试!</h3>
      </body>
    </html>
    

    启动项目结果如下:

    image.png

    然后启动natapp,进行内网透传 natapp -authtoken mytoken

    image.png

    根据动态生成的ip地址访问.,得到如下效果,则表示外网可以成功访问:

    image.png

    进入微信测试公众号管理界面,在接口配置信息中填入映射的外网地址和代码中声明的token,如下图所示:

    点击提交,会显示配置成功, 控制台就会打印信息, 显示签名校验通过.

    注意: URL是 外网的ip地址加上 web.xml中配置的servlet名称

    image.png

    image.png

    到此,我们的公众号应用已经能够和微信服务器正常通信了,也就是说我们的公众号已经接入到微信公众平台了。

    5.access_token管理

    我们的公众号和微信服务器对接成功之后,接下来要做的就是根据我们的业务需求调用微信公众号提供的接口来实现相应的逻辑了。在使用微信公众号接口中都需要一个access_token。

    1)access_token介绍

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

    总结以上说明,access_token需要做到以下两点:

    1.因为access_token有2个小时的时效性,要有一个机制保证最长2个小时重新获取一次。

    2.因为接口调用上限每天2000次,所以不能调用太频繁。

    2)获取access_token步骤

    公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。

    接口调用请求说明

    https请求方式: GET
    https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
    

    参数说明

    参数 是否必须 说明
    grant_type 获取access_token填写client_credential
    appid 第三方用户唯一凭证
    secret 第三方用户唯一凭证密钥,即appsecret

    返回说明

    正常情况下,微信会返回下述JSON数据包给公众号:

    {"access_token":"ACCESS_TOKEN","expires_in":7200}
    

    参数说明

    参数 说明
    access_token 获取到的凭证
    expires_in 凭证有效时间,单位:秒

    错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):

    {"errcode":40013,"errmsg":"invalid appid"}
    

    返回码说明

    返回码 说明
    -1 系统繁忙,此时请开发者稍候再试
    0 请求成功
    40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
    40002 请确保grant_type字段值为client_credential
    40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置

    3)代码实现获取access_token

    定义一个默认启动的servlet,在init方法中启动一个Thread,这个进程中定义一个无限循环的方法,用来获取access_token,当获取成功后,此进程休眠7000秒(7000秒=1.944444444444444小时),否则休眠3秒钟继续获取。流程图如下:

    image.png

    定义一个dto AccessToken:

    package AccessToken;
    
    public class AccessToken {
        private String tokenName; //获取到的凭证
        private int expireSecond;    //凭证有效时间  单位:秒
    
        public String getTokenName() {
            return tokenName;
        }
    
        public void setTokenName(String tokenName) {
            this.tokenName = tokenName;
        }
    
        public int getExpireSecond() {
            return expireSecond;
        }
    
        public void setExpireSecond(int expireSecond) {
            this.expireSecond = expireSecond;
        }
    }
    
    
    package AccessToken;
    
    import AccessToken.AccessToken;
    
    public class AccessTokenInfo {
    
        public static AccessToken accessToken = null;
    }
    
    

    编写一个用于发起https请求的工具类NetWorkUtil,代码如下:

    getHttpsResponse方法是请求一个https地址,参数requestMethod为字符串“GET”或者“POST”,传null或者“”默认为get方式。

    package AccessToken;
    
    import javax.net.ssl.*;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.security.KeyManagementException;
    import java.security.NoSuchAlgorithmException;
    import java.security.cert.CertificateException;
    import java.security.cert.X509Certificate;
    
    /**
     * created by xiuhong.chen
     * 2017/12/28
     * 发起HTTPS请求的工具类
     * getHttpsResponse方法是请求一个https地址,参数requestMethod为字符串“GET”或者“POST”,传null或者“”默认为get方式。
     */
    public class NetWorkUtil {
        /**
         * 发起HTTPS请求
         * @param reqUrl
         * @param requestMethod
         * @return 相应字符串
         */
        public String getHttpsResponse(String reqUrl, String requestMethod) {
            URL url;
            InputStream is;
            String result ="";
    
            try {
                url = new URL(reqUrl);
                HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
    
                TrustManager[] tm = {xtm};
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(null, tm, null);
    
                con.setSSLSocketFactory(ctx.getSocketFactory());
                con.setHostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String arg0, SSLSession arg1) {
                        return true;
                    }
                });
    
                con.setDoInput(true); //允许输入流,即允许下载
    
                //在android中必须将此项设置为false
                con.setDoOutput(false); //允许输出流,即允许上传
                con.setUseCaches(false); //不使用缓冲
                if (null != requestMethod && !requestMethod.equals("")) {
                    con.setRequestMethod(requestMethod); //使用指定的方式
                } else {
                    con.setRequestMethod("GET"); //使用get请求
                }
                is = con.getInputStream();   //获取输入流,此时才真正建立链接
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader bufferReader = new BufferedReader(isr);
                String inputLine;
                while ((inputLine = bufferReader.readLine()) != null) {
                    result += inputLine + "\n";
                }
                System.out.println(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        X509TrustManager xtm = new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
    
            @Override
            public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                    throws CertificateException {
            }
    
            @Override
            public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                    throws CertificateException {
            }
        };
    }
    
    

    定义一个默认启动的servlet,在init方法中启动一个新的线程去获取accessToken:

    此处需要将JSON数据解析为object, 需要用到fastjson.jar

    package AccessToken;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    
    
    public class AccessTokenServlet extends HttpServlet {
        static Logger logger = LoggerFactory.getLogger(AccessTokenServlet.class);
    
        @Override
        public void init() throws ServletException {
            System.out.println("-----启动AccessTokenServlet-----");
            super.init();
    
            final String appId = getInitParameter("appId");
            final String appSecret = getInitParameter("appSecret");
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            //获取accessToken
                            AccessTokenInfo.accessToken = getAccessToken(appId, appSecret);
                            //获取成功
                            if (AccessTokenInfo.accessToken != null) {
                                //获取到access_token 休眠7000秒,大约2个小时左右
                                Thread.sleep(7000 * 1000);
                            } else {
                                //获取失败
                                Thread.sleep(1000 * 3); //获取的access_token为空 休眠3秒
                            }
                        } catch (Exception e) {
                            System.out.println("发生异常:" + e.getMessage());
                            e.printStackTrace();
                            try {
                                Thread.sleep(1000 * 10); //发生异常休眠1秒
                            } catch (Exception e1) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }).start();
        }
    
        private AccessToken getAccessToken(String appId, String appSecret) {
            NetWorkUtil netHelper = new NetWorkUtil();
            /**
             * 接口地址为https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,其中grant_type固定写为client_credential即可。
             */
            String Url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
            //此请求为https的get请求,返回的数据格式为{"access_token":"ACCESS_TOKEN","expires_in":7200}
            String result = netHelper.getHttpsResponse(Url, "");
            System.out.println("获取到的access_token="+result);
    
            //使用FastJson将Json字符串解析成Json对象
            JSONObject json = JSON.parseObject(result);
            AccessToken token = new AccessToken();
            token.setTokenName(json.getString("access_token"));
            token.setExpireSecond(json.getInteger("expires_in"));
            return token;
        }
    
    }
    
    

    然后在web.xml中配置AccessTokenServlet:

    	<servlet>
            <servlet-name>accessTokenServlet</servlet-name>
            <servlet-class>weChatServlet.AccessTokenServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>accessTokenServlet</servlet-name>
            <url-pattern>/accessTokenServlet</url-pattern> <!--url-pattern必须与servlet-name一致-->
        </servlet-mapping>
    

    index.jsp:

    <%-- Created by IntelliJ IDEA. --%>
    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
    <c:set var="basePath" value="${pageContext.request.contextPath }"></c:set>
    <%@ page import="AccessToken.AccessTokenInfo"%>
    <html>
      <head>
        <title></title>
      </head>
      <body>
    
          <h3>微信公众号测试!</h3>
          <form action="${pageContext.request.contextPath}/weChatServlet" method="get">
              <button οnclick="submit">测试微信公众号</button>
          </form>
    
          <hr/>
    		
          <%--获取access_token--%>
          <form action="${pageContext.request.contextPath}/accessTokenServlet" method="get">
              <button οnclick="submit">获取access_token</button>
          </form>
          <c:if test="AccessTokenInfo.accessToken != null">
              access_token为:<%=AccessTokenInfo.accessToken.getTokenName()%>
          </c:if>
    
      </body>
    </html>
    

    启动项目, 点击获取access_token按钮: 获取出错, 返回码40013,表示AppID无效错误

    -----启动AccessTokenServlet-----
    SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
    SLF4J: Defaulting to no-operation (NOP) logger implementation
    SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
    {"errcode":40013,"errmsg":"invalid appid hint: [OwC0oa06551466]"}
    
    获取到的access_token={"errcode":40013,"errmsg":"invalid appid hint: [OwC0oa06551466]"}
    

    再次确认appID和appsecret没有错误之后,再次获取token:

    image.png

    至此, access_token获取成功 !

    6.总结一下项目启动之后, 通过微信公众号测试的全过程:

    1. 开启外网访问 : CMD进入natapp目录下, 运行命令natapp -authtoken yourauthtoken , 得到外网访问的域名

    2. Tomcat启动项目

    3. 进入微信公众号测试管理平台, 修改接口配置信息URL为: 新域名/weChatServlet , 待签名校验通过,就可以测试

    4. 进入测试公众号, 发送消息进行测试

    7.被动发送用户消息

    业务逻辑(一) — 发送文本消息

    经过上述的三步,我们开发前的准备工作已经完成了,接下来要做的就是接收微信服务器发送的消息并做出响应

    从微信公众平台接口消息指南中可以了解到,当用户向公众帐号发消息时,微信服务器会将消息通过POST方式提交给我们在接口配置信息中填写的URL,而我们就需要在URL所指向的请求处理类WxServlet的doPost方法中接收消息、处理消息和响应消息。

    可以参考微信API文档 – 被动回复用户消息 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543

    1)编写一个用于处理消息的工具类

    这个工具类主要是解析消息, 构建消息

    package weChatServlet;
    
    import org.dom4j.Document;
    import org.dom4j.Element;
    import org.dom4j.io.SAXReader;
    
    import javax.servlet.http.HttpServletRequest;
    import java.io.InputStream;
    import java.text.DateFormat;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class MessageUtil {
        /**
         * 解析微信发来的请求(XML)
         * @param request
         * @return map
         * @throws Exception
         */
        public static Map<String,String> parseXml(HttpServletRequest request) throws Exception {
            // 将解析结果存储在HashMap中
            Map<String,String> map = new HashMap();
            // 从request中取得输入流
            InputStream inputStream = request.getInputStream();
            System.out.println("获取输入流");
            // 读取输入流
            SAXReader reader = new SAXReader();
            Document document = reader.read(inputStream);
            // 得到xml根元素
            Element root = document.getRootElement();
            // 得到根元素的所有子节点
            List<Element> elementList = root.elements();
    
            // 遍历所有子节点
            for (Element e : elementList) {
                System.out.println(e.getName() + "|" + e.getText());
                map.put(e.getName(), e.getText());
            }
    
            // 释放资源
            inputStream.close();
            inputStream = null;
            return map;
        }
    
        /**
         * 根据消息类型 构造返回消息
         */
        public static String buildXml(Map<String,String> map) {
            String result;
            String msgType = map.get("MsgType").toString();
            System.out.println("MsgType:" + msgType);
            if(msgType.toUpperCase().equals("TEXT")){
                result = buildTextMessage(map, "Cherry的小小窝, 请问客官想要点啥?");
            }else{
                String fromUserName = map.get("FromUserName");
                // 开发者微信号
                String toUserName = map.get("ToUserName");
                result = String
                        .format(
                                "<xml>" +
                                        "<ToUserName><![CDATA[%s]]></ToUserName>" +
                                        "<FromUserName><![CDATA[%s]]></FromUserName>" +
                                        "<CreateTime>%s</CreateTime>" +
                                        "<MsgType><![CDATA[text]]></MsgType>" +
                                        "<Content><![CDATA[%s]]></Content>" +
                                        "</xml>",
                                fromUserName, toUserName, getUtcTime(),
                                "请回复如下关键词:\n文本\n图片\n语音\n视频\n音乐\n图文");
            }
    
            return result;
        }
    
        /**
         * 构造文本消息
         *
         * @param map
         * @param content
         * @return
         */
        private static String buildTextMessage(Map<String,String> map, String content) {
            //发送方帐号
            String fromUserName = map.get("FromUserName");
            // 开发者微信号
            String toUserName = map.get("ToUserName");
            /**
             * 文本消息XML数据格式
             */
            return String.format(
                    "<xml>" +
                            "<ToUserName><![CDATA[%s]]></ToUserName>" +
                            "<FromUserName><![CDATA[%s]]></FromUserName>" +
                            "<CreateTime>%s</CreateTime>" +
                            "<MsgType><![CDATA[text]]></MsgType>" +
                            "<Content><![CDATA[%s]]></Content>" + "</xml>",
                    fromUserName, toUserName, getUtcTime(), content);
        }
    
        private static String getUtcTime() {
            Date dt = new Date();// 如果不需要格式,可直接用dt,dt就是当前系统时间
            DateFormat df = new SimpleDateFormat("yyyyMMddhhmm");// 设置显示格式
            String nowTime = df.format(dt);
            long dd = (long) 0;
            try {
                dd = df.parse(nowTime).getTime();
            } catch (Exception e) {
    
            }
            return String.valueOf(dd);
        }
    
    }
    
    
    2)在WxServlet的doPost方法中处理请求

    WxServlet的doPost方法的代码如下:

    package weChatServlet;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.Arrays;
    import java.util.Map;
    
    public class WeChatAccounts extends HttpServlet {
        static Logger logger = LoggerFactory.getLogger(WeChatAccounts.class);
    
        /*
        * 自定义token, 用作生成签名,从而验证安全性
        * */
        private final String TOKEN = "cherry";
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // TODO 接收、处理、响应由微信服务器转发的用户发送给公众帐号的消息
            // 将请求、响应的编码均设置为UTF-8(防止中文乱码)
            req.setCharacterEncoding("UTF-8");
            resp.setCharacterEncoding("UTF-8");
            System.out.println("请求进入");
            String result = "";
            try {
                Map<String,String> map = MessageUtil.parseXml(req);
    
                System.out.println("开始构造消息");
                result = MessageUtil.buildXml(map);
                System.out.println(result);
    
                if(result.equals("")){
                    result = "未正确响应";
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("发生异常:"+ e.getMessage());
            }
            resp.getWriter().println(result);
        }
        @Override
        public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("-----开始校验签名-----");
         ..............................
            
        }
    }
    
    

    将WxStudy部署到Tomcat服务器,启动服务器,记得使用ngrok将本地Tomcat服务器的8080端口映射到外网,保证接口配置信息的URL地址:http://xdp.ngrok.natapp.cn/WxServlet可以正常与微信服务器通信

    登录到我们的测试公众号的管理后台,然后用微信扫描一下测试号的二维码,如下图所示:

    image.png

    关注成功后,我们开发好的公众号应用会先给用户发一条提示用户操作的文本消息,微信用户根据提示操作输入"文本",我们的公众号应用接收到用户请求后就给用户回复了一条我们自己构建好的文本消息,如下图所示:

    image.png

    我们公众号获取到的输入流如下:

    第一次关注时获取的输入流, 消息类型为事件event, 订阅
    ToUserName|gh_fbcf752402d4
    FromUserName|oEG9V1PeFHnsQviezVY9D4IDcyAk
    CreateTime|1514461156
    MsgType|event
    Event|subscribe
    EventKey|
    
    第一次发送消息内容为"文本"获取的输入流, 消息类型是text
    ToUserName|gh_fbcf752402d4
    FromUserName|oEG9V1PeFHnsQviezVY9D4IDcyAk
    CreateTime|1514461351
    MsgType|text
    Content|文本
    MsgId|6504561974052876654
    

    我们公众号应用响应给微信用户的文本消息的XML数据如下:

    关注之后返回的消息是:
    MsgType:event
    <xml>
    	<ToUserName><![CDATA[oEG9V1PeFHnsQviezVY9D4IDcyAk]]></ToUserName>
        <FromUserName><![CDATA[gh_fbcf752402d4]]></FromUserName>					     			<CreateTime>1514417940000</CreateTime>
    	<MsgType><![CDATA[text]]></MsgType>
    	<Content><![CDATA[请回复如下关键词:
    	文本
    	图片
    	语音
    	视频
    	音乐
    	图文]]></Content>
    </xml>
    
    
    输入文本之后返回的消息是:
    <xml>
    	<ToUserName><![CDATA[oEG9V1PeFHnsQviezVY9D4IDcyAk]]></ToUserName>
    	<FromUserName><![CDATA[gh_fbcf752402d4]]></FromUserName>
    	<CreateTime>1514418120000</CreateTime>
    	<MsgType><![CDATA[text]]></MsgType>
    	<Content><![CDATA[Cherry的小小窝, 请问客官想要点啥?]]></Content>
    </xml>
    

    业务逻辑(二) — 上传素材获取Media_id

    图片,语音,视频的回复消息构造,这三种消息构造时的都需要一个mediaId,而这个mediaId是通过素材管理接口上传多媒体文件得到的,为了构造图片,语音,视频的这几种回复消息,我事先准备好了测试素材,

    如图片路径为: C:\Users\Chen Xiuhong\Pictures\timg (1).jpg

    然后通过微信公众号平台提供的素材管理接口将图片,语音,视频上传到微信服务器上,上传成功后,微信服务器会给我们返回一个mediaId,用于标识上传成功的多媒体素材,上传素材的工具类代码如下:

    package weChatServlet;
    
    import AccessToken.AccessToken;
    import AccessToken.NetWorkUtil;
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONException;
    import com.alibaba.fastjson.JSONObject;
    import org.apache.commons.httpclient.HttpClient;
    import org.apache.commons.httpclient.HttpException;
    import org.apache.commons.httpclient.HttpStatus;
    import org.apache.commons.httpclient.methods.PostMethod;
    import org.apache.commons.httpclient.methods.multipart.FilePart;
    import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
    import org.apache.commons.httpclient.methods.multipart.Part;
    import org.apache.commons.httpclient.methods.multipart.StringPart;
    import org.apache.commons.httpclient.protocol.Protocol;
    import org.apache.commons.httpclient.protocol.SSLProtocolSocketFactory;
    import org.apache.commons.httpclient.util.HttpURLConnection;
    
    import java.io.*;
    import java.net.URL;
    
    /**
     * Author xiuhong.chen@hand-china.com
     * created on 2018/1/9
     * 上传media素材, 获取media_id
     */
    public class UploadMediaApiUtil {
        // token 接口(GET)
        private static final String ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
        // 素材上传(POST)URL
        private static final String UPLOAD_MEDIA = "https://api.weixin.qq.com/cgi-bin/media/upload";
        // 素材下载:不支持视频文件的下载(GET)
        private static final String DOWNLOAD_MEDIA = "http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s";
    
        public static String getTokenUrl(String appId, String appSecret) {
         return String.format(ACCESS_TOKEN, appId, appSecret);
        }
    
        public static String getDownloadUrl(String token, String mediaId) {
         return String.format(DOWNLOAD_MEDIA, token, mediaId);
        }
    
        /**
         * 通用接口获取token凭证
         * @param appId
         * @param appSecret
         * @return
         */
        public String getAccessToken(String appId, String appSecret) {
            NetWorkUtil netHelper = new NetWorkUtil();
            String Url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
            String result = netHelper.getHttpsResponse(Url, "");
            JSONObject json = JSON.parseObject(result);
            return json.getString("access_token");
        }
    
        /**
         * 素材上传到微信服务器
         * @param file  File file = new File(filePath); // 获取本地文件
         * @param token access_token
         * @param type type只支持四种类型素材(video/image/voice/thumb)
         * @return
         */
        public  JSONObject uploadMedia(File file, String token, String type) {
            if(file == null || token == null || type == null){
                return null;
            }
            if(!file.exists()){
                System.out.println("上传文件不存在,请检查!");
                return null;
            }
            JSONObject jsonObject = null;
            PostMethod post = new PostMethod(UPLOAD_MEDIA);
            post.setRequestHeader("Connection", "Keep-Alive");
            post.setRequestHeader("Cache-Control", "no-cache");
            FilePart media;
            HttpClient httpClient = new HttpClient();
            //信任任何类型的证书
            Protocol myhttps = new Protocol("https", new SSLProtocolSocketFactory(), 443);
            Protocol.registerProtocol("https", myhttps);
    
            try {
                media = new FilePart("media", file);
                Part[] parts = new Part[]{
                        new StringPart("access_token", token),
                        new StringPart("type", type),
                        media
                        };
                MultipartRequestEntity entity = new MultipartRequestEntity(parts,post.getParams());
                post.setRequestEntity(entity);
                int status = httpClient.executeMethod(post);
                if (status == HttpStatus.SC_OK) {
                    String text = post.getResponseBodyAsString();
                    jsonObject = JSONObject.parseObject(text);
                } else {
                    System.out.println("upload Media failure status is:" + status);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (HttpException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return jsonObject;
        }
        public static File downloadMedia(String fileName, String token, String mediaId) {
            String path = getDownloadUrl(token, mediaId);
            //return httpRequestToFile(fileName, url, "GET", null);
    
            if (fileName == null || path == null) {
                return null;
            }
            File file = null;
            HttpURLConnection conn = null;
            InputStream inputStream = null;
            FileOutputStream fileOut = null;
            try {
                 URL url = new URL(path);
                 conn = (HttpURLConnection) url.openConnection();
                 conn.setDoOutput(true);
                 conn.setDoInput(true);
                 conn.setUseCaches(false);
                 conn.setRequestMethod("GET");
    
                 inputStream = conn.getInputStream();
                 if (inputStream != null) {
                     file = new File(fileName);
                 } else {
                     return file;
                 }
    
                 //写入到文件
                 fileOut = new FileOutputStream(file);
                 if (fileOut != null) {
                     int c = inputStream.read();
                     while (c != -1) {
                         fileOut.write(c);
                         c = inputStream.read();
                     }
                 }
            } catch (Exception e) {
            } finally {
                 if (conn != null) {
                     conn.disconnect();
                 }
    
                 try {
                      inputStream.close();
                      fileOut.close();
                   } catch (IOException execption) {
                 }
            }
        return file;
        }
    
    }
    
    
    package weChatServlet;
    
    import com.alibaba.fastjson.JSONObject;
    
    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 java.io.File;
    import java.io.IOException;
    
    /**
     * 上传素材servlet
     */
    @WebServlet(name = "UploadMediaServlet")
    public class UploadMediaServlet extends HttpServlet {
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
        }
    
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            UploadMediaApiUtil uploadMediaApiUtil = new UploadMediaApiUtil();
            String appId = "wx0aa26453a7ec9df7";
            String appSecret = "2819f0c98199daef39cb6220b4d01b96";
            String accessToken = uploadMediaApiUtil.getAccessToken(appId,appSecret);
    
            String filePath = "C:\\Users\\Chen Xiuhong\\Pictures\\timg (1).jpg";
            File file = new File(filePath);
            String type = "IMAGE";
            JSONObject jsonObject = uploadMediaApiUtil.uploadMedia(file,accessToken,type);
            System.out.println(jsonObject.toString());
        }
    }
    
    

    运行的时候老是在PostMethod处报错, 错误信息显示缺包

    image.png

    百度之后发现Http协议使用封装jar包(commons-codec-1.3.jar、commons-httpclient-3.1.jar、commons-logging-1.1.jar), 所以还缺少一个包, 添加三个包之后就成功运行,可以得到media_id

    image.png

    可以看到,素材上传成功后,微信服务器就会返回一个media_id,用于标识上传后的文件.有了这个media_id后,我们就可以构建我们想要的图片,语音,视频回复消息了.

    业务逻辑(三) — 发送图片消息

    在第7部分中我们已经上传了一个图片素材, 并且也获得了media_id

    UCWXNCogK5ub6YFFQf7QcEpvDIYLf3Zh0L5W9i4aEp2ehfnTrASeV59x3LMD88SS
    

    接下来我们就使用media_id来构造图片消息:

    	/**
         *  构建图片消息
         * @param map
         * @param picUrl
         * @return
         */
        private static String buildImageMessage(Map<String, String> map, String picUrl) {
            String fromUserName = map.get("FromUserName");
            String toUserName = map.get("ToUserName");
            /*返回指定的图片(该图片是上传为素材的,获得其media_id)*/
            //String media_id = "UCWXNCogK5ub6YFFQf7QcEpvDIYLf3Zh0L5W9i4aEp2ehfnTrASeV59x3LMD88SS";
    
            /*返回用户发过来的图片*/
            String media_id = map.get("MediaId");
            return String.format(
                    "<xml>" +
                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                    "<CreateTime>%s</CreateTime>" +
                    "<MsgType><![CDATA[image]]></MsgType>" +
                    "<Image>" +
                    "   <MediaId><![CDATA[%s]]></MediaId>" +
                    "</Image>" +
                    "</xml>",
                    fromUserName,toUserName, getUtcTime(),media_id
            );
        }
    
    

    用户发送图片时,公众号返回给用户我之前上传的素材; 或者将用户发送的图片又返回一次

    image.png

    业务逻辑(四) — 发送语音消息

     	/**
         * 构造语音消息
         * @param map
         * @return
         */
        private static String buildVoiceMessage(Map<String, String> map) {
            String fromUserName = map.get("FromUserName");
            String toUserName = map.get("ToUserName");
            /*返回用户发过来的语音*/
            String media_id = map.get("MediaId");
            return String.format(
                    "<xml>" +
                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                    "<CreateTime>%s</CreateTime>" +
                    "<MsgType><![CDATA[voice]]></MsgType>" +
                    "<Voice>" +
                    "   <MediaId><![CDATA[%s]]></MediaId>" +
                    "</Voice>" +
                    "</xml>",
                    fromUserName,toUserName, getUtcTime(),media_id
            );
        }
    
    

    业务逻辑(五) — 发送视频消息

    首先需要调用业务逻辑(二)上传一段小视频,获取media_id,上传的视频不大于10MB,支持MP4格式, 然后当用户发送视频格式时,公众号就回复视频格式

    	/**
         * 回复视频消息
         * @param map
         * @return
         */
        private static String buildVideoMessage(Map<String, String> map) {
            String fromUserName = map.get("FromUserName");
            String toUserName = map.get("ToUserName");
            String title = "客官发过来的视频哟~~";
            String description = "客官您呐,现在肯定很开心,对不啦 嘻嘻?";
            /*返回用户发过来的视频*/
            //String media_id = map.get("MediaId");
            String media_id = "hTl1of-w78xO-0cPnF_Wax1QrTwhnFpG1WBkAWEYRr9Hfwxw8DYKPYFX-22hAwSs";
            return String.format(
                    "<xml>" +
                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                    "<CreateTime>%s</CreateTime>" +
                    "<MsgType><![CDATA[video]]></MsgType>" +
                    "<Video>" +
                    "   <MediaId><![CDATA[%s]]></MediaId>" +
                    "   <Title><![CDATA[%s]]></Title>" +
                    "   <Description><![CDATA[%s]]></Description>" +
                    "</Video>" +
                    "</xml>",
                    fromUserName,toUserName, getUtcTime(),media_id,title,description
            );
        }
    

    展示效果如下:

    image.png

    注意: media_id只能用上传的视频的id, 不能使用用户发送视频时的那个id, 至于原因待探究

    业务逻辑(六) — 发送音乐消息

    	/**
         * 回复音乐消息
         * @param map
         * @return
         */
        private static String buildMusicMessage(Map<String, String> map) {
            String fromUserName = map.get("FromUserName");
            String toUserName = map.get("ToUserName");
            String title = "亲爱的路人";
            String description = "多听音乐 心情棒棒 嘻嘻?";
            String hqMusicUrl ="http://www.kugou.com/song/20qzz4f.html?frombaidu#hash=20C16B9CCCCF851D1D23AF52DD963986&album_id=0";
            return String.format(
                    "<xml>" +
                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                    "<CreateTime>%s</CreateTime>" +
                    "<MsgType><![CDATA[music]]></MsgType>" +
                    "<Music>" +
                    "   <Title><![CDATA[%s]]></Title>" +
                    "   <Description><![CDATA[%s]]></Description>" +
                    "   <MusicUrl>< ![CDATA[%s] ]></MusicUrl>" +  //非必须项 音乐链接
                    "   <HQMusicUrl><![CDATA[%s]]></HQMusicUrl>"+ //非必须项 高质量音乐链接,WIFI环境优先使用该链接播放音乐
                    "</Music>" +
                    "</xml>",
                    fromUserName,toUserName, getUtcTime(),title,description,hqMusicUrl,hqMusicUrl
            );
        }
    

    image.png

    业务逻辑(七) — 发送图文消息

    /**
         * 返回图文消息
         * @param map
         * @return
         */
        private static String buildNewsMessage(Map<String, String> map) {
            String fromUserName = map.get("FromUserName");
            String toUserName = map.get("ToUserName");
            String title1 = "HAP审计的实现和使用";
            String description1 = "由于HAP框架用的是Spring+SpringMVC+Mybatis,其中Mybatis中的拦截器可以选择在被拦截的方法前后执行自己的逻辑。所以我们通过拦截器实现了审计功能,当用户对某个实体类进行增删改操作时,拦截器可以拦截,然后将操作的数据记录在审计表中,便于用户以后审计。";
            String picUrl1 ="http://upload-images.jianshu.io/upload_images/7855203-b9e9c9ded8a732a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240";
            String textUrl1 = "http://blog.csdn.net/a1786223749/article/details/78330890";
    
            String title2 = "KendoUI之Grid的问题详解";
            String description2 = "kendoLov带出的值出现 null和undefined";
            String picUrl2 ="https://demos.telerik.com/kendo-ui/content/shared/images/theme-builder.png";
            String textUrl2 = "http://blog.csdn.net/a1786223749/article/details/78330908";
    
            return String.format(
                    "<xml>" +
                    "<ToUserName><![CDATA[%s]]></ToUserName>" +
                    "<FromUserName><![CDATA[%s]]></FromUserName>" +
                    "<CreateTime>%s</CreateTime>" +
                    "<MsgType><![CDATA[news]]></MsgType>" +
                    "<ArticleCount>2</ArticleCount>" + //图文消息个数,限制为8条以内
                    "<Articles>" + //多条图文消息信息,默认第一个item为大图,注意,如果图文数超过8,则将会无响应
                        "<item>" +
                            "<Title><![CDATA[%s]]></Title> " +
                            "<Description><![CDATA[%s]]></Description>" +
                            "<PicUrl><![CDATA[%s]]></PicUrl>" + //图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
                            "<Url><![CDATA[%s]]></Url>" + //点击图文消息跳转链接
                        "</item>" +
                        "<item>" +
                            "<Title><![CDATA[%s]]></Title>" +
                            "<Description><![CDATA[%s]]></Description>" +
                            "<PicUrl><![CDATA[%s]]]></PicUrl>" +
                            "<Url><![CDATA[%s]]]></Url>" +
                        "</item>" +
                    "</Articles>" +
                    "</xml>"
                    ,
                    fromUserName,toUserName, getUtcTime(),
                    title1,description1,picUrl1,textUrl1,
                    title2,description2,picUrl2,textUrl2
            );
        }
    

    image.png

    网络不太好,所以图片没有加载出来, 在手机上测试是可以看到图片的.

    8.天气预报功能开发

    借助百度API查询天气, 所以首先我们要在百度开发平台注册信息

    image.png

    申请天气查询的API, 然后获取AK

    image.png

    但是查询天气的时候AK并不能使用, 显示APP服务被禁用

    image.png

    看来用百度的API这一步行不通, 只能使用Webservice网站上公开的接口

    调用WebService查询天气预报

    直接使用wsimport 通过该地址生成Java文件时,会报错。因为该wsdl里面包含 ref = “s:schema” 这样的引用。而jaxb是不支持的。所以需要手动将该wsdl下载下来做下修改,然后再生成java文件。

    wsimport -keep http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl

    D:\Eclipse Files\TEST_EMAIL\src\weatherService>wsimport -keep  http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl
    正在解析 WSDL...
    
    
    [WARNING] src-resolve.4.2: 解析组件 's:schema' 时出错。在该组件中检测到 's:schema' 位于名称空间 'http://www.w3.org/2001/XMLSchema' 中, 但无法从方案文档 'http://
    ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1' 引用此名称空间的组件。如果这是不正确的名称空间, 则很可能需要更改 's:schema' 的前缀。如果这是正确
    的名称空间, 则应将适当的 'import' 标记添加到 'http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1'。
      http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1的第 15 行
    
    [WARNING] src-resolve: 无法将名称 's:schema' 解析为 'element declaration' 组件。
      http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl#types?schema1的第 15 行
    
    [ERROR] undefined element declaration 's:schema'
      http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl的第 15 行
    
    [ERROR] undefined element declaration 's:schema'
      http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl的第 61 行
    
    [ERROR] undefined element declaration 's:schema'
      http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl的第 101 行
    
    Exception in thread "main" com.sun.tools.internal.ws.wscompile.AbortException
            at com.sun.tools.internal.ws.processor.modeler.wsdl.JAXBModelBuilder.bind(JAXBModelBuilder.java:129)
            at com.sun.tools.internal.ws.processor.modeler.wsdl.WSDLModeler.buildJAXBModel(WSDLModeler.java:2283)
            at com.sun.tools.internal.ws.processor.modeler.wsdl.WSDLModeler.internalBuildModel(WSDLModeler.java:183)
            at com.sun.tools.internal.ws.processor.modeler.wsdl.WSDLModeler.buildModel(WSDLModeler.java:126)
            at com.sun.tools.internal.ws.wscompile.WsimportTool.buildWsdlModel(WsimportTool.java:429)
            at com.sun.tools.internal.ws.wscompile.WsimportTool.run(WsimportTool.java:190)
            at com.sun.tools.internal.ws.wscompile.WsimportTool.run(WsimportTool.java:168)
            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
            at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:497)
            at com.sun.tools.internal.ws.Invoker.invoke(Invoker.java:159)
            at com.sun.tools.internal.ws.WsImport.main(WsImport.java:42)
    
    

    解决方法:

     1) 打开 http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl , 页面另存为xml文件;
     2)将所有的
    <s:element ref="s:schema" />
    <s:any />
    改成 <s:any minOccurs="2" maxOccurs="2"/>,一共有三处需要修改,建议查找<s:element ref="s:schema" />,修改时把<s:any />也要删掉
    3)运行命令
    wsimport -keep D:\WeatherWS.xml
    	
    需要注意的是,要修改WeatherWS.java文件中wsdl的url,因为我们用的是本地文件生成的,要修改成网站的
    url:http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl
    

    image.png

    image.png

    然后就可以调用生成的代码weatherWSSoap.getWeather(cityName,null);,来查询天气:

    public static String buildXml(Map<String,String> map) {
            String result = "";
            String msgType = map.get("MsgType").toString().toUpperCase();
            String content = map.get("Content");
            if("TEXT".equals(msgType)){
                /*查询天气*/
                if(content.contains("天气") && !"".equals(content)){
                    if(content.contains(":")){
                        String cityName = content.substring(content.lastIndexOf(":")+1,content.length());
                        WeatherInfo weather = new WeatherInfo();
                        String weaInfo = weather.getWeatherInfo(cityName);
                        result = buildTextMessage(map,weaInfo);
                    }else{
                        String notice = "查询天气的正确姿势: 天气:城市名\n请客官输入正确的格式哟~";
                        result = buildTextMessage(map,notice);
                    }
                }else{
                    result = buildTextMessage(map,"");
                }
            }
            return result;
        }
    
    package weChatServlet;
    
    import cn.com.webxml.ArrayOfString;
    import cn.com.webxml.WeatherWS;
    import cn.com.webxml.WeatherWSSoap;
    
    import java.util.List;
    
    /**
     * 调用weather 的webservice, 并处理json数据
     */
    public class WeatherInfo {
        public String getWeatherInfo(String cityName){
            /*实例化工厂WeatherWS  创建实例WeatherWSSoap  调用实例的方法getWeather()*/
            WeatherWS weatherWS = new WeatherWS();
            WeatherWSSoap weatherWSSoap = weatherWS.getWeatherWSSoap();
    
            /*响应信息*/
            StringBuffer sb = new StringBuffer();
    
            /*获取指定城市的天气预报*/
            ArrayOfString weatherInfo = weatherWSSoap.getWeather(cityName,null);
            List<String> listWeatherInfo = weatherInfo.getString();
            for(String str :  listWeatherInfo){
                if(!str.contains(".gif")){
                    sb.append(str);
                    sb.append("\n");
                    System.out.println(str);
                    System.out.println("------------------------");
                }
            }
            return sb.toString();
        }
    }
    
    

    结果如下: 查询天气的格式为: 天气:城市名

    image.png

    image.png

    9.翻译功能开发(有道智云)

    网络上有很多翻译API,大家可以根据自己的需求进行选择。这里我们选择应用比较广泛的,翻译功能还比较不错的有道翻译API,下面对这种API的相关信息进行分析。

    2017.12.31之前,我们可以使用有道翻译API

    关于有道翻译API使用 ,请参考链接: http://blog.csdn.net/nomasp/article/details/48995039

    有道翻译API key申请,请参考链接: http://fanyi.youdao.com/openapi?path=data-mode

    image.png

    有道翻译API已经禁用,所以我们要去有道智云寻求方法(http://ai.youdao.com/index.s), 首先需要注册,注册之后账户里默认有100元的额度,使用有道智云是有字数限制的,资费如下:

    image.png

    image.png

    然后我们需要创建应用, 绑定服务之后,就可以生成appKey和密钥 , 这两个数据在调用API中会用到

    image.png

    有道智云的API文档: http://ai.youdao.com/docs/doc-trans-api.s#p01

    下边就详细介绍如何调用有道智云API

    1) 接口调用参数:

    调用API需要向接口发送以下字段来访问服务。

    字段名 类型 含义 必填 备注
    q text 要翻译的文本 True 必须是UTF-8编码
    from text 源语言 True 语言列表 (可设置为auto)
    to text 目标语言 True 语言列表 (可设置为auto)
    appKey text 应用 ID True 可在 应用管理 查看
    salt text 随机数 True
    sign text 签名,通过md5(appKey+q+salt+密钥)生成 True appKey+q+salt+密钥的MD5值

    签名生成方法如下:

    1. 将请求参数中的 appKey,翻译文本 q (注意为UTF-8编码),随机数 salt密钥 (可在 应用管理 查看), 按照 appKey+q+salt+密钥 的顺序拼接得到字符串 str
    2. 对字符串 str 做md5,得到32位大写的 sign (参考Java生成MD5示例)

    注意:

    1. 请先将需要翻译的文本转换为 UTF-8 编码
    2. 在发送 HTTP 请求之前需要对各字段做 URL encode。
    3. 在生成签名拼接 appKey+q+salt+密钥 字符串时,q 不需要做 URL encode,在生成签名之后,发送 HTTP 请求之前才需要对要发送的待翻译文本字段 q 做 URL encode。

    我们代码中需要使用CloseableHttpClient, 所以需要引入两个jar包 httpclient-4.5.3.jar 和 httpcore-4.4.6.jar

    2) 输出结果

    返回的结果是json格式,包含字段与FROM和TO的值有关,具体说明如下:

    字段名 类型 含义 备注
    errorCode text 错误返回码 一定存在
    query text 源语言 查询正确时,一定存在
    translation text 翻译结果 查询正确时一定存在
    basic text 词义 基本词典,查词时才有
    web text 词义 网络释义,该结果不一定存在
    l text 源语言和目标语言 一定存在
    dict text 词典deeplink 查询语种为支持语言时,存在
    webdict text webdeeplink 查询语种为支持语言时,存在
    3)支持的语言表
    语言 代码
    中文 zh-CHS
    日文 ja
    英文 EN
    韩文 ko
    法文 fr
    俄文 ru
    葡萄牙文 pt
    西班牙文 es
    4) 错误代码列表
    错误码 含义
    101 缺少必填的参数,出现这个情况还可能是et的值和实际加密方式不对应
    102 不支持的语言类型
    103 翻译文本过长
    104 不支持的API类型
    105 不支持的签名类型
    106 不支持的响应类型
    107 不支持的传输加密类型
    108 appKey无效,注册账号, 登录后台创建应用和实例并完成绑定, 可获得应用ID和密钥等信息,其中应用ID就是appKey( 注意不是应用密钥)
    109 batchLog格式不正确
    110 无相关服务的有效实例
    111 开发者账号无效,可能是账号为欠费状态
    201 解密失败,可能为DES,BASE64,URLDecode的错误
    202 签名检验失败
    203 访问IP地址不在可访问IP列表
    301 辞典查询失败
    302 翻译查询失败
    303 服务端的其它异常
    401 账户已经欠费停
    5) DEMO

    result = EntityUtils.toString(httpEntity, “utf-8”); 返回的是JSON对象格式的字符串,如下:

    {
    	"web":[
    		{
    			"value":["Hello","How do you do","hi"],
    			"key":"你好"
    		},
    
    		{
                "value":["How are you","How Do You Do","Harvey, how are you Harvey"],
                "key":"你好吗"
    		},
    
    		{
                "value":["Teacher Kim Bong-du","My Teacher Mr Kim","Seonsaeng Kim Bong-du"],
                "key":"老师你好"
    		}
    	],
    
    	"query":"你好",
    
    	"translation":[
    		"How are you"
    	],
    
    	"errorCode":"0",
    
    	"dict":{"url":"yddict://m.youdao.com/dict?le=eng&q=%E4%BD%A0%E5%A5%BD"},
    
    	"webdict":{"url":"http://m.youdao.com/dict?le=eng&q=%E4%BD%A0%E5%A5%BD"},
    
    	"basic":{
    		"explains":["hello","hi"]
    	},
    
    	"l":"zh-CHS2EN"
    }
    

    所以需要将JSON字符串转换为实体类对象DTO

    /*处理JSON字符串为实体对象TranslateResponseDto*/
    TranslateResponseDto dto = new TranslateResponseDto();
    dto=JSON.parseObject(result, TranslateResponseDto.class);
    

    image.png

    YouDaoAPI.java

    package translate;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONArray;
    import com.alibaba.fastjson.JSONObject;
    import org.apache.http.HttpEntity;
    import org.apache.http.client.entity.UrlEncodedFormEntity;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.message.BasicNameValuePair;
    import org.apache.http.util.EntityUtils;
    
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    
    public class YouDaoAPI {
        public String translate(String q)throws Exception{
            //应用申请的key
            String appKey ="2d156317f9da5d91";
            //要翻译的文本 必须是UTF-8编码
            String query = q;
            //随机数
            String salt = String.valueOf(System.currentTimeMillis());
            //源语言
            String from = "auto";
            //目标语言
            String to = "auto";
            //签名,通过md5(appkey+q+salt+密钥)生成
            String sign = md5(appKey + query + salt+"PXwLK5mGbARMsbtjIfpZBM7sDwt40YAL");
    
            Map params = new HashMap();
            params.put("q", query);
            params.put("from", from);
            params.put("to", to);
            params.put("sign", sign);
            params.put("salt", salt);
            params.put("appKey", appKey);
            return requestForHttp("http://openapi.youdao.com/api", params);
        }
    
        public  String requestForHttp(String url,Map requestParams) throws Exception{
            String result = null; //翻译后的JSON字符串
            StringBuffer sb = new StringBuffer(); //处理后的返回结果
    
            CloseableHttpClient httpClient = HttpClients.createDefault();
            /**HttpPost*/
            HttpPost httpPost = new HttpPost(url);
    
            List params = new ArrayList();
            Iterator it = requestParams.entrySet().iterator();
            while (it.hasNext()) {
                Entry<String, String> en = (Entry)it.next();
                String key = en.getKey();
                String value = en.getValue();
                if (value != null) {
                    params.add(new BasicNameValuePair(key, value));
                }
            }
            httpPost.setEntity(new UrlEncodedFormEntity(params,"UTF-8"));
            /**HttpResponse*/
            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            try{
                HttpEntity httpEntity = httpResponse.getEntity();
                result = EntityUtils.toString(httpEntity, "utf-8");
                //EntityUtils.consume(httpEntity);
    
                /*处理JSON字符串为实体对象TranslateResponseDto*/
                TranslateResponseDto dto = new TranslateResponseDto();
                dto=JSON.parseObject(result, TranslateResponseDto.class);
    
                /*遍历实体类的每个字段,拼接为字符串返回给用户*/
                if(!"".equals(dto.getQuery()) && dto.getQuery() != null){
                    sb.append("原文:"+dto.getQuery() + "\n");
                }
                if(!"".equals(dto.getTranslation()) && dto.getTranslation() != null){
                    sb.append("翻译结果:");
                    List<String> translation = dto.getTranslation();
                    for(String s : translation){
                        sb.append(s + "\n");
                    }
                }
                if(!"".equals(dto.getBasic()) && dto.getBasic() != null){
                    Map<String, List<String>> basic = dto.getBasic();
                    sb.append("词义:");
                    for (Map.Entry<String, List<String>> entry : basic.entrySet()) {
                        sb.append(entry.getValue()+ "\n");
                    }
                }
                if(!"".equals(dto.getErrorCode()) && dto.getErrorCode() != null && !"0".equals(dto.getErrorCode())){
                    if("103".equals(dto.getErrorCode())){
                        sb.append("翻译出错:翻译文本过长\n");
                    }else{
                        sb.append("翻译出错:错误代码为"+dto.getErrorCode()+"\n");
                    }
                }
    
    
            }finally{
                try{
                    if(httpResponse!=null){
                        httpResponse.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
            return sb.toString();
        }
    
        /**
         * 生成32位MD5摘要
         * @param string
         * @return
         */
        public static String md5(String string) {
            if(string == null){
                return null;
            }
            char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                    'A', 'B', 'C', 'D', 'E', 'F'};
    
            try{
                byte[] btInput = string.getBytes("utf-8");
                /** 获得MD5摘要算法的 MessageDigest 对象 */
                MessageDigest mdInst = MessageDigest.getInstance("MD5");
                /** 使用指定的字节更新摘要 */
                mdInst.update(btInput);
                /** 获得密文 */
                byte[] md = mdInst.digest();
                /** 把密文转换成十六进制的字符串形式 */
                int j = md.length;
                char str[] = new char[j * 2];
                int k = 0;
                for (byte byte0 : md) {
                    str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                    str[k++] = hexDigits[byte0 & 0xf];
                }
                return new String(str);
            }catch(NoSuchAlgorithmException | UnsupportedEncodingException e){
                return null;
            }
        }
    
        /**
         * 根据api地址和参数生成请求URL
         * @param url
         * @param params
         * @return
         */
        public static String getUrlWithQueryString(String url, Map params) {
            if (params == null) {
                return url;
            }
    
            StringBuilder builder = new StringBuilder(url);
            if (url.contains("?")) {
                builder.append("&");
            } else {
                builder.append("?");
            }
    
            int i = 0;
            for (String key : (List<String>)params.keySet()) {
                String value = (String) params.get(key);
                if (value == null) { // 过滤空的key
                    continue;
                }
    
                if (i != 0) {
                    builder.append('&');
                }
    
                builder.append(key);
                builder.append('=');
                builder.append(encode(value));
    
                i++;
            }
    
            return builder.toString();
        }
    
        /**
         * 进行URL编码
         * @param input
         * @return
         */
        public static String encode(String input) {
            if (input == null) {
                return "";
            }
    
            try {
                return URLEncoder.encode(input, "utf-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
    
            return input;
        }
    }
    
    
    
    if(content.contains("翻译") && !"".equals(content)){
                    if(content.contains(":")){
                        String word = content.substring(content.lastIndexOf(":")+1,content.length()).trim();
                        YouDaoAPI translateInfo = new YouDaoAPI();
                        String weaInfo = translateInfo.translate(word);
                        result = buildTextMessage(map,weaInfo);
                    }else{
                        String notice = "翻译的正确姿势: 翻译:翻译文本\n请客官输入正确的格式哟~";
                        result = buildTextMessage(map,notice);
                    }
    

    image.png

    image.png

    10.发送emoji表情

    关于普通表情和emoji表情的处理可以参考:http://blog.csdn.net/frankcheng5143/article/details/64129433

    一般的表情是/:: 或者 [heart] 或者 空白的(其实是有特殊编码的), 所以此处我们要对emoji转换为unicode编码, 代码如下:

    package weChatServlet;
    
    import java.util.Formatter;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    /**
     * emoji表情的工具类
     * 工具类中是将emoji转换为unicode编码,可以将其替换为图片,或者用空格过滤掉。
     */
    public class EmojiUtil {
    
        /**
         * 显示不可见字符的Unicode
         *
         * @param input
         * @return
         */
        public static String escapeUnicode(String input) {
            StringBuilder sb = new StringBuilder(input.length());
            @SuppressWarnings("resource")
            Formatter format = new Formatter(sb);
            for (char c : input.toCharArray()) {
                if (c < 128) {
                    sb.append(c);
                } else {
                    format.format("\\u%04x", (int) c);
                }
            }
            return sb.toString();
        }
    
        /**
         * 将emoji替换为unicode
         *
         * @param source
         * @return
         */
        public  String filterEmoji(String source) {
            if (source != null) {
                Pattern emoji = Pattern.compile("[\ue000-\uefff]", Pattern.CASE_INSENSITIVE);
                Matcher emojiMatcher = emoji.matcher(source);
                Map<String, String> tmpMap = new HashMap<>();
                while (emojiMatcher.find()) {
                    String key = emojiMatcher.group();
                    String value = escapeUnicode(emojiMatcher.group());
                    tmpMap.put(key, value);
                }
                if (!tmpMap.isEmpty()) {
                    for (Map.Entry<String, String> entry : tmpMap.entrySet()) {
                        String key = entry.getKey().toString();
                        String value = entry.getValue().toString();
                        source = source.replace(key, value);
                    }
                }
            }
            return source;
        }
    }
    
    

    在messageUtil中调用emoji工具类

    String content = map.get("Content");
    
    EmojiUtil emojiUtil = new EmojiUtil();
    String unicodeEmoji = emojiUtil.filterEmoji(content); //unicode编码的Emoji
    
    if(content.contains("/:")  || content.contains("\\:")  || content.contains("[") && content.contains("]") || unicodeEmoji.contains("\\")){
        result = buildTextMessage(map,"客官发送的内容很特别哟~/:heart    " + content);
    }
    
    

    结果如下:

    image.png

    展开全文
  • 微信公众号开发基本流程

    万次阅读 多人点赞 2019-04-26 09:40:21
    过年前后做了个微信公众号项目,已经过去一段时间了,抽空回忆总结下基本流程吧,不然很快估计自己就忘了。。 微信公众平台官网:https://mp.weixin.qq.com 文章目录一、注册公众号二、了解公众号管理页面三、必备...

    背景:
    过年前后做了个微信公众号项目,已经过去一段时间了,抽空回忆总结下基本流程吧,不然很快估计自己就忘了。。

    微信公众平台官网:https://mp.weixin.qq.com



    一、注册公众号

    在这里插入图片描述
    首先注册时可以看到公众号有三种类型,个人用户大多数选择订阅号,而企业用户一般选择服务号和企业号

    我们平常大多数关注的都是订阅号,他们统一都放置在微信应用的订阅号消息列表中,没有微信支付等高级功能,只是用于发布文章等基础功能。
    在这里插入图片描述

    服务号企业号都在会话列表,和我们的微信好友是同级别的位置,具备微信支付等高级功能,一般是某个企业品牌的对外操作窗口,如海底捞火锅、顺丰速运等。
    在这里插入图片描述

    我们前期开发测试只需要注册个人订阅号即可,真正开发使用的是开发者工具里的测试号,具体下面会说。

    真正生产的话,使用的都是经过微信认证的订阅号、服务号、企业号。


    二、了解公众号管理页面

    我们在微信公众平台扫码登录后可以发现管理页面左侧菜单栏有丰富的功能:
    在这里插入图片描述
    大概可以分为这几大模块:
    首页功能小程序管理推广统计设置开发

    作为开发人员,首先应该关注的是设置、开发模块;而作为产品运营人员,关注的是功能、管理、推广模块;作为数据分析人员,关注的是统计模块。

    首先我们不妨各个功能模块都点击看一看,大概了解下我们能做些什么。可以确认的是,这个微信公众平台当然不只是给开发人员使用的,它提供了很多非技术人员可在UI界面上交互操作的功能模块。

    如配置消息回复、自定义菜单、发布文章等:
    在这里插入图片描述
    这个时候我们可能会想:这些功能好像非技术人员都能随意操作,那么还需要我们技术人员去开发吗?

    答案是: 如果只是日常简单的推送文章,就像我们关注的大多数公众号一样,那确实不需要技术人员去开发;但是,如果你想将你们的网站嵌入进去公众号菜单里(这里指的是把前端项目的首页链接配置在自定义菜单),并且实现微信端的独立登录认证、获取微信用户信息、微信支付等高级功能,或者觉得UI交互的配置方式无法满足你的需求,你需要更加自由、随心所欲的操作,那么我们就必须启用开发者模式了,通过技术人员的手段去灵活控制公众号。

    这里有一点需要注意,如果我们决定技术人员开发公众号,必须启用服务器配置,而这将导致UI界面设置的自动回复和自定义菜单失效!

    我们在 开发 - 基本配置 - 服务器配置 中点击启用
    在这里插入图片描述
    在这里插入图片描述
    我们团队就遇到过这种情况:两个项目组共用一个公众号,结果一个启用了服务器配置,使另一个项目组手动配置的菜单失效了。所以要注意这点!

    至于服务器配置中的选项代表什么意思、如何填写,我们下面再讲。


    三、必备开发者工具的使用

    在这里插入图片描述
    我们进入 开发 - 开发者工具, 可以发现微信提供了六种开发者工具,其中前四种属于开发必备:开发者文档在线接口调试工具web开发者工具公众平台测试账号

    1.开发者文档

    在这里插入图片描述
    这个不用说!在我们开发中属于最最最基础和重要的东西了,我们要想熟练开发公众号,首先必须熟读开发者文档!有些功能的开发甚至非要反复研读、咬文嚼字一番不可。PS:该文档吐槽的地方也不少,有些地方的确讲的不够明确!

    2.在线接口调试工具

    在这里插入图片描述
    这个工具也算比较实用,包含大多数接口的在线调试,我们可以直接在上面输入参数,获取微信服务端的返回结果。

    3.web开发者工具

    在这里插入图片描述
    这个工具是一款桌面应用,需要下载,它通过模拟微信客户端的UI使得开发者可以使用这个工具方便地在PC或者Mac上进行开发和调试工作,一般是前端使用该工具进行页面、接口调试。

    4.公众平台测试账号

    在这里插入图片描述
    这个测试号工具对我们的重要性可以说是仅次于开发者文档。我们可以创建测试号无需申请、认证真实的公众帐号、可在测试帐号中体验并测试微信公众平台所有高级接口。并且所有的配置都可在一个页面上编辑,使开发测试变得极其便利。


    四、细读开发者文档

    文档地址:https://mp.weixin.qq.com/wiki

    需要注意的是,细读开发者文档不是让你所有模块都去阅读,而是重点的重复细读,非重点的选择性阅读。
    在这里插入图片描述
    其中前两个模块:开始前必读开始开发,属于重点关注对象,也是整个微信开发的基石所在,需要多读几遍。其次是微信网页开发模块微信网页授权,比较难理解,需要特别注意。其他的模块则根据你们的项目功能需求,有选择性的阅读即可。

    这里我就不多罗嗦了,大家看文档去吧!下面我会描述一些重点内容的实际操作情况以及代码,请确保你已经浏览过文档


    五、开发流程重点解析


    1.开发环境准备

    这里所谓的开发环境准备主要指的是我们项目服务端和微信服务端的网络通讯环境准备。

    我们平常开发可能只需要IP端口就能通讯,顶多配置下白名单放行,但微信公众号开发我们需要通过域名通讯(微信会访问我们配置的域名地址:服务器基本配置中的URL,下面会介绍),也就是我们各自开发环境需要拥有独立的域名,微信就能通过这个域名请求到我们的本地开发服务,各自进行开发测试。

    而我们一般都是内网开发,整个内网只有一个对外域名,所以这时就需要 内网穿透 ,为我们每个开发人员配置各自开发机器的域名。

    那如何进行内网穿透呢?你首先可以找下你们的网管,看他能不能帮你解决,如果不能,那就安装内网穿透工具,我们自己动手!

    我选择的内网穿透工具是natapp,这个有免费版、收费版,免费版的域名会随机变化,而收费版可以拥有固定域名,建议选择收费版,9元每月并不贵;大家可以对照natapp的文档安装使用,并不难。
    在这里插入图片描述
    这样我们本地开发环境就拥有自己的域名啦!然后就可以在测试号管理页面配置本地访问地址URL了。

    2.服务器基本配置

    无论是在真实公众号开发 - 基本配置 - 服务器配置,还是在 测试号管理 中,我们都可以看到这几个基本参数:
    开发者ID(AppID)、开发者密码(AppSecret)、服务器地址(URL)、令牌(Token)

    AppID 是公众号唯一开发识别码,配合开发者密码可调用公众号的接口能力,大多数微信接口都需要附带该参数。

    AppSecret 是校验公众号开发者身份的密码,具有极高的安全性。切记勿把密码直接交给第三方开发者或直接存储在代码中。如需第三方代开发公众号,请使用授权方式接入。其中获取accessToken就需要同时传入AppID和AppSecret获取。

    URL 是开发者用来接收微信消息和事件的接口URL,也就是我们服务后端的入口地址,需要注意的是该地址必须以域名形式填写,且必须以http 或 https 开头,分别支持80端口和443端口。如:http://yuanj.natapp1.cc/wechat。

    Token 可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性),也就是我们项目和微信服务端进行通信时,必须保证公众平台配置的Token和我们后台代码配置的Token保持一致,这样微信就能验证我们身份。

    注:EncodingAESKey 参数由开发者手动填写或随机生成,将用作消息体加解密密钥,我们前期可以采用明文模式进行开发测试,暂时先不用关注。
    在这里插入图片描述
    我们点击提交时,微信会以GET请求的方式访问我们配置的URL地址,并附加几个参数进行验证,所以你需要在该地址对应的项目后端接口里对这几个参数进行加工处理返回微信需要的结果,这样就可以验证成功,使微信服务端认可你配置的URL和Token参数,后续就能互相通信了!
    在这里插入图片描述
    具体情况可以阅读微信文档 - 开始前必读 - 接入指南

    这里附上该接口的Java代码:

    /**
     * 微信对接验证接口
     * */
    @RestController
    @RequestMapping(value = "/wechat")
    public class ValidateController {
        @Autowired
        WechatConfig wechatConfig;
    
       @RequestMapping(value = "", method = RequestMethod.GET)
       public void validate(HttpServletRequest req, HttpServletResponse resp) {
            System.out.println("-----开始校验签名-----");
    
            // 接收微信服务器发送请求时传递过来的参数
            String signature = req.getParameter("signature");
            String timestamp = req.getParameter("timestamp");
            String nonce = req.getParameter("nonce"); //随机数
            String echostr = req.getParameter("echostr");//随机字符串
    
            // 将token、timestamp、nonce三个参数进行字典序排序并拼接为一个字符串
            String TOKEN = wechatConfig.getToken();
            String sortStr = sort(TOKEN,timestamp,nonce);
            
            // 字符串进行shal加密
            String mySignature = WechatUtils.shal(sortStr);
            
            // 校验微信服务器传递过来的签名 和  加密后的字符串是否一致, 若一致则签名通过
            if(!"".equals(signature) && !"".equals(mySignature) && signature.equals(mySignature)){
                System.out.println("-----签名校验通过-----");
                try {
                    resp.getWriter().write(echostr);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }else {
                System.out.println("-----校验签名失败-----");
            }
        }
        
       /**
         * 参数排序
         * @param token
         * @param timestamp
         * @param nonce
         * @return
         */
        public static String sort(String token, String timestamp, String nonce) {
            String[] strArray = {token, timestamp, nonce};
            Arrays.sort(strArray);
            StringBuilder sb = new StringBuilder();
            for (String str : strArray) {
                sb.append(str);
            }
            return sb.toString();
        }
    }
    
    

    3.存取access_token参数

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

    access_token这个参数非常重要,几乎贯穿整个微信公关号项目开发,我们如何在有效期内定时刷新获取呢?
    如果我们的微信公众号项目是单服务架构,可以直接作为静态变量存储在内存里;如果是多服务,可以用中间件存储Redis、数据库都可以。SpringBoot项目内部可以通过@Scheduled注解,执行定时任务,既然access_token有效期是2小时,那我们可以一小时刷新获取一次,将其存入Redis,覆盖之前的access_token。


    4.公众号消息管理

    在这里插入图片描述
    很多公众号都可以通过消息发送来与其进行交互,那这样的功能如何代码实现呢?

    具体我们可以在微信文档 - 消息管理 模块查阅:
    在这里插入图片描述
    在此我要提到的一点就 微信公众号的消息交互都是通过XML格式进行的!这点就很坑了。。现在我们前后端、服务端的消息传输基本都是Json格式了,也习惯了Json格式的解析处理,所以遇到XMl格式的处理又要多费些事了。

    为什么微信采用XML格式呢?我个人猜测是几年前还是XML格式的天下,当时Json还没有这么流行,腾讯毕竟是产品业务驱动的,当然选择当时开发人员最熟悉的XML格式了开发,后面随着微信平台的普及,用户越来越多,想重构改成Json格式估计也十分困难,所以历史就遗留下来了呗。。

    我在此推荐一个GitHub上一个微信开发 Java SDK,里面有整个微信开发平台很多功能模块造好的轮子,我们可以参考下直接使用:
    https://github.com/Wechat-Group/WxJava
    在这里插入图片描述
    比如现在对于XMl消息解析这个需求,上面就提供了完整详尽的代码。


    5.获取openid以及网页授权(重难点)

    注意,这是公众号开发的重难点之一,请把技术文档中的微信网页授权模块多读两遍,然后带着疑问来看我的解析。

    (1)先明确为什么需要网页授权?我们的目的是什么?

    答:用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。也就是通过这种授权机制,我们能获取微信用户信息,比如:头像、昵称、地区、个性签名等。

    (2)既然目的是获取用户基本信息,微信不是提供了专门的接口吗?非要网页授权?

    答:在文档的 用户管理 - 获取用户基本信息(UnionID机制) 模块可以看到的确有获取用户基本信息接口:
    在这里插入图片描述
    可以看到,这个接口只需要提供openid或者unionid,即可直接获取用户基本信息。那么问题来了,openid(unionid)又是如何获取呢?

    微信平台提供了两种方式获取用户的openid

    第一种方式:

    用户与公众号产生消息交互时,会以POST请求的方式向我们配置的服务器URL地址发送XML格式的消息,并附带该用户对应公众号的openid!关于什么是消息交互我们可以查看文档中的消息管理模块,比如我们在公众号输入栏中发送文字图片语音等属于普通消息交互,我们关注、取关、点击自定义菜单等属于事件消息交互,每当前端用户进行这个操作时,微信服务端都会向我们项目后台发送POST请求给我们传达信息:
    在这里插入图片描述
    可以看到,这个推送数据包中就包含了用户的消息交互类型、时间以及我们需要的openid!也就是说,无论用户在公众号里干了啥操作,我们都能知道他这个操作干了啥,以及他是谁(openid),这时就能调用 用户管理 - 获取用户基本信息(UnionID机制) 接口获取用户基本信息了。

    别高兴太早,这种通过消息交互获取用户信息的方式,用户占主动地位,我们项目后端服务被动接受,那么如果我有个基本需求:我想在自定义菜单 - 对应我们网站的前端页面上展示微信用户基本信息,能做到吗?你如何把后台接收到的消息和前端用户关联绑定?
    可见,这种被动的方式并不能实现该功能,我们需要主动出击,在前端就能获取到当前操作用户的openid!

    第二种方式:

    这种方式就是通过网页授权机制主动出击!详情见下文。

    (3)网页授权有哪几种机制?分别是怎样实现?应用于什么场景?

    答:主要有两种机制,对应两种scope:

    snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)。

    snsapi_userinfo为scope发起的网页授权,是用来获取用户基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。

    光看这两句解释你可能有一堆疑问,我们逐一分析:

    两种机制的前面授权步骤相同,大概如下:

    我们先要按照文档要求构造一个链接https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
    其中重点参数是redirect_uri,这个参数填的既可以是前端项目url,也可以是后端接口url,然后点击这个链接后,微信服务端经过重定向到我们填写的redirect_uri,会在此redirect_uri后拼接上一个code参数!然后前端或者后端通过code参数就可以调微信接口https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code获取openid等信息了:
    在这里插入图片描述
    这里讲下 snsapi_basesnsapi_userinfo不同点

    首先snsapi_base静默授权,什么意思呢?就是用户没有感知;与之对应的就是非静默授权snsapi_userinfo了,这个scope公众号会弹出一个小窗口需要用户手动点击授权,类似这种:
    在这里插入图片描述
    那么这两种scope授权的优劣势在哪呢?

    snsapi_base 的优势在于用户无感知,体验好,方便快捷;劣势在于获取openid后只能通过用户管理 - 获取用户基本信息(UnionID机制) 接口获取用户基本信息,而这种方式需要确保用户已经关注,不然是没有相关信息的!
    snsapi_userinfo 的优势在于无需用户关注公众号,只要用户点击了授权确认,即可通过access_token和openid调用专门的拉去用户信息接口获取信息,比较暴力。。;劣势在于需要用户手动授权,可能影响用户体验
    在这里插入图片描述

    在此说下,我们项目是通过snsapi_base静默授权的,其中redirect_uri配置的是前端项目首页地址(前后端分离),并将构造的这个链接封装起来,直接配置在自定义菜单里,那么用户点击菜单,就直接重定向到前端项目,然后前端获取code参数调用后端获取openid接口,将获取的openid缓存到客户端,以便后面使用。

    (4)想要进行网页授权,我们需要在公众平台配置什么吗?

    答:需要!
    如果是测试号,需要在 测试号管理 - 体验接口权限表 - 网页服务 - 网页帐号 点击 修改
    在这里插入图片描述
    在这里插入图片描述
    在这里配置的是回调页面redirect_uri的域名

    如果是正式号(需要微信认证),需要在 开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息 的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;

    而且正式号其他配置的地方也和测试号不一样,比如多了IP白名单、域名根路径下的txt验证文件,这个稍微摸索下应该没啥问题的。


    over 暂时就回忆这么多了。。。可能有遗漏大家可以提出哈 ~ 下一篇博客写几个开发时的小问题补充下吧

    展开全文
  • 微信公众号开发——项目搭建

    万次阅读 2018-09-11 15:23:25
    前往微信公众平台(https://mp.weixin.qq.com/)获取开发权限和开发账号,公众号分为好几种,小程序,订阅号,服务号,企业号,个人只能用订阅号,权限比较少(api接口权限); 二 想要自定义模块功能或者是回复...
  • 微信公众号开发技术要点

    万次阅读 多人点赞 2019-03-08 09:29:26
    微信公众号开发技术要点 微信公众号开发技术要点 微信公众号及其接口功能介绍 基本概念 公众号开发者模式 代码验证及图示 Open ID与Union ID 基本概念 使用说明 Access_token 基本介绍 注意事项 获取流程 ...
  • 微信公众号开发小记(一)开篇

    万次阅读 2018-08-10 12:33:33
    接下来的时间里,我会以连载的形式分享微信公众号开发的相关文章,其中也会说一下我遇到的问题以及解决的思路,以及在开发过程中的一些感悟!因为是一个连载系列,所以会以一个初学者,也就是接触过编程但是没有接触...
  • PHP 一一 微信公众号开发(二次开发)

    万次阅读 多人点赞 2018-01-11 19:13:59
    两个星期前,学校请了传智播客的老师给我们做实训,讲了一个微信公众号开发,感觉挺有意思,在这里做一下记录,以便以后复习. 一、了解微信公众平台 1. 什么是微信公众平台? 微信公众平台是腾讯为了让用户申请和管理...
  • 微信公众号开发教程(一) 验证接入

    万次阅读 多人点赞 2018-06-22 14:22:35
    微信公众号开发教程(一)验证接入本篇文章主要介绍了微信公众号开发接入详细流程,希望对刚接触公众号开发的同学有所帮助,有兴趣的同学可多多关注叩丁狼公众号,后续会更新不同的公众号小案例。公众号的分类我们平常...
  • 微信公众平台开发入门

    万人学习 2019-12-30 15:18:17
    通过本课程的学习,学员能够入门微信公众平台开发,能够胜任企业级的订阅号、服务号、企业号的应用开发工作。 通过本课程的学习,学员能够对微信公众平台有一个清晰的、系统性的认识。例如,公众号是什么,它有...
  • 如何在本地进行微信公众号开发和调试
  • 微信页面不能使用session,不知道如何才能在页面中使用session,请大神不吝赐教!!!!
  • 微信开发中,一个页面如何跳转到公众号首页? 相当于在页面实现一个退出登陆的效果,跳转到刚进入公众号首页的样子
  • 微信公众号开发(三)前端界面

    万次阅读 热门讨论 2020-06-26 18:20:09
    由于该公众号主要的用途是用于公司售后服务,经沟通确定相关需求. 第一: 了解相关的UI框架,最终选定了Jquery-WeUI框架. 第二: 制作简单案例,在移动端进行测试,要达到移动端的自适应. 目前完成三个界面如下: ...
  • 微信公众h5页面如何在web端调试

    千次阅读 2018-03-06 16:51:24
    由于微信公众页面在手机上不好调试。所以可以选择使用微信开发者工具登录微信公众号-》开发者工具-》绑定微信账号-》下载工具-》安装-》微信扫码-》工具内输入公众号网页地址...
  • 基于python的微信公众号开发教程

    千人学习 2019-10-08 17:15:40
    基于python的微信公众号开发教程
  • 在工作中经常遇到公众号开发,其中公众号的缓存问题是比较恶心人的。 解决办法: 找到 文件传输助手,发送 debugtbs.qq.com ,打开链接,清楚TBS内核,success ...
  • 微信公众号使用教程

    万人学习 2018-11-27 16:29:08
    微信公众号使用,是一套基础课程,为后面的《PHP微信公众号开发》做铺垫。微信几乎是智能手机上必装的软件,很多的商家在微信上开通公众号,想要把公众号的价值发挥大,必须学会公众号的使用,开发和营销。这一套...
  • 微信公众号开发测试帐号

    万次阅读 2018-01-10 10:33:01
    扫描关注后登录 ...填写JS接口安全域名 ,设置JS接口安全域后,通过关注该测试号,开发者即可在该域名下调用微信开放的JS接口,请阅读微信JSSDK开发文档。 注意:不知道啥原因,用自己的帐号申请测试
  • 下载微信开发者工具 https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 下载好后界面如下: 二.在命令行运行项目,npm run dev 三.在上张图片的红框内输入项目URL地址,如:...
1 2 3 4 5 ... 20
收藏数 124,933
精华内容 49,973
关键字:

微信公众号开发