須要準備的東西:java
編輯模式和開發模式的關係:
web
編輯模式和開發模式是互斥的關係,也就是說,當咱們使用開發模式時,編輯模式下的操做就會失效。反之,使用編輯模式時,開發模式下的操做就會失效,因此只能使用其中一個模式進行公衆號的開發。算法
開發模式下,公衆號數據的交互流程:
spring
注:圖中的微信公衆號服務器,就是咱們開發者所要開發的部分服務器
微信公衆平臺相關技術文檔地址以下:微信
咱們根據 「接入指南」 中的說明來完成公衆平臺的接入,可是咱們跳過文檔中的第一步,先來完成第二步的操做,即驗證消息的確來自微信服務器。由於提交服務器配置信息時微信會對配置的URL發起調用,驗證該服務器是否正常可用,因此咱們得先把第二步完成,才能去完成第一步。既然是開發就得建工程了,因此在IDEA中建立一個SpringBoot工程,工程結構以下:
app
先說明一點:當咱們提交服務器配置信息後,微信服務器將發送GET請求到填寫的服務器地址URL上,GET請求攜帶參數分別爲signature、timestamp、nonce、echostr。開發者經過檢驗signature對請求進行校驗,若確認這次GET請求來自微信服務器,則原樣返回echostr參數內容,表示接入生效,成爲開發者成功,不然接入失敗。加密/校驗流程以下:微信公衆平臺
1)將token、timestamp、nonce三個參數進行字典序排序
2)將三個參數字符串拼接成一個字符串進行SHA1加密
3)開發者得到加密後的字符串可與signature對比,標識該請求來源於微信dom
能夠看到,第二步中,咱們須要將三個參數字符串拼接成一個字符串進行SHA1加密,這就涉及到SHA1加密算法。那麼就須要一個專門的工具類來完成SHA1加密,因此須要在util包中,新建一個 SHA1Util 類,用於進行SHA1加密,代碼以下:ide
package org.zero01.weixin.mqdemo.util; import java.security.MessageDigest; /** * @program: mq-demo * @description: SHA1加密 * @author: 01 * @create: 2018-06-23 18:06 **/ public final class SHA1Util { private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; /** * Takes the raw bytes from the digest and formats them correct. * * @param bytes the raw bytes from the digest. * @return the formatted bytes. */ private static String getFormattedText(byte[] bytes) { int len = bytes.length; StringBuilder buf = new StringBuilder(len * 2); // 把密文轉換成十六進制的字符串形式 for (byte aByte : bytes) { buf.append(HEX_DIGITS[(aByte >> 4) & 0x0f]); buf.append(HEX_DIGITS[aByte & 0x0f]); } return buf.toString(); } public static String encode(String str) { if (str == null) { return null; } try { MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); messageDigest.update(str.getBytes()); return getFormattedText(messageDigest.digest()); } catch (Exception e) { throw new RuntimeException(e); } } }
在util包中,再新建一個 WechatMqCheckedUtil 工具類,用於校驗微信發起調用時所傳遞的參數,代碼以下:
package org.zero01.weixin.mqdemo.util; import java.util.Arrays; /** * @program: mq-demo * @description: 校驗微信發起調用時所傳遞的參數 * @author: 01 * @create: 2018-06-23 17:57 **/ public class WechatMqCheckedUtil { // 在公衆平臺上配置的自定義token private static final String token = "zeroJun"; /** * 校驗微信加密簽名 * * @param signature 微信加密簽名 * @param timestamp 時間戳 * @param nonce 隨機字符串 * @return */ public static boolean checkedSignature(String signature, String timestamp, String nonce) { // 1.加入token進行排序 String[] paramArr = new String[]{token, timestamp, nonce}; Arrays.sort(paramArr); // 2.拼接成字符串,進行sha1加密 StringBuilder content = new StringBuilder(); for (String aParamArr : paramArr) { content.append(aParamArr); } String temp = SHA1Util.encode(content.toString()); // 3.與signature參數進行對比,並返回對比結果 return temp.equals(signature); } }
在controller包中,新建一個 WeChatMqController 控制器類,提供給微信調用的接口,代碼以下:
package org.zero01.weixin.mqdemo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.zero01.weixin.mqdemo.util.WechatMqCheckedUtil; /** * @program: mq-demo * @description: 接入微信公衆平臺 * @author: 01 * @create: 2018-06-23 17:51 **/ @RestController @RequestMapping("/wechat/mq") public class WeChatMqController { /** * 驗證消息的確來自微信服務器 * * @param signature 微信加密簽名 * @param timestamp 時間戳 * @param nonce 隨機數 * @param echostr 隨機字符串 * @return */ @GetMapping("/common") public String token(@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) { // 驗證成功則返回echostr if (WechatMqCheckedUtil.checkedSignature(signature, timestamp, nonce)) { System.out.println(echostr); return echostr; } return null; } }
完成代碼的編寫,並運行了工程及natapp客戶端後,就能夠到公衆平臺上填寫服務器的配置信息了。進入「基本配置」 的頁面,點擊 「修改配置」 ,以下:
填寫好基本的配置:
提交配置:
提交成功後,啓用服務器配置:
到此爲止,咱們的開發者模式就接入完成了。此時,在編輯模式的界面中,能夠看到編輯模式下的操做都已失效:
消息管理相關的文檔:
咱們先來完成文本消息的接收及回覆,因爲微信傳遞的數據是xml格式的,因此咱們須要添加一些用於解析xml的包,在pom.xml中添加以下依賴:
<dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream --> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.3.1</version> </dependency>
在工程中,新建一個vo包,在該包下新建一個 AllMessage 類,用於封裝全部普通消息的字段。關於不一樣類型的普通消息所包含的具體字段及描述信息,請參考:接收普通消息。代碼以下:
package org.zero01.weixin.mqdemo.vo; import lombok.Data; /** * @program: mq-demo * @description: 全部類型的消息封裝對象 * @author: 01 * @create: 2018-06-23 21:16 **/ @Data // lombok註解 public class AllMessage { /** * 屬性名首字母大寫的緣由是由於返回的xml中標籤的名稱是須要大寫的,不然微信解析不了 */ private String ToUserName; // 接收方帳號 private String FromUserName; // 發送方帳號 private long CreateTime; // 消息建立時間 (整型) private String MsgType; // 消息類型 private String PicUrl; // 消息內容 private String Content; // 消息內容 private String MediaId; // 消息媒體id,能夠調用多媒體文件下載接口拉取數據。 private String Format; // 語音格式,如amr,speex等 private String Recognition; // 語音識別結果,UTF8編碼 private String MsgId; // 消息id,64位整型 private String ThumbMediaId; // 視頻消息縮略圖的媒體id,能夠調用多媒體文件下載接口拉取數據。 private String Location_X; // 地理位置維度 private String Location_Y; // 地理位置經度 private String Scale; // 地圖縮放大小 private String Label; // 地理位置信息 private String Title; // 消息標題 private String Description; // 消息描述 private String Url; // 消息連接 private String Event; // 事件類型 private String EventKey; // 事件KEY值 private String Ticket; // 二維碼的ticket private String MenuId; // 指菜單ID,若是是個性化菜單,則能夠經過這個字段,知道是哪一個規則的菜單被點擊了 private ScanCodeInfo ScanCodeInfo; // 掃描信息 public static class ScanCodeInfo { private String ScanType; // 掃描類型,通常是qrcode private String ScanResult; // 掃描結果,即二維碼對應的字符串信息 } }
新建一個common包,並在該包中,新建一個 MessageTypeEnum 枚舉類,用於存放普通消息的類型。代碼以下:
package org.zero01.weixin.mqdemo.common; import lombok.AllArgsConstructor; import lombok.Getter; /** * @program: mq-demo * @description: 普通消息類型 * @author: 01 * @create: 2018-06-24 14:00 **/ @Getter @AllArgsConstructor public enum MessageTypeEnum { MSG_TEXT("text"), // 文本消息類型 MSG_IMAGE("image"), // 圖片消息類型 MSG_VOICE("voice"), // 語音消息類型 MSG_VIDEO("video"), // 視頻消息類型 MSG_SHORTVIDEO("shortvideo"), // 小視頻消息類型 MSG_LOCATION("location"), // 地理位置消息類型 MSG_LINK("link"), // 連接消息類型 MSG_EVENT("event"), // 事件消息類型 ; private String msgType; }
事件消息類型中包含訂閱/取消訂閱兩種事件類型,因此咱們也須要增長一個枚舉來存放這兩種事件類型。代碼以下:
package org.zero01.weixin.mqdemo.common; import lombok.AllArgsConstructor; import lombok.Getter; /** * @program: mq-demo * @description: 事件推送類型 * @author: 01 * @create: 2018-06-24 14:09 **/ @Getter @AllArgsConstructor public enum EventType { EVENT_SUBSCRIBE("subscribe"), // 訂閱事件類型 EVENT_UNSUBSCRIBE("unsubscribe"), // 取消訂閱事件類型 ; private String eventType; }
咱們但願有一個專門的地方,來配置咱們的自動回覆內容,因此再次新建一個枚舉類,用於存放自動回覆的內容。代碼以下:
package org.zero01.weixin.mqdemo.common; import lombok.AllArgsConstructor; import lombok.Getter; /** * @program: mq-demo * @description: 回覆的內容 * @author: 01 * @create: 2018-06-24 14:09 **/ @AllArgsConstructor @Getter public enum ContentEnum { CONTENT_SUBSCRIBE("你好,歡迎關注zero菌~"), CONTENT_NONSUPPORT("暫不支持文本之外的消息回覆!"), CONTENT_PREFIX("你發送的消息是:"), ; private String content; }
在util包下,新建一個 MessageUtil 工具類,用於轉換消息數據類型,代碼以下:
package org.zero01.weixin.mqdemo.util; import com.thoughtworks.xstream.XStream; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.zero01.weixin.mqdemo.vo.AllMessage; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @program: mq-demo * @description: 轉換消息數據類型的工具類 * @author: 01 * @create: 2018-06-23 21:04 **/ public class MessageUtil { private final static String XML = "xml"; /** * xml轉換爲map集合 * * @param request * @return * @throws IOException * @throws DocumentException */ public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException { Map<String, String> map = new HashMap<>(); SAXReader reader = new SAXReader(); InputStream inputStream = request.getInputStream(); Document document = reader.read(inputStream); Element root = document.getRootElement(); List<Element> elementList = root.elements(); for (Element element : elementList) { map.put(element.getName(), element.getText()); } inputStream.close(); return map; } /** * 將 AllMessage 消息對象,轉換爲xml * * @param allMessage * @return */ public static String allMessageToXml(AllMessage allMessage) { XStream xStream = new XStream(); xStream.alias(XML, allMessage.getClass()); return xStream.toXML(allMessage); } /** * 將 AllMessage 消息對象,轉換爲xml,並指定content的內容 * * @param allMessage * @return */ public static String allMessageToXml(AllMessage allMessage, String content) { allMessage.setContent(content); return allMessageToXml(allMessage); } /** * 將xml轉換爲 AllMessage消息對象 * * @param xmlStr * @return */ public static AllMessage xmlToAllMessage(String xmlStr) { XStream xStream = new XStream(); AllMessage allMessage = new AllMessage(); xStream.aliasType(XML, allMessage.getClass()); allMessage = (AllMessage) xStream.fromXML(xmlStr); return allMessage; } /** * 將xml轉換爲 AllMessage 消息對象,並指定content的內容 * * @param xmlStr * @param content * @return */ public static AllMessage xmlToAllMessage(String xmlStr, String content) { AllMessage allMessage = xmlToAllMessage(xmlStr); allMessage.setContent(content); return allMessage; } /** * 設置並獲取文本消息類型的 AllMessage 對象 * @param fromUserName * @param toUserName * @param content * @return */ public static AllMessage setGetTextMsg(String fromUserName, String toUserName, String content) { AllMessage allMessage = new AllMessage(); allMessage.setFromUserName(toUserName); allMessage.setToUserName(fromUserName); allMessage.setMsgType(MessageTypeEnum.MSG_TEXT.getMsgType()); allMessage.setCreateTime(new Date().getTime()); allMessage.setContent(content); return allMessage; } /** * 自動回覆 * @param allMessage * @param content * @return */ public static String autoReply(AllMessage allMessage,String content) { allMessage = setGetTextMsg(allMessage.getFromUserName(), allMessage.getToUserName(), content); return allMessageToXml(allMessage); } }
最後在 WeChatMqController 控制器類中,新增接收微信公衆號消息的接口。注意,接口映射的uri也是/wechat/mq/common,但請求方式是post。代碼以下:
/** * 接收微信公衆號消息的接口 * * @param xmlStr * @return */ @PostMapping("/common") public String text(@RequestBody String xmlStr) { // 將xml格式的數據,轉換爲 AllMessage 對象 AllMessage allMessage = MessageUtil.xmlToAllMessage(xmlStr); // 是不是文本消息類型 if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_TEXT.getMsgType())) { // 自動回覆用戶所發送的文本消息 return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_PREFIX.getContent() + allMessage.getContent()); } // 是不是事件推送類型 else if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_EVENT.getMsgType())) { // 是否爲訂閱事件,即公衆號被關注時所觸發的事件 if (EventType.EVENT_SUBSCRIBE.getEventType().equals(allMessage.getEvent())) { // 自動回覆歡迎語 return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_SUBSCRIBE.getContent()); } } else { // 暫不支持文本之外的消息回覆 return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent()); } return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent()); }
編寫完以上代碼後,運行SpringBoot工程以及natapp客戶端,接着向公衆號發送各類類型的普通消息,自動回覆結果以下:
如上圖,能夠看到,當公衆號被關注時,回覆了歡迎語。併成功接收了全部類型的普通消息,進行了相應的自動回覆。到此爲止,咱們就完成了公衆號開發模式的接入。