在微信里有這樣一個公眾號【華為運動健康】,當點擊最新排行的時候,公眾號就會發送今天最新的運動步數給你。如下圖:

這里有兩種格式的消息
1、有頭像框,有聊天框——普通消息
2、消息有樣式、顏色等——模板消息

本篇文章主要介紹的就是如何讓微信公眾號自動回復消息
參考文檔鏈接:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
開發之前,給大家介紹一個weixin-java-tools
簡單介紹一下:
1、微信各種平台的api它都集成了,直接調用就行,不用自己維護微信官方url、各種常量、accessToken等;
2、微信那艹蛋的xml返回數據格式可以直接使用對象進行構建,它已經做好了封裝;
3、WxMpMessageRouter非常好用,將微信的各種事件進行分發處理,示例如下:
maven引入:
<!-- weixin-java-mp SDK框架--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>3.6.0</version> </dependency>
github地址如下:https://github.com/binarywang/weixin-java-mp-demo
wiki文檔如下:https://github.com/Wechat-Group/WxJava/wiki
SpringBoot-WeChat示例參考項目:https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-wechat
一、登錄微信公眾平台,啟用服務器配置
1、登錄https://mp.weixin.qq.com,選擇公眾號進入,在設置與開發—基本配置
2、記下開發者AppId和開發者秘鑰,IP白名單待會再說
3、點擊修改配置,配置服務器地址,輸入令牌生成加解密秘鑰,選擇安全模式點擊保存
4、上面配置的服務器地址,就是微信回調我們服務器的回調地址,由於微信的數據格式是xml,所以我們需要在接口上進行一些處理
代碼如下:
@RequestMapping(value = "handleWxEvent", method = RequestMethod.POST, produces = "application/xml; charset=UTF-8") @ResponseBody public String handleWxEvent(@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){ //微信加密簽名 String signature = request.getParameter("signature"); //時間戳 String timestamp = request.getParameter("timestamp"); //隨機數 String nonce = request.getParameter("nonce"); //隨機字符串 String echostr = request.getParameter("echostr"); //接入驗證 if (checkSignature(signature, timestamp, nonce, gzhToken)) { log.info("微信公眾號校驗完成echostr:[{}]", echostr); try {
//這樣寫可以防止服務器給返回的字符串加上雙引號,導致驗證失敗 response.getWriter().print(echostr); } catch (IOException e) { log.error("輸出返回值異常", e); } return; } throw new DscException(ErrorCodeEnum.SYSTEM_EXCEPTION, "解析簽名發生異常"); } /** * 校驗簽名 * * @param signature 簽名 * @param timestamp 時間戳 * @param nonce 隨機數 * @return 布爾值 */ public static boolean checkSignature(String signature, String timestamp, String nonce, String token) { String checkText = null; if (null != signature) { //對ToKen,timestamp,nonce 按字典排序 String[] paramArr = new String[]{token, timestamp, nonce}; Arrays.sort(paramArr); //將排序后的結果拼成一個字符串 String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]) try { MessageDigest md = MessageDigest.getInstance("SHA-1"); //對接后的字符串進行sha1加密 byte[] digest = md.digest(content.toString().getBytes()); checkText = byteToStr(digest); } catch (Exception e) { log.error("解碼發生異常", e); } } //將加密后的字符串與signature進行對比 return checkText != null ? checkText.equals(signature.toUpperCase()) : false; }

二、配置公眾號信息,實例化WxMpService、WxMpMessageRouter
WxMpService功能很多如驗證消息的確來自微信服務器、獲取access_token、進行相應的公眾號切換...
WxMpMessageRouter:微信消息路由器,通過代碼化的配置,把來自微信的消息交給handler處理
說明:
1. 配置路由規則時要按照從細到粗的原則,否則可能消息可能會被提前處理
2. 默認情況下消息只會被處理一次,除非使用 WxMpMessageRouterRule.next()
3. 規則的結束必須用WxMpMessageRouterRule.end()或者WxMpMessageRouterRule.next(),否則不會生效
1、在application.properties或者application.yml配置好公眾號的相關信息
2、wxmp包如下

WxMpConfig.class
import com.google.common.collect.Maps; import me.chanjar.weixin.common.api.WxConsts; 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.WxMpConfigStorage; import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Map; 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.EVENT; import static me.chanjar.weixin.mp.constant.WxMpEventConstants.CustomerService.*; import static me.chanjar.weixin.mp.constant.WxMpEventConstants.POI_CHECK_NOTIFY;
@Configuration public class WxMpConfig { /** * 設置微信公眾號的appid */ @Value("${gzh.appId}") private String appId; /** * 設置微信公眾號的app secret */ @Value("${gzh.appSecret}") private String secret; /** * 設置微信公眾號的token */ @Value("${gzh.token}") private String token; /** * 設置微信公眾號的EncodingAESKey */ @Value("${gzh.aesKey}") private String aesKey; /** * 日志處理 */ @Autowired private LogHandler logHandler; @Autowired private NullHandler nullHandler; @Autowired private KfSessionHandler kfSessionHandler; @Autowired private StoreCheckNotifyHandler storeCheckNotifyHandler; @Autowired private LocationHandler locationHandler; @Autowired private MenuHandler menuHandler; @Autowired private MsgHandler msgHandler; @Autowired private UnsubscribeHandler unsubscribeHandler; @Autowired private SubscribeHandler subscribeHandler; @Autowired private ScanHandler scanHandler; @Autowired private TextMsgHandler textMsgHandler; @Autowired private ImgHandler imgHandler; @Bean("wxMpService") public WxMpService wxMpService() { WxMpDefaultConfigImpl wxMpDefaultConfig = new WxMpDefaultConfigImpl(); wxMpDefaultConfig.setAppId(appId); wxMpDefaultConfig.setSecret(secret); wxMpDefaultConfig.setToken(token); wxMpDefaultConfig.setAesKey(aesKey); Map<String, WxMpConfigStorage> configMap = Maps.newHashMap(); configMap.put(appId,wxMpDefaultConfig); WxMpService service = new WxMpServiceImpl(); service.setMultiConfigStorages(configMap); 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(WxConsts.EventType.CLICK).handler(this.menuHandler).end(); // 點擊菜單連接事件 newRouter.rule().async(false).msgType(EVENT).event(WxConsts.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(WxConsts.EventType.LOCATION).handler(this.locationHandler).end(); // 接收地理位置消息 newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION).handler(this.locationHandler).end(); // 掃碼事件 newRouter.rule().async(false).msgType(EVENT).event(WxConsts.EventType.SCAN).handler(this.scanHandler).end(); // 文本消息處理 newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.TEXT).handler(this.textMsgHandler).end(); // 圖片消息處理 newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.IMAGE).handler(this.imgHandler).end(); // 默認 newRouter.rule().async(false).handler(this.msgHandler).end(); return newRouter; } }
3、各個handler如下

需要注意的是:
(1)消息回復的類型有文本、圖片、圖文、語音、視頻、音樂等;
(2)圖片、視頻等消息都需要先上傳到素材庫並獲取該素材的mediaId;
(3)回復的消息還可以是小程序卡片,但需要保證當前小程序必須綁定在該公眾號下
(4)假如服務器無法保證在五秒內處理並回復,也必須回復,這樣微信服務器才不會對此作任何處理,並且不會發起重試(這種情況下,可以使用客服消息接口進行異步回復),否則,將出現嚴重的錯誤提示
三、服務器處理微信回調事件,並進行分發
import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.mp.api.WxMpMessageRouter; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.*; @Slf4j @AllArgsConstructor @RestControllerpublic class WxPortalController { private final WxMpService wxService; private final WxMpMessageRouter messageRouter; @PostMapping(value="handleWxEvent",produces = "application/xml; charset=UTF-8") public String handleWxEvent(@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 (!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; } private WxMpXmlOutMessage route(WxMpXmlMessage message) { try { return this.messageRouter.route(message); } catch (Exception e) { log.error("路由消息時出現異常!", e); } return null; } }
四、構建回復消息
1、關注公眾號回復歡迎用戶

這里我們可以看到,關注【鄉榮】公眾號后,不僅回復了信息,而且是兩條。實現思路:
(1)第一條消息,當用戶訂閱公眾號時會調用SubscribeHandler,該handler會返回一個WxMpXmlOutMessage,這個就是默認返回值
(2)第二條消息,我們可以使用【微信客服】發送,直接使用下面的方法進行發送
wxService.getKefuService().sendKefuMessage(WxMpKefuMessage .TEXT() .toUser(wxMessage.getFromUser()) .content("客服回復內容") .build());
2、回復小程序卡片消息

回復小程序卡片這個功能在微信公眾平台是找不到的,這個功能只能使用代碼實現,這里就體現出代碼的強大了💪。
如上圖所示:當我們輸入“最新直播”時,小程序會返回當前最新直播的小程序卡片,點擊即可進入小程序。實現思路:
(1)用戶輸入的是文本消息,處理應該在TextMsgHandler中
(2)默認返回的WxMpXmlOutMessage是沒有小程序卡片這個類型的,所以這里還得借助客服消息
(3)小程序卡片需要一個封面,且這個封面圖必須上傳到微信素材庫后獲取mediaId,上傳代碼如下:
//獲取素材庫相關實現 WxMpMaterialService materialService = wpService.getMaterialService(); //上傳臨時素材 WxMediaUploadResult wxMediaUploadResult = materialService.mediaUpload(WxConsts.KefuMsgType.IMAGE, ".jpg", inputStream); //獲取素材ID String mediaId = wxMediaUploadResult.getMediaId();
(4)構建小程序卡片信息
wpService.getKefuService().sendKefuMessage(WxMpKefuMessage .MINIPROGRAMPAGE() .title("小程序卡片標題") .toUser(wxMessage.getFromUser()) .thumbMediaId(mediaId) .appId("小程序ID") .pagePath("卡片要跳轉的路徑").build());
