小伙伴们,大家好,关于微信系列的文章好久没有更新了,偶尔看到有小伙伴在文末评论说文章太浅显了,想让我写点有进阶性的东西,其实一开始写微信相关文章的目的是帮助更多零基础的微信开发者快速了解、接入、熟悉到微信公众号开发,快速融入到这个环境中,以及学习如何使用当下比较流行的WxJava
这一款SDK
框架开发我们自己的微信公众号后台,实现一些常用的: 文本消息回复、图片消息回复、自定义菜单、菜单点击事件、以及模板消息推送、自定义带参数二维码流量分销等功能,因此本篇文章将以在接入开发者后,如何使用Java
语言回复微信公众号号上的文本消息,与粉丝进行互动。
如果你使用的是SpringBOot
框架,如果不熟悉或者还没有整合WxJava
环境的小伙伴,可以参考我之前写过的文章:
SpringBoot 系列教程(六十五):Spring Boot整合WxJava开发微信公众号
如果你使用的是Spring+SpringMVC+Mybatis 传统框架,不熟悉或者还没有整合WxJava
环境的小伙伴,可以参考文章:
Java开发微信公众号之整合weixin-java-tools框架开发微信公众号
WxPortalController
这个类,从命名就可以看出,这是一个门户接口,其作用类似于大门一样,在WxPortalController
中,主要有两个核心方法,第一个方法是get
,用来接入开发者;第二个方法是post
,用来处理与微信交互的消息处理和响应。
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 "非法请求";
}
post
处理微信交互消息post
方法的主要功能是当你在微信公众号对话栏里输入:文本、图片、语音、视频、点击菜单等操作时,该一操作将会被封装为一个xml
数据体,记住啊,微信开发使用的是xml
格式传输的,非JSON
格式;该xml
数据体被微信服务器从我们接入开发者的URL
上推送到我们应用程序的后台,这时候这一类请求都是Post
类型。传递的Post
请求在我们应用程序后台被接收了之后,首先做参数的签名校验,目的也是防止非法请求;SHA
散列对请求合法性签名校验,相对来说还是挺安全的哦,所以密文就显得没必要了。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;
}
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;
}
}
新建一个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();
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
数据传输格式。
上述例子中,我只是将响应的内容写死了在程序中,这只适合自己研究用用了,如果要动态的回复消息,比如关键字回复,就可以使用数据库预先存储好一些需要处理的关键字消息,然后将每次请求发送的会话内容与数据库的关键字表中的数据做比对,筛选出匹配的关键字对应的内容回复,这样就动态了。
这一类消息都属于多媒体消息,只有文本消息比较特殊,属于文本类,除开文本消息外,其他多媒体消息的回复,都需要预先通过上传多媒体文件到微信公众平台,也就是调用素材管理相关的接口,上传素材,素材上传成功后会返回一个叫做MediaId
的字段,这个字段最好自己通过存储方式记录下来,然后在回复图片、或者图文等多媒体文件信息时,通过传递MediaId
即可从微信公众平台找到对应的多媒体素材文件,响应给微信会话者。
详细参考: https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html#1
例如回复图片消息的xml格式:
回复图片消息
<xml>
<ToUserName>ToUserName>
<FromUserName>FromUserName>
<CreateTime>12345678CreateTime>
<MsgType>MsgType>
<Image>
<MediaId>MediaId>
Image>
xml>
源码: https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-wechat
至此,使用SpringBoot+WxJava
开发微信公众号的文本消息回复功能就讲解到这里了,相信有不少小伙伴对于第一次接触WxJava
这个SDK
时由无从下手到能够很快进入开发状态了吧,如果小伙伴们有其他需要更新的文章请评论里留言,后期安排上,如果对你有帮助,麻烦点个赞支持,谢谢。