在微信里有这样一个公众号【华为运动健康】,当点击最新排行的时候,公众号就会发送今天最新的运动步数给你。如下图:
这里有两种格式的消息
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引入:
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-mpartifactId>
<version>3.6.0version>
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
签名校验代码如下:
@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功能很多如验证消息的确来自微信服务器、获取access_token、进行相应的公众号切换…
WxMpMessageRouter:微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理
说明:
配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理
默认情况下消息只会被处理一次,除非使用 WxMpMessageRouterRule.next()
规则的结束必须用WxMpMessageRouterRule.end()或者WxMpMessageRouterRule.next(),否则不会生效
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;
}
}
需要注意的是:
(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)第一条消息,当用户订阅公众号时会调用SubscribeHandler,该handler会返回一个WxMpXmlOutMessage,这个就是默认返回值
(2)第二条消息,我们可以使用【微信客服】发送,直接使用下面的方法进行发送
wxService.getKefuService().sendKefuMessage(WxMpKefuMessage
.TEXT()
.toUser(wxMessage.getFromUser())
.content("客服回复内容")
.build());
回复小程序卡片这个功能在微信公众平台是找不到的,这个功能只能使用代码实现,这里就体现出代码的强大了。
如上图所示:当我们输入“最新直播”时,小程序会返回当前最新直播的小程序卡片,点击即可进入小程序。实现思路:
(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());