记一次微信公众号开发
前言:
最近尝试写一个微信公众号开发的项目,遇到很多坑(那个编码真是难受),也学到了一些,就记录一下,我是看百度看别人写的去做后来行不通,就在同事推荐下去看了罗老师视频,附上视频链接:https://www.bilibili.com/video/av35042298?
相应的包、文件、源码,老师的笔记里有,附上笔记链接:https://github.com/Michaelliangxu/book/tree/notebook
我就下了这几个包:https://files.cnblogs.com/files/yuanmaolin/lib%E5%8C%85.zip
目前只做了一部分:接口验证、回复到微信(文本信息、图文信息、图灵机器人)、自定义菜单,但相应的像音乐、视频等信息的基础代码有写,基本依样画葫芦。
想起来这个没有服务器的话,可以用内网穿透来测试。。。
代码:
WeixinController类:我做了一些修改,老师是用servlet,我没搞好,就用ssm框架了,get/post也对应改了(之前一直编码不行,怎么设置编码都没用,回复到微信上的都是问号,也不知道怎么回事,就改成现在这个样子了,一些处理方法我直接扔这个类里了)
package com.fh.controller.weixin; import java.io.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.fh.entity.weixinobject.*; import com.fh.util.url.UrlUtil; import com.thoughtworks.xstream.XStream; import net.sf.json.JSONObject; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.fh.controller.base.BaseController; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * 微信公众平台开发 */ @Controller @RequestMapping(value = "/weixin") public class WeixinController extends BaseController { private static final String TOKEN = "lymm"; //图灵机器人APPkey(这个是老师的,现在好像是申请不了图灵机器人的接口了,以后还能不能用我就不知道了) private static final String APPKEY="1fec136dbd19f44743803f89bd55ca62"; //微信公众号(前三个对应自己在微信公众平台申请的测试号里和开发文档可以看到,对应修改为自己的) private static final String GET_TOKEN_URL="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"; private static final String APPID="wx6a6f8b95fb2e1d0b"; private static final String APPSECRET="5af3f07d2f2f3ff954c31dfe9fdb45a5"; private static AccessToken at; /** * 接口验证(GET) * @param request * @param response * @throws IOException */ @ResponseBody @RequestMapping(value = "/index", method = RequestMethod.GET, produces = "application/json;charset=UTF-8") public void getWXSendMsg(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"); //校验证签名 if(check(timestamp,nonce,signature)) { System.out.println("接入成功"); PrintWriter out = response.getWriter(); //原样返回echostr参数 out.print(echostr); out.flush(); out.close(); }else { System.out.println("接入失败"); } } /** * 处理微信服务器发过来的各种消息,包括:文本、图片、地理位置、音乐等等(POST) * * @param request * @param response * @throws Exception */ @ResponseBody @RequestMapping(value = "/index", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") public void sendMsg(HttpServletRequest request, HttpServletResponse response) throws Exception { response.setCharacterEncoding("UTF-8"); //处理消息和事件推送 Map<String, String> requestMap = parseRequest(request.getInputStream()); System.out.println(requestMap); //准备回复的数据包 String respXml = getResponseinfo(requestMap); System.out.println(respXml); PrintWriter out = response.getWriter(); out.print(respXml); out.flush(); out.close(); } /** * 验证签名 * @param timestamp * @param nonce * @param signature * @return */ public static boolean check(String timestamp, String nonce, String signature) { //1)将token、timestamp、nonce三个参数进行字典序排序 String[] strs = new String[] {TOKEN,timestamp,nonce}; Arrays.sort(strs); //2)将三个参数字符串拼接成一个字符串进行sha1加密 String str = strs[0]+strs[1]+strs[2]; String mysig = sha1(str); //3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 return mysig.equalsIgnoreCase(signature); } /** * 进行sha1加密 */ private static String sha1(String src) { try { //获取一个加密对象 MessageDigest md = MessageDigest.getInstance("sha1"); //加密 byte[] digest = md.digest(src.getBytes()); char[] chars= {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; StringBuilder sb = new StringBuilder(); //处理加密结果 for(byte b:digest) { sb.append(chars[(b>>4)&15]); sb.append(chars[b&15]); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } /** * 处理所有事件和消息的回复 * @param requesstMap * @return 返回的是xml数据包 */ private static String getResponseinfo(Map<String, String> requesstMap) { BaseMessage msg = null; String msgType = requesstMap.get("MsgType"); switch (msgType){ case "text": msg = dealTextMessage(requesstMap); break; case "image": break; case "voice": break; case "video": break; case "music": break; case "news": break; } //把消息对象处理为xml数据包 if(msg!=null){ return beanToXml(msg); } return null; } /** * 把消息对象处理为xml数据包 * @param msg * @return */ private static String beanToXml(BaseMessage msg) { XStream stream = new XStream(); //设置需要处理XStreamAlias("xml")注释的类(写了这个,才会去读取这些类的注解) stream.processAnnotations(TextMessage.class); stream.processAnnotations(ImageMessage.class); stream.processAnnotations(MusicMessage.class); stream.processAnnotations(NewsMessage.class); stream.processAnnotations(VideoMessage.class); stream.processAnnotations(VoiceMessage.class); //消息对象处理为xml数据包 String xml = stream.toXML(msg); return xml; } /** * 处理文本信息 * @param requesstMap * @return */ private static BaseMessage dealTextMessage(Map<String, String> requesstMap) { //用户发来的内容 String msg = requesstMap.get("Content"); if(msg.equals("图文")){ List<Article> articles = new ArrayList<>(); articles.add(new Article("这是图文消息的标题", "这是图文消息的详细介绍", "http://mmbiz.qpic.cn/mmbiz_jpg/dtRJz5K066YczqeHmWFZSPINM5evWoEvW21VZcLzAtkCjGQunCicDubN3v9JCgaibKaK0qGrZp3nXKMYgLQq3M6g/0", "http://www.baidu.com")); NewsMessage nm = new NewsMessage(requesstMap, articles); return nm; } //调用方法返回聊天的内容(图灵机器人) String resp = chat(msg); TextMessage tm = new TextMessage(requesstMap,resp); return tm; } /** * 调用图灵机器人聊天 * @param msg * @return */ private static String chat(String msg) { String result =null; String url ="http://op.juhe.cn/robot/index";//请求接口地址 Map params = new HashMap();//请求参数 params.put("key",APPKEY);//您申请到的本接口专用的APPKEY params.put("info",msg);//要发送给机器人的内容,不要超过30个字符 params.put("dtype","");//返回的数据的格式,json或xml,默认为json params.put("loc","");//地点,如北京中关村 params.put("lon","");//经度,东经116.234632(小数点后保留6位),需要写为116234632 params.put("lat","");//纬度,北纬40.234632(小数点后保留6位),需要写为40234632 params.put("userid","");//1~32位,此userid针对您自己的每一个用户,用于上下文的关联 try { result = UrlUtil.net(url, params, "GET"); //解析json JSONObject jsonObject = JSONObject.fromObject(result); //取出error_code int code = jsonObject.getInt("error_code"); if(code!=0) { return null; } //取出返回的消息的内容 String resp = jsonObject.getJSONObject("result").getString("text"); return resp; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 处理微信服务器发过来的xml数据包 */ public static Map<String, String> parseRequest(InputStream is) { Map<String, String> map = new HashMap<>(); SAXReader reader = new SAXReader(); try { //读取输入流,获取文档对象 Document document = reader.read(is); //根据文档对象获取根节点 Element root = document.getRootElement(); //获取根节点的所有子节点 List<Element> elements = root.elements(); for (Element e : elements) { map.put(e.getName(), e.getStringValue()); } } catch (DocumentException e) { e.printStackTrace(); } return map; } /** * 获取token(自定义菜单等微信提供的接口需要用到) */ private static void getToken() { String url = GET_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET); String tokenStr = UrlUtil.get(url); JSONObject jsonObject = JSONObject.fromObject(tokenStr); String token = jsonObject.getString("access_token"); String expireIn = jsonObject.getString("expires_in"); //创建token对象,并存起来。 at = new AccessToken(token, expireIn); } /** * 向外暴露的获取token的方法(当token对象不存在或过期时,才创建token对象;否则直接返回一个token) */ public static String getAccessToken() { if(at==null||at.isExpired()) { getToken(); } return at.getAccessToken(); } }
Util工具类
package com.fh.util.url; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.Map; public class UrlUtil { public static final String DEF_CHATSET = "UTF-8"; public static final int DEF_CONN_TIMEOUT = 30000; public static final int DEF_READ_TIMEOUT = 30000; public static String userAgent = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36"; // 配置您申请的KEY public static final String APPKEY = "*************************"; /** * 向指定的地址发送一个post请求,带着data数据 * * @param url * @param data */ public static String post(String url, String data) { try { URL urlObj = new URL(url); URLConnection connection = urlObj.openConnection(); // 要发送数据出去,必须要设置为可发送数据状态 connection.setDoOutput(true); // 获取输出流 OutputStream os = connection.getOutputStream(); // 写出数据 os.write(data.getBytes()); os.close(); // 获取输入流 InputStream is = connection.getInputStream(); byte[] b = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = is.read(b)) != -1) { sb.append(new String(b, 0, len)); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 向指定的地址发送get请求 * * @param url * @return by 罗召勇 Q群193557337 */ public static String get(String url) { try { URL urlObj = new URL(url); // 开连接 URLConnection connection = urlObj.openConnection(); InputStream is = connection.getInputStream(); byte[] b = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = is.read(b)) != -1) { sb.append(new String(b, 0, len)); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } /** * * @param strUrl * 请求地址 * @param params * 请求参数 * @param method * 请求方法 * @return 网络请求字符串 * @throws Exception */ public static String net(String strUrl, Map params, String method) throws Exception { HttpURLConnection conn = null; BufferedReader reader = null; String rs = null; try { StringBuffer sb = new StringBuffer(); if (method == null || method.equals("GET")) { strUrl = strUrl + "?" + urlencode(params); } URL url = new URL(strUrl); conn = (HttpURLConnection) url.openConnection(); if (method == null || method.equals("GET")) { conn.setRequestMethod("GET"); } else { conn.setRequestMethod("POST"); conn.setDoOutput(true); } conn.setRequestProperty("User-agent", userAgent); conn.setUseCaches(false); conn.setConnectTimeout(DEF_CONN_TIMEOUT); conn.setReadTimeout(DEF_READ_TIMEOUT); conn.setInstanceFollowRedirects(false); conn.connect(); if (params != null && method.equals("POST")) { try { DataOutputStream out = new DataOutputStream(conn.getOutputStream()); out.writeBytes(urlencode(params)); } catch (Exception e) { // TODO: handle exception } } InputStream is = conn.getInputStream(); reader = new BufferedReader(new InputStreamReader(is, DEF_CHATSET)); String strRead = null; while ((strRead = reader.readLine()) != null) { sb.append(strRead); } rs = sb.toString(); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { reader.close(); } if (conn != null) { conn.disconnect(); } } return rs; } // 将map型转为请求参数型 public static String urlencode(Map<String, Object> data) { StringBuilder sb = new StringBuilder(); for (Map.Entry i : data.entrySet()) { try { sb.append(i.getKey()).append("=").append(URLEncoder.encode(i.getValue() + "", "UTF-8")).append("&"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return sb.toString(); } }
2019-8-30 补充~~ AccessToken实体类
package 包名; public class AccessToken { private String accessToken; private Long expireTime; public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public Long getExpireTime() { return expireTime; } public void setExpireTime(Long expireTime) { this.expireTime = expireTime; } public AccessToken(String accessToken, Integer expireIn) { this.accessToken = accessToken; expireTime = System.currentTimeMillis() + expireIn * 1000; } /** * 判断token是否过期 * * @return */ public boolean isExpired() { return System.currentTimeMillis() > expireTime; } }
自定义菜单创建
参看:https://www.cnblogs.com/syd08/p/5374513.html