# love_wechat **Repository Path**: declan1/love_wechat ## Basic Information - **Project Name**: love_wechat - **Description**: 女朋友专属微信公众号,定时发送早安、喝水、星座运势消息。 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 9 - **Forks**: 0 - **Created**: 2022-11-30 - **Last Updated**: 2024-10-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 微信公众号开发定义 接下来就带大家开发一个公众号定时发送的接口。话不多说,上车! # 接入微信公众号 申请测试公众号:[测试公众号](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index) 公众号开发文档:[开发文档](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Operation_Specifications.html) ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%201.png) ## 配置接口信息 由于是微信服务器调用,所以他需要我们的 URL 能够被公网访问。这里我们可以使用免费的内网穿透工具 Nateapp 配置流程:[配置](https://blog.csdn.net/qq_41311259/article/details/119767186) ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%202.png) 我们可以把这个分配给我们的 URL 填入其中,Token 值可以随便写一个。这时你点击确认按钮,他会弹出“配置失败”的提示信息。 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%203.png) ### 请求校验 配置失败是因为微信服务器会对这个网址发送一次 **GET** 请求进行相关信息的校验。我们可以启动 SpringBoot 项目,开放一个接口来响应微信的请求,那么这个请求要做什么事情呢?就是通过检验signature对请求进行校验。 加密/校验流程如下: 1. 将token、timestamp、nonce三个参数进行字典序排序 2. 将三个参数字符串拼接成一个字符串进行sha1加密 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 4. 如果验证成功原样返回echostr 校验代码: ```java package com.dai.wechat.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.io.PrintWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @RestController @RequestMapping public class Test extends HttpServlet { private static final long serialVersionUID = 1L; public static final String TOKEN = "123456"; // 消息验证请求方式为 GET @GetMapping("/weixin") protected void doget(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); PrintWriter out = response.getWriter(); // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 if (checkSignature(signature, timestamp, nonce).equals(signature)) { out.write(echostr); System.out.println("微信服务验证成功!"+echostr); }else { out.print(echostr); System.out.println("微信服务验证失败!"+echostr); } out.flush(); out.close(); } public static String checkSignature(String signature ,String timestamp, String nonce) { String[] src = {TOKEN,timestamp,nonce}; List list =Arrays.asList(src); Collections.sort(list); StringBuilder sb = new StringBuilder(); for (int i = 0; i < list.size(); i++) { sb.append(list.get(i)); } return SHA1(sb.toString()); } /** * * @param decript * @return */ public static String SHA1(String decript) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update(decript.getBytes()); byte messageDigest[] = digest.digest(); // Create Hex String 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 ""; } } ``` **注意:**这里是开放了接口 "/weixin",所以填入到微信微信接口配置信息中的 URL应该就是 “内网穿透地址/weixin” # 定时发送模板消息 ## 概述 通过该功能可以每天定时把定制消息发送给那个你在乎的人,这也是程序员独有的浪漫吧~~ 比如早安短信: ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%204.png) ## 流程 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%205.png) ## 做法 ### 获取基本配置信息 发送消息涉及到发送者以及接收者。发送者显然是我们的公众号,接收者就是其他用户。 标识公众号的信息就是_appId_和_appsecret;_而具体表示接收者的就是_openId;_ _appId_和_appsecret_在公众测试平台可以看到 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%206.png) _openId_需要用户关注公众号,这时微信服务器就可以返回给我们需要的_openId_ ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%207.png) ### 设置模板 在微信测试开发平台上点击新增测试模板,即可填入相应的内容。 注意: - 模板内容可设置参数(模板标题不可),供接口调用时使用,参数需以{{开头,以.DATA}}结尾,这个相当于占位符,我们可以在程序中对其进行填充 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%208.png) ### 填充模板信息 这里主要填充的信息包括:今日日期、今日天气、温度、纪念时间、甜言蜜语、每日金句 #### 天气信息 这里可以调用外部的 api 进行获取; [天气预报 api](https://www.juhe.cn/docs/api/id/73) 只需要在这个网站上进行实名认证即可进行调用。 我们封装一个请求工具方法,只需要把网址以及对应的参数放入就可以得到返回的响应字符串。然后再用JSON对象对这个响应字符串进行解析,即可获得相应的数据。 ```java public static String request(String httpUrl, Map map) { BufferedReader reader = null; String result = null; StringBuffer sbf = new StringBuffer(); String urlencode = urlencode(map); httpUrl += urlencode; try { // 根据Url创建出对象 URL url = new URL(httpUrl); // 获取连接 HttpURLConnection connection = (HttpURLConnection) url .openConnection(); connection.setRequestMethod("GET"); // 发送请求,获取响应输入流 InputStream is = connection.getInputStream(); // 通过包装类获取流中信息 reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); String strRead = null; while ((strRead = reader.readLine()) != null) { sbf.append(strRead); sbf.append("\r\n"); } reader.close(); result = sbf.toString(); } catch (Exception e) { e.printStackTrace(); } return result; } public static String urlencode(Map 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(); } } String result = sb.toString(); result = result.substring(0, result.lastIndexOf("&")); return result; } ``` 具体的获取天气信息的代码: ```java // 这里规定集合中第一个元素存放今日日期,第二个元素存放最低温度,第三个元素存放最高温度,第四个元素存放天气状况 public static List getWeather(){ Map map = new HashMap<>(); map.put("key",API_KEY); map.put("city","南昌"); String request = request(API_URL, map); System.out.println(request); // 获得json字符串 List list = new ArrayList<>(); JSONObject jsonObject = JSONObject.parseObject(request); if ("查询成功!".equals(jsonObject.getString("reason"))) { JSONObject obj = jsonObject.getJSONObject("result").getJSONArray("future").getJSONObject(0); // 获取今日日期 String date = obj.getString("date"); list.add(date); // 获取今日最高温度以及最低温度 String temperature = obj.getString("temperature"); String[] split = temperature.substring(0, temperature.length() - 1).split("/",3); for (String s : split) { list.add(s); } // 获取今日天气状况 String weather = obj.getString("weather"); list.add(weather); }else{ list.add("数据错误"); } return list; } ``` #### 彩虹屁 api 地址:[彩虹屁](https://www.tianapi.com/apiview/181) 注意:只需要进行注册就可以免费调用。 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%209.png) ```java public static String getRainbow(){ //java环境中文传值时,需特别注意字符编码问题 String jsonResult = request(rainbowUrl, key); // 获得json字符串 JSONObject jsonObject = JSONObject.parseObject(jsonResult); if (jsonObject.getIntValue("code") == 200) { String str = jsonObject.getJSONArray("newslist").getJSONObject(0).getString("content"); return str; } // 返回默认语句 return rainbowWord; } ``` #### 每日英语 api 地址:[每日英语](https://www.tianapi.com/apiview/174) ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%2010.png) ```java public static String getEnglish(){ String jsonResult = request(englishUrl, key); // 获得json字符串 JSONObject jsonObject = JSONObject.parseObject(jsonResult); StringBuilder stringBuilder = new StringBuilder(); if (jsonObject.getIntValue("code") == 200) { String str = jsonObject.getJSONArray("newslist").getJSONObject(0).getString("content"); String note = jsonObject.getJSONArray("newslist").getJSONObject(0).getString("note"); stringBuilder.append(str); stringBuilder.append("\n"); stringBuilder.append(note); return stringBuilder.toString(); } // 返回默认语句 return english; } ``` #### 填充信息并推送 ```java public static Result push(String openId){ //1,配置 WxMpInMemoryConfigStorage wxStorage = new WxMpInMemoryConfigStorage(); wxStorage.setAppId(appId); wxStorage.setSecret(secret); WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setWxMpConfigStorage(wxStorage); //2,推送消息 WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() .toUser(openId) .templateId(templateId) .build(); // 配置信息 List weather1 = ApiUtil.getWeather("南昌"); String week = ApiUtil.dateToWeek(weather1.get(0)); templateMessage.addData(new WxMpTemplateData("data",weather1.get(0) + " "+ week ,"#00BFFF")); templateMessage.addData(new WxMpTemplateData("weather", weather1.get(3) ,"#173177")); templateMessage.addData(new WxMpTemplateData("lowTemperature", weather1.get(1) ,"#FF6347" )); templateMessage.addData(new WxMpTemplateData("highTemperature",weather1.get(2) ,"#FF69B4")); templateMessage.addData(new WxMpTemplateData("memorial",Memorial.getLoveDay() +"","#FF1493")); templateMessage.addData(new WxMpTemplateData("meet",Memorial.getMeetDay() +"","#FFA500")); templateMessage.addData(new WxMpTemplateData("saying","\n" + ApiUtil.getRainbow() + "\n\n","#C71585")); templateMessage.addData(new WxMpTemplateData("english", ApiUtil.getEnglish() + "","#FF6347")); String beizhu = ""; if(Memorial.getLoveDay() % 365 == 0){ beizhu = "今天是恋爱纪念日!"; } templateMessage.addData(new WxMpTemplateData("note",beizhu,"#FF0000")); try { // 公众号推送消息 String s = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); //System.out.println("s = "+ s); return Result.suceessWithoutData(s); } catch (Exception e) { System.out.println("推送失败:" + e.getMessage()); e.printStackTrace(); return Result.failed("500","推送失败:" + e.getMessage()); } } ``` ### 定时任务 为了能让我们的程序定时运行,设置了定时任务。 ```java @Configuration // 开启定时任务 @EnableScheduling public class SaticScheduleTask { private final String openId = "xxxxx"; // 选择接收消息用户的openId private final String teng_openId = "xxxxxxx"; // 添加定时任务(每天早上七点半) @Scheduled(cron = "0 30 7 * * ?") private void configureTasks() { Result push = Pusher.push(teng_openId); if("200".equals(push.getCode())){ System.out.println("发送成功!"); }else{ System.out.println(push.getMessage()); } } } ``` ### 部署服务器 当然做到这里还是远远不够的,现在只能满足在程序不间断的情况下,定时发送消息。为了不占有本地的 cpu 资源我们可以把其放到云服务器上运行。 - 使用 maven 的 package 命令把程序打包成 jar 包 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%2011.png) - 把打成的 jar 包放到云服务器上,这里需要你的云服务器上有 jdk 的环境,没有环境的同学可以去 csdn 上搜寻相关博客进行安装 - 进入相应的文件夹,使用命令 _nohup java -jar wechat-0.0.1-SNAPSHOT.jar >a.log 2>& 1 &_ 让程序不间断运行 - 使用命令查看程序运行状态 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%2012.png) 防火墙开放端口 _firewall-cmd --add-port=443/tcp --permanent_ 重启防火墙 _systemctl restart firewalld_ # 被动回复消息 要回复用户的消息,我们先得接受用户消息。 > 微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。详见下面说明: > 1、直接回复success(推荐方式) 2、直接回复空串(指字节长度为0的空字符串,而不是 XML 结构体中 content 字段的内容为空) 相应的文档信息:[开发文档](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html) ## 接收消息 还记得我们配置的接口 URL 吗,当微信公众号接收到用户发送的消息时,它会把消息封装成 XML 的格式并用 POST 的方式访问我么的接口。我们可以通过解析 XML 数据包获得消息,并根据内容进行响应。 - 通过 POST 请求方式接收请求 ```java // 消息接收请求方式为 POST @PostMapping(value = "/weixin") public String doReply(HttpServletRequest request, HttpServletResponse response){ try { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); Map stringStringMap = ParseXmlUtils.parseXml(request); //System.out.println(stringStringMap); //System.out.println("接收到的消息为:" + stringStringMap.get("Content")); String s = ReplyModel.responseReply(stringStringMap); return s; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } ``` - 微信公众号发送的原始数据 ```xml 1348831860 1234567890123456 xxxx xxxx ``` - 解析 XMl 数据包 这时我们的所有消息都封装在了map中 ```java public static Map parseXml(HttpServletRequest request) throws IOException, DocumentException { Map map = new HashMap<>(); // 利用dom4j解析XMl文件 // 获取到文档对象 InputStream inputStream = request.getInputStream(); SAXReader saxReader = new SAXReader(); Document document = saxReader.read(inputStream); // 开始解析 Element rootElement = document.getRootElement(); List elements = rootElement.elements(); // 遍历所有节点 for (Element element : elements) { String name = element.getName(); String stringValue = element.getText(); map.put(name, stringValue); } // 关闭流 inputStream.close(); return map; } ``` - 接收成功 ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%2013.png) ## 回复消息 由于消息分为文本、图片、语音、视频、音乐、图文,这里我们演示接收文本消息并且回复文本消息。 回复消息的格式也是 XML ,我们通过注解把一个 JAVA 对象转为 XML 格式的对象。 ```xml 12345678 ``` ```java @XStreamAlias("xml") public class BaseMessage { private String ToUserName; private String FromUserName; private String CreateTime; private String MsgType; public BaseMessage(Map requestMap){ this.ToUserName = requestMap.get("FromUserName"); this.FromUserName = requestMap.get("ToUserName"); this.CreateTime = System.currentTimeMillis()/1000+""; } 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; } } @XStreamAlias("xml") public class TextMessage extends BaseMessage { // 消息内容 private String Content; public TextMessage(Map requestMap,String Content) { super(requestMap); this.setMsgType("text"); this.Content = Content; } public String getContent() { return Content; } public void setContent(String Content) { Content = Content; } @Override public String toString() { return "TextMessage[Content="+Content+",getToUserName()="+getToUserName()+",getFromUserName()="+getFromUserName()+",getCreateTime()="+getCreateTime()+"]"; } } ``` **新建一个工具类将我们要回复给用户的消息转换为xml数据包。** ```java package com.dai.wechat.util; import com.dai.wechat.pojo.BaseMessage; import com.dai.wechat.pojo.ReplyVO; import com.thoughtworks.xstream.XStream; import java.util.Map; /** * 解析微信公众号服务器返回的xml文件 * 转换成xml文件 * 需要打导入dom4j的jar包 * */ public class ReplyModel { public static String responseReply( Map parseXml){ BaseMessage msg=null; //获取到消息的类型,如果还有其他类型的消息直接加case就可以 String type = parseXml.get("MsgType"); switch (type){ case "text": //调用处理文本消息的方法 msg=dealTextMessage(parseXml); break; } //如果该消息不为null if(msg!=null){ //调用把消息对象处理为xml数据包的方法 return beanToxml(msg); } return null; } ///把消息对象处理为xml数据包的方法 private static String beanToxml(BaseMessage msg){ XStream xStream =new XStream(); xStream.processAnnotations(ReplyVO.class); String xml = xStream.toXML(msg); return xml; } //处理文本消息 private static BaseMessage dealTextMessage(Map parseXml) { //将键值对的map直接转为对象,并为Content参数赋值 String content = parseXml.get("Content"); ReplyVO TM =new ReplyVO(parseXml,"欢迎来到专属公众号~~"); return TM; } } ``` ```java // 消息接收请求方式为 POST @PostMapping(value = "/weixin") public String doReply(HttpServletRequest request){ try { request.setCharacterEncoding("UTF-8"); // 获得用户发送的消息 Map stringStringMap = ParseXmlUtils.parseXml(request); // 获得响应消息 String s = ReplyModel.responseReply(stringStringMap); return s; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } ``` 回复消息: ![输入图片说明](image/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91/%E5%9B%BE%E7%89%87%2014.png) # 创建菜单 文档:[创建菜单](https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html) 菜单类型分为点击触发事件和点击跳转链接两种。 1. click:点击推事件用户点击 click 类型按钮后,微信服务器会通过消息接口推送消息类型为 event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的 key 值,开发者可以通过自定义的 key 值与用户进行交互; 2. view:跳转 URL 用户点击 view 类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。 ## 调用接口 调用**该接口即可创建菜单** http请求方式:POST(请使用 https 协议) [https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN](https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN) 把下列 josn 数据放入请求体中向微信服务器调用接口即可。 **click和 view 的请求示例** ```json { "button":[ { "type":"click", "name":"今日歌曲", "key":"V1001_TODAY_MUSIC" }, { "name":"菜单", "sub_button":[ { "type":"view", "name":"搜索", "url":"http://www.soso.com/" }, { "type":"miniprogram", "name":"wxa", "url":"http://mp.weixin.qq.com", "appid":"wx286b93c14bbf93aa", "pagepath":"pages/lunar/index" }, { "type":"click", "name":"赞一下我们", "key":"V1001_GOOD" }] }] } ``` 注意:调用该接口需要获取 ACCESS_TOKEN ,通过调用 [https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=xxxxx&secret=xxxxx](https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=xxxxxx&secret=xxxx) 可获取