需要準(zhǔn)備的東西:
目前成都創(chuàng)新互聯(lián)已為成百上千的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬主機(jī)、網(wǎng)站托管運(yùn)營(yíng)、企業(yè)網(wǎng)站設(shè)計(jì)、豐順網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長(zhǎng),共同發(fā)展。
編輯模式和開(kāi)發(fā)模式的關(guān)系:
編輯模式和開(kāi)發(fā)模式是互斥的關(guān)系,也就是說(shuō),當(dāng)我們使用開(kāi)發(fā)模式時(shí),編輯模式下的操作就會(huì)失效。反之,使用編輯模式時(shí),開(kāi)發(fā)模式下的操作就會(huì)失效,所以只能使用其中一個(gè)模式進(jìn)行公眾號(hào)的開(kāi)發(fā)。
開(kāi)發(fā)模式下,公眾號(hào)數(shù)據(jù)的交互流程:
注:圖中的微信公眾號(hào)服務(wù)器,就是我們開(kāi)發(fā)者所要開(kāi)發(fā)的部分
微信公眾平臺(tái)相關(guān)技術(shù)文檔地址如下:
我們根據(jù) “接入指南” 中的說(shuō)明來(lái)完成公眾平臺(tái)的接入,但是我們跳過(guò)文檔中的第一步,先來(lái)完成第二步的操作,即驗(yàn)證消息的確來(lái)自微信服務(wù)器。因?yàn)樘峤环?wù)器配置信息時(shí)微信會(huì)對(duì)配置的URL發(fā)起調(diào)用,驗(yàn)證該服務(wù)器是否正常可用,所以我們得先把第二步完成,才能去完成第一步。既然是開(kāi)發(fā)就得建工程了,所以在IDEA中創(chuàng)建一個(gè)SpringBoot工程,工程結(jié)構(gòu)如下:
先說(shuō)明一點(diǎn):當(dāng)我們提交服務(wù)器配置信息后,微信服務(wù)器將發(fā)送GET請(qǐng)求到填寫的服務(wù)器地址URL上,GET請(qǐng)求攜帶參數(shù)分別為signature、timestamp、nonce、echostr。開(kāi)發(fā)者通過(guò)檢驗(yàn)signature對(duì)請(qǐng)求進(jìn)行校驗(yàn),若確認(rèn)此次GET請(qǐng)求來(lái)自微信服務(wù)器,則原樣返回echostr參數(shù)內(nèi)容,表示接入生效,成為開(kāi)發(fā)者成功,否則接入失敗。加密/校驗(yàn)流程如下:
1)將token、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序
2)將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行SHA1加密
3)開(kāi)發(fā)者獲得加密后的字符串可與signature對(duì)比,標(biāo)識(shí)該請(qǐng)求來(lái)源于微信
可以看到,第二步中,我們需要將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行SHA1加密,這就涉及到SHA1加密算法。那么就需要一個(gè)專門的工具類來(lái)完成SHA1加密,所以需要在util包中,新建一個(gè) SHA1Util 類,用于進(jìn)行SHA1加密,代碼如下:
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);
// 把密文轉(zhuǎn)換成十六進(jìn)制的字符串形式
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包中,再新建一個(gè) WechatMqCheckedUtil 工具類,用于校驗(yàn)微信發(fā)起調(diào)用時(shí)所傳遞的參數(shù),代碼如下:
package org.zero01.weixin.mqdemo.util;
import java.util.Arrays;
/**
* @program: mq-demo
* @description: 校驗(yàn)微信發(fā)起調(diào)用時(shí)所傳遞的參數(shù)
* @author: 01
* @create: 2018-06-23 17:57
**/
public class WechatMqCheckedUtil {
// 在公眾平臺(tái)上配置的自定義token
private static final String token = "zeroJun";
/**
* 校驗(yàn)微信加密簽名
*
* @param signature 微信加密簽名
* @param timestamp 時(shí)間戳
* @param nonce 隨機(jī)字符串
* @return
*/
public static boolean checkedSignature(String signature, String timestamp, String nonce) {
// 1.加入token進(jìn)行排序
String[] paramArr = new String[]{token, timestamp, nonce};
Arrays.sort(paramArr);
// 2.拼接成字符串,進(jìn)行sha1加密
StringBuilder content = new StringBuilder();
for (String aParamArr : paramArr) {
content.append(aParamArr);
}
String temp = SHA1Util.encode(content.toString());
// 3.與signature參數(shù)進(jìn)行對(duì)比,并返回對(duì)比結(jié)果
return temp.equals(signature);
}
}
在controller包中,新建一個(gè) WeChatMqController 控制器類,提供給微信調(diào)用的接口,代碼如下:
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: 接入微信公眾平臺(tái)
* @author: 01
* @create: 2018-06-23 17:51
**/
@RestController
@RequestMapping("/wechat/mq")
public class WeChatMqController {
/**
* 驗(yàn)證消息的確來(lái)自微信服務(wù)器
*
* @param signature 微信加密簽名
* @param timestamp 時(shí)間戳
* @param nonce 隨機(jī)數(shù)
* @param echostr 隨機(jī)字符串
* @return
*/
@GetMapping("/common")
public String token(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
// 驗(yàn)證成功則返回echostr
if (WechatMqCheckedUtil.checkedSignature(signature, timestamp, nonce)) {
System.out.println(echostr);
return echostr;
}
return null;
}
}
完成代碼的編寫,并運(yùn)行了工程及natapp客戶端后,就可以到公眾平臺(tái)上填寫服務(wù)器的配置信息了。進(jìn)入“基本配置” 的頁(yè)面,點(diǎn)擊 “修改配置” ,如下:
填寫好基本的配置:
提交配置:
提交成功后,啟用服務(wù)器配置:
到此為止,我們的開(kāi)發(fā)者模式就接入完成了。此時(shí),在編輯模式的界面中,可以看到編輯模式下的操作都已失效:
消息管理相關(guān)的文檔:
我們先來(lái)完成文本消息的接收及回復(fù),由于微信傳遞的數(shù)據(jù)是xml格式的,所以我們需要添加一些用于解析xml的包,在pom.xml中添加如下依賴:
dom4j
dom4j
1.6.1
com.thoughtworks.xstream
xstream
1.3.1
在工程中,新建一個(gè)vo包,在該包下新建一個(gè) AllMessage 類,用于封裝所有普通消息的字段。關(guān)于不同類型的普通消息所包含的具體字段及描述信息,請(qǐng)參考:接收普通消息。代碼如下:
package org.zero01.weixin.mqdemo.vo;
import lombok.Data;
/**
* @program: mq-demo
* @description: 所有類型的消息封裝對(duì)象
* @author: 01
* @create: 2018-06-23 21:16
**/
@Data // lombok注解
public class AllMessage {
/**
* 屬性名首字母大寫的原因是因?yàn)榉祷氐膞ml中標(biāo)簽的名稱是需要大寫的,否則微信解析不了
*/
private String ToUserName; // 接收方賬號(hào)
private String FromUserName; // 發(fā)送方賬號(hào)
private long CreateTime; // 消息創(chuàng)建時(shí)間 (整型)
private String MsgType; // 消息類型
private String PicUrl; // 消息內(nèi)容
private String Content; // 消息內(nèi)容
private String MediaId; // 消息媒體id,可以調(diào)用多媒體文件下載接口拉取數(shù)據(jù)。
private String Format; // 語(yǔ)音格式,如amr,speex等
private String Recognition; // 語(yǔ)音識(shí)別結(jié)果,UTF8編碼
private String MsgId; // 消息id,64位整型
private String ThumbMediaId; // 視頻消息縮略圖的媒體id,可以調(diào)用多媒體文件下載接口拉取數(shù)據(jù)。
private String Location_X; // 地理位置維度
private String Location_Y; // 地理位置經(jīng)度
private String Scale; // 地圖縮放大小
private String Label; // 地理位置信息
private String Title; // 消息標(biāo)題
private String Description; // 消息描述
private String Url; // 消息鏈接
private String Event; // 事件類型
private String EventKey; // 事件KEY值
private String Ticket; // 二維碼的ticket
private String MenuId; // 指菜單ID,如果是個(gè)性化菜單,則可以通過(guò)這個(gè)字段,知道是哪個(gè)規(guī)則的菜單被點(diǎn)擊了
private ScanCodeInfo ScanCodeInfo;
// 掃描信息
public static class ScanCodeInfo {
private String ScanType; // 掃描類型,一般是qrcode
private String ScanResult; // 掃描結(jié)果,即二維碼對(duì)應(yīng)的字符串信息
}
}
新建一個(gè)common包,并在該包中,新建一個(gè) 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"), // 語(yǔ)音消息類型
MSG_VIDEO("video"), // 視頻消息類型
MSG_SHORTVIDEO("shortvideo"), // 小視頻消息類型
MSG_LOCATION("location"), // 地理位置消息類型
MSG_LINK("link"), // 鏈接消息類型
MSG_EVENT("event"), // 事件消息類型
;
private String msgType;
}
事件消息類型中包含訂閱/取消訂閱兩種事件類型,所以我們也需要增加一個(gè)枚舉來(lái)存放這兩種事件類型。代碼如下:
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;
}
我們希望有一個(gè)專門的地方,來(lái)配置我們的自動(dòng)回復(fù)內(nèi)容,所以再次新建一個(gè)枚舉類,用于存放自動(dòng)回復(fù)的內(nèi)容。代碼如下:
package org.zero01.weixin.mqdemo.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @program: mq-demo
* @description: 回復(fù)的內(nèi)容
* @author: 01
* @create: 2018-06-24 14:09
**/
@AllArgsConstructor
@Getter
public enum ContentEnum {
CONTENT_SUBSCRIBE("你好,歡迎關(guān)注zero菌~"),
CONTENT_NONSUPPORT("暫不支持文本以外的消息回復(fù)!"),
CONTENT_PREFIX("你發(fā)送的消息是:"),
;
private String content;
}
在util包下,新建一個(gè) MessageUtil 工具類,用于轉(zhuǎn)換消息數(shù)據(jù)類型,代碼如下:
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: 轉(zhuǎn)換消息數(shù)據(jù)類型的工具類
* @author: 01
* @create: 2018-06-23 21:04
**/
public class MessageUtil {
private final static String XML = "xml";
/**
* xml轉(zhuǎn)換為map集合
*
* @param request
* @return
* @throws IOException
* @throws DocumentException
*/
public static Map xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
Map map = new HashMap<>();
SAXReader reader = new SAXReader();
InputStream inputStream = request.getInputStream();
Document document = reader.read(inputStream);
Element root = document.getRootElement();
List elementList = root.elements();
for (Element element : elementList) {
map.put(element.getName(), element.getText());
}
inputStream.close();
return map;
}
/**
* 將 AllMessage 消息對(duì)象,轉(zhuǎn)換為xml
*
* @param allMessage
* @return
*/
public static String allMessageToXml(AllMessage allMessage) {
XStream xStream = new XStream();
xStream.alias(XML, allMessage.getClass());
return xStream.toXML(allMessage);
}
/**
* 將 AllMessage 消息對(duì)象,轉(zhuǎn)換為xml,并指定content的內(nèi)容
*
* @param allMessage
* @return
*/
public static String allMessageToXml(AllMessage allMessage, String content) {
allMessage.setContent(content);
return allMessageToXml(allMessage);
}
/**
* 將xml轉(zhuǎn)換為 AllMessage消息對(duì)象
*
* @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轉(zhuǎn)換為 AllMessage 消息對(duì)象,并指定content的內(nèi)容
*
* @param xmlStr
* @param content
* @return
*/
public static AllMessage xmlToAllMessage(String xmlStr, String content) {
AllMessage allMessage = xmlToAllMessage(xmlStr);
allMessage.setContent(content);
return allMessage;
}
/**
* 設(shè)置并獲取文本消息類型的 AllMessage 對(duì)象
* @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;
}
/**
* 自動(dòng)回復(fù)
* @param allMessage
* @param content
* @return
*/
public static String autoReply(AllMessage allMessage,String content) {
allMessage = setGetTextMsg(allMessage.getFromUserName(), allMessage.getToUserName(), content);
return allMessageToXml(allMessage);
}
}
最后在 WeChatMqController 控制器類中,新增接收微信公眾號(hào)消息的接口。注意,接口映射的uri也是/wechat/mq/common,但請(qǐng)求方式是post。代碼如下:
/**
* 接收微信公眾號(hào)消息的接口
*
* @param xmlStr
* @return
*/
@PostMapping("/common")
public String text(@RequestBody String xmlStr) {
// 將xml格式的數(shù)據(jù),轉(zhuǎn)換為 AllMessage 對(duì)象
AllMessage allMessage = MessageUtil.xmlToAllMessage(xmlStr);
// 是否是文本消息類型
if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_TEXT.getMsgType())) {
// 自動(dòng)回復(fù)用戶所發(fā)送的文本消息
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_PREFIX.getContent() + allMessage.getContent());
}
// 是否是事件推送類型
else if (allMessage.getMsgType().equals(MessageTypeEnum.MSG_EVENT.getMsgType())) {
// 是否為訂閱事件,即公眾號(hào)被關(guān)注時(shí)所觸發(fā)的事件
if (EventType.EVENT_SUBSCRIBE.getEventType().equals(allMessage.getEvent())) {
// 自動(dòng)回復(fù)歡迎語(yǔ)
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_SUBSCRIBE.getContent());
}
} else {
// 暫不支持文本以外的消息回復(fù)
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
}
return MessageUtil.autoReply(allMessage, ContentEnum.CONTENT_NONSUPPORT.getContent());
}
編寫完以上代碼后,運(yùn)行SpringBoot工程以及natapp客戶端,接著向公眾號(hào)發(fā)送各種類型的普通消息,自動(dòng)回復(fù)結(jié)果如下:
如上圖,可以看到,當(dāng)公眾號(hào)被關(guān)注時(shí),回復(fù)了歡迎語(yǔ)。并成功接收了所有類型的普通消息,進(jìn)行了相應(yīng)的自動(dòng)回復(fù)。到此為止,我們就完成了公眾號(hào)開(kāi)發(fā)模式的接入。