SpringBoot 系列教程(九十六):SpringBoot+WxJava开发微信公众号之回复文本消息

一、前言

小伙伴们,大家好,关于微信系列的文章好久没有更新了,偶尔看到有小伙伴在文末评论说文章太浅显了,想让我写点有进阶性的东西,其实一开始写微信相关文章的目的是帮助更多零基础的微信开发者快速了解、接入、熟悉到微信公众号开发,快速融入到这个环境中,以及学习如何使用当下比较流行的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请求需要携带appidsignaturetimestampnonceechostr这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时由无从下手到能够很快进入开发状态了吧,如果小伙伴们有其他需要更新的文章请评论里留言,后期安排上,如果对你有帮助,麻烦点个赞支持,谢谢。

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客
应支付9.90元
点击重新获取
扫码支付

支付成功即可阅读