一、前言
小夥伴們,大家好,關於微信系列的文章好久沒有更新了,偶爾看到有小夥伴在文末評論說文章太淺顯了,想讓我寫點有進階性的東西,其實一開始寫微信相關文章的目的是幫助更多零基礎的微信開發者快速瞭解、接入、熟悉到微信公衆號開發,快速融入到這個環境中,以及學習如何使用當下比較流行的WxJava
這一款SDK
框架開發我們自己的微信公衆號後臺,實現一些常用的: 文本消息回覆、圖片消息回覆、自定義菜單、菜單點擊事件、以及模板消息推送、自定義帶參數二維碼流量分銷等功能,因此本篇文章將以在接入開發者後,如何使用Java
語言回覆微信公衆號號上的文本消息,與粉絲進行互動。
如果你使用的是SpringBOot
框架,如果不熟悉或者還沒有整合WxJava
環境的小夥伴,可以參考我之前寫過的文章:
SpringBoot 系列教程(六十五):Spring Boot整合WxJava開發微信公衆號
如果你使用的是Spring+SpringMVC+Mybatis 傳統框架,不熟悉或者還沒有整合WxJava
環境的小夥伴,可以參考文章:
Java開發微信公衆號之整合weixin-java-tools框架開發微信公衆號
二、版本
- spring boot.version: v2.1.7.RELEASE
- java.version: 1.8
- weixin-java-mp.version: 3.5.0
三、淺析WxJava路由規則
WxPortalController
這個類,從命名就可以看出,這是一個門戶接口,其作用類似於大門一樣,在WxPortalController
中,主要有兩個核心方法,第一個方法是get
,用來接入開發者;第二個方法是post
,用來處理與微信交互的消息處理和響應。
1. get
處理接入開發者
get
方法的主要功能是當你登錄到微信公衆平臺,在接入開發者選項,填入消息交互的URL
地址時,這時候會觸發一個get
請求,get
請求由微信服務器發出,請求我們的微信後臺應用程序,該get
請求需要攜帶appid
、signature
、timestamp
、nonce
、echostr
這5個參數,缺一不可,目的是使用SHA
散列算法做簽名校驗,防止惡意非法請求以及防止參數被篡改,這也是考慮到安全層面,所以做了Sign
簽名校驗。
@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(@PathVariable String appid,
@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
log.info("\n接收到來自微信服務器的認證消息:[{}, {}, {}, {}]", signature,
timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("請求參數非法,請覈實!");
}
if (!this.wxService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到對應appid=[%s]的配置,請覈實!", appid));
}
if (wxService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法請求";
}
2. post
處理微信交互消息
- SHA簽名校驗:
post
方法的主要功能是當你在微信公衆號對話欄裏輸入:文本、圖片、語音、視頻、點擊菜單等操作時,該一操作將會被封裝爲一個xml
數據體,記住啊,微信開發使用的是xml
格式傳輸的,非JSON
格式;該xml
數據體被微信服務器從我們接入開發者的URL
上推送到我們應用程序的後臺,這時候這一類請求都是Post
類型。傳遞的Post
請求在我們應用程序後臺被接收了之後,首先做參數的簽名校驗,目的也是防止非法請求; - 區分明密文: 然後再是區分消息是明文傳輸還是密文傳輸,是明文還是密碼區別於你在微信公衆平臺接入開發者時是否勾選了密文傳輸。一般都是使用明文傳輸,因爲有使用
SHA
散列對請求合法性簽名校驗,相對來說還是挺安全的哦,所以密文就顯得沒必要了。 - 匹配route: 區分明文還是密文之後,會根據消息類型或者事件的類型來動態的遍歷已經裝載的
route
,匹配到對應類型的路由處理器,也就是xxxHandler
,通過路由找到消息或事件的處理器之後,剩下的事情就交給xxxHandler
來完成了,xxxHandler
中會進行一些業務邏輯處理,其中可能會涉及到數據庫交互,總之需要做的事情就在這裏面處理,最後xxxHandler
會將響應結果組裝成xml
響應給會話者,這一過程在WxJava
中被包裝在WxMpXmlOutMessage
類來完成。
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable String appid,
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
log.info("\n接收微信請求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
+ " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
if (!this.wxService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到對應appid=[%s]的配置,請覈實!", appid));
}
if (!wxService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法請求,可能屬於僞造的請求!");
}
String out = null;
if (encType == null) {
// 明文傳輸的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}
out = outMessage.toXml();
} else if ("aes".equalsIgnoreCase(encType)) {
// aes加密的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(),
timestamp, nonce, msgSignature);
log.debug("\n消息解密後內容爲:\n{} ", inMessage.toString());
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}
out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage());
}
log.debug("\n組裝回覆信息:{}", out);
return out;
}
四、自定義TestMsgHandler
1. 路由初始化
WxMpConfiguration
這個類就是用來裝載和初始化路由類的一個Bean
,具體的路由匹配規則在WxMpMessageRouter
。
package com.thinkingcao.weixin.config;
import com.thinkingcao.weixin.handler.*;
import lombok.AllArgsConstructor;
import static me.chanjar.weixin.common.api.WxConsts.EventType;
import static me.chanjar.weixin.common.api.WxConsts.EventType.SUBSCRIBE;
import static me.chanjar.weixin.common.api.WxConsts.EventType.UNSUBSCRIBE;
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType.EVENT;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import static me.chanjar.weixin.mp.constant.WxMpEventConstants.CustomerService.*;
import static me.chanjar.weixin.mp.constant.WxMpEventConstants.POI_CHECK_NOTIFY;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.stream.Collectors;
/**
* wechat mp configuration
*
* @author Binary Wang(https://github.com/binarywang)
*/
@AllArgsConstructor
@Configuration
@EnableConfigurationProperties(WxMpProperties.class)
public class WxMpConfiguration {
private final LogHandler logHandler;
private final NullHandler nullHandler;
private final KfSessionHandler kfSessionHandler;
private final StoreCheckNotifyHandler storeCheckNotifyHandler;
private final LocationHandler locationHandler;
private final MenuHandler menuHandler;
private final MsgHandler msgHandler;
private final UnsubscribeHandler unsubscribeHandler;
private final SubscribeHandler subscribeHandler;
private final ScanHandler scanHandler;
private final WxMpProperties properties;
private final TextMsgHandler textMsgHandler;
@Bean
public WxMpService wxMpService() {
// 代碼裏 getConfigs()處報錯的同學,請注意仔細閱讀項目說明,你的IDE需要引入lombok插件!!!!
final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
if (configs == null) {
throw new RuntimeException("大哥,拜託先看下項目首頁的說明(readme文件),添加下相關配置,注意別配錯了!");
}
WxMpService service = new WxMpServiceImpl();
service.setMultiConfigStorages(configs
.stream().map(a -> {
WxMpDefaultConfigImpl configStorage = new WxMpDefaultConfigImpl();
configStorage.setAppId(a.getAppId());
configStorage.setSecret(a.getSecret());
configStorage.setToken(a.getToken());
configStorage.setAesKey(a.getAesKey());
return configStorage;
}).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o)));
return service;
}
@Bean
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
// 記錄所有事件的日誌 (異步執行)
newRouter.rule().handler(this.logHandler).next();
// 接收客服會話管理事件
newRouter.rule().async(false).msgType(EVENT).event(KF_CREATE_SESSION)
.handler(this.kfSessionHandler).end();
newRouter.rule().async(false).msgType(EVENT).event(KF_CLOSE_SESSION)
.handler(this.kfSessionHandler).end();
newRouter.rule().async(false).msgType(EVENT).event(KF_SWITCH_SESSION)
.handler(this.kfSessionHandler).end();
// 門店審覈事件
newRouter.rule().async(false).msgType(EVENT).event(POI_CHECK_NOTIFY).handler(this.storeCheckNotifyHandler).end();
// 自定義菜單事件
newRouter.rule().async(false).msgType(EVENT).event(EventType.CLICK).handler(this.menuHandler).end();
// 點擊菜單連接事件
newRouter.rule().async(false).msgType(EVENT).event(EventType.VIEW).handler(this.nullHandler).end();
// 關注事件
newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
// 取消關注事件
newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.unsubscribeHandler).end();
// 上報地理位置事件
newRouter.rule().async(false).msgType(EVENT).event(EventType.LOCATION).handler(this.locationHandler).end();
// 接收地理位置消息
newRouter.rule().async(false).msgType(XmlMsgType.LOCATION).handler(this.locationHandler).end();
// 掃碼事件
newRouter.rule().async(false).msgType(EVENT).event(EventType.SCAN).handler(this.scanHandler).end();
// 文本時間處理
newRouter.rule().async(false).msgType(XmlMsgType.TEXT).handler(this.textMsgHandler).end();
// 默認
newRouter.rule().async(false).handler(this.msgHandler).end();
return newRouter;
}
}
2. 自定義TEXT類型消息處理器
新建一個TextMsgHandler
類繼承AbstractHandler
並且使用@Component
註解將其注入到Spring
容器,使用TextMsgHandler
處理文本消息的具體寫法如下:
package com.thinkingcao.weixin.handler;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @desc: 文本累心消息處理-TEXT
* @link: XmlMsgType.TEXT
* @author: cao_wencao
* @date: 2020-05-20 15:15
*/
@Component
public class TextMsgHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
//判斷傳遞過來的消息,類型是否爲TEXT
if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) {
//TODO: 如果需要做微信消息日誌存儲,可以在這裏進行日誌存儲到數據庫,這裏省略不寫。
}
// 獲取微信用戶基本信息
WxMpUser userWxInfo = wxMpService.getUserService().userInfo(wxMessage.getFromUser(), "zh_CN");
if (null != userWxInfo){
//下面兩種響應方式都可以
//return new TextBuilder().build("您的一互動,泛起了我內心的漣漪。",wxMessage,wxMpService);
return WxMpXmlOutMessage.TEXT().content("您的一互動,就激起了我內心的無限可能")
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.build();
}
return null;
}
}
添加至路由初始化類WxMpConfiguration
中,在Spring
容器初始化時裝載Bean
。
// 文本事件處理
newRouter.rule().async(false).msgType(XmlMsgType.TEXT).handler(this.textMsgHandler).end();
3. 發送文本消息
後臺處理請求響應日誌:
2020-05-20 17:20:49.644 DEBUG 21812 --- [nio-8080-exec-1] m.c.w.mp.api.impl.BaseWxMpServiceImpl :
【請求地址】: https://api.weixin.qq.com/cgi-bin/user/info?access_token=33_I35PwZO23jQw2uX2Nv2m3Xvemvujx6hV8b1Lqs8zf4MUV8ov_bY2H4spLmar59HNWFPsmjRNstvLbqdlDzgu9TBFbfT6cF67mHQjRdwPjX8j2AB9sscT0j9A_tM6gNgQgMM-qu9UYiiwer0oIMUjAIAUYG
【請求參數】:openid=oGjQdw2EyT7CBNfN84Te6IpmflCM&lang=zh_CN
【響應數據】:{"subscribe":1,"openid":"oGjQdw2EyT7CBNfN84Te6IpmflCM","nickname":"曹","sex":1,"language":"zh_CN","city":"墨爾本","province":"維多利亞","country":"澳大利亞","headimgurl":"http:\/\/thirdwx.qlogo.cn\/mmopen\/1ZMUBCDTp8ZAsxH99cX3icFXXDSstNaIR1FDpibnmfNPEn1J7Hf9yLXicSHJiciaEgtwgTXRicib9X2mua4bpeEg2sWNics6rXnIKKq7\/132","subscribe_time":1589956861,"remark":"","groupid":0,"tagid_list":[],"subscribe_scene":"ADD_SCENE_QR_CODE","qr_scene":0,"qr_scene_str":""}
2020-05-20 17:20:49.663 DEBUG 21812 --- [nio-8080-exec-1] m.c.weixin.mp.api.WxMpMessageRouter : End session access: async=false, sessionId=oGjQdw2EyT7CBNfN84Te6IpmflCM
2020-05-20 17:20:49.666 DEBUG 21812 --- [pool-1-thread-2] m.c.weixin.mp.api.WxMpMessageRouter : End session access: async=true, sessionId=oGjQdw2EyT7CBNfN84Te6IpmflCM
2020-05-20 17:20:49.700 DEBUG 21812 --- [nio-8080-exec-1] c.t.w.controller.WxPortalController :
組裝回覆信息:<xml>
<ToUserName><![CDATA[oGjQdw2EyT7CBNfN84Te6IpmflCM]]></ToUserName>
<FromUserName><![CDATA[gh_833ac613acf7]]></FromUserName>
<CreateTime>1589966449</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[您的一互動,就激起了我內心的無限可能]]></Content>
</xml>
仔細觀察組裝回覆信息的xml
結構,主要包含了ToUserName(接收者)
、FromUserName(發送者)
、CreateTime(時間)
、MsgType(消息類型)
、Content(內容)
,其中Content
被![CDATA[]
標籤包起來了,這是一個標準的xml
數據傳輸格式。
4. 動態文本消息響應
上述例子中,我只是將響應的內容寫死了在程序中,這隻適合自己研究用用了,如果要動態的回覆消息,比如關鍵字回覆,就可以使用數據庫預先存儲好一些需要處理的關鍵字消息,然後將每次請求發送的會話內容與數據庫的關鍵字表中的數據做比對,篩選出匹配的關鍵字對應的內容回覆,這樣就動態了。
5. 關於如何回覆圖片、圖文、語音、視頻、音樂消息
這一類消息都屬於多媒體消息,只有文本消息比較特殊,屬於文本類,除開文本消息外,其他多媒體消息的回覆,都需要預先通過上傳多媒體文件到微信公衆平臺,也就是調用素材管理相關的接口,上傳素材,素材上傳成功後會返回一個叫做MediaId
的字段,這個字段最好自己通過存儲方式記錄下來,然後在回覆圖片、或者圖文等多媒體文件信息時,通過傳遞MediaId
即可從微信公衆平臺找到對應的多媒體素材文件,響應給微信會話者。
詳細參考: https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html#1
例如回覆圖片消息的xml格式:
回覆圖片消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[media_id]]></MediaId>
</Image>
</xml>
五、源碼
源碼: https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-wechat
至此,使用SpringBoot+WxJava
開發微信公衆號的文本消息回覆功能就講解到這裏了,相信有不少小夥伴對於第一次接觸WxJava
這個SDK
時由無從下手到能夠很快進入開發狀態了吧,如果小夥伴們有其他需要更新的文章請評論裏留言,後期安排上,如果對你有幫助,麻煩點個贊支持,謝謝。