参考:
https://github.com/Wechat-Group/WxJava/tree/release
https://blog.csdn.net/weixin_44795440/article/details/106097502
调试参考:
https://developers.weixin.qq.com/apiExplorer?type=messagePush
例如构造的结果:
body xml:
<xml><ToUserName><![CDATA[1111gh_923f702d75ab]]></ToUserName>
<FromUserName><![CDATA[oawho5ql_1mQFXNniGNbaNBCeWgc]]></FromUserName>
<CreateTime>1719891424</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[龙族]]></Content>
<MsgId>24622924390466889</MsgId>
</xml>
需要服务器回复的结果:
<xml>
<ToUserName>
<![CDATA[oawho5ql_1mQFXNniGNbaNBCeWgc]]>
</ToUserName>
<FromUserName>
<![CDATA[1111gh_923f702d75ab]]>
</FromUserName>
<CreateTime>1719895753</CreateTime>
<MsgType>
<![CDATA[text]]>
</MsgType>
<Content>
<![CDATA[龙族(2022) 评分:6.3题材:动画
龙族之龙王(2030) 评分:暂无评分题材:剧情
钢铁镇:龙族之战(2030) 评分:暂无评分题材:爱情
驱魔龙族马小玲(2025) 评分:暂无评分题材:剧情|动作|爱情
龙族 第二季(2024) 评分:暂无评分题材:
龙族:救援骑士:赫斯佳乐佳节(2020) 评分:暂无评分题材:动画|家庭
龙族的后裔(2019) 评分:暂无评分题材:剧情
龙族:救援骑士 第一季(2019) 评分:暂无评分题材:动画
龙族少年(2018) 评分:暂无评分题材:动作
龙族(2006) 评分:暂无评分题材:奇幻
独龙族(1960) 评分:暂无评分题材:纪录片]]>
</Content>
</xml>
注意toUser和fromUser要调转过来。
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime 排重。
如果开发者希望增强安全性,可以在开发者中心处开启消息加密,这样,用户发给公众号的消息以及公众号被动回复用户消息都会继续加密,详见被动回复消息加解密说明。
假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。详见下面说明:
1、直接回复success(推荐方式) 2、直接回复空串(指字节长度为0的空字符串,而不是XML结构体中content字段的内容为空)
一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:
1、开发者在5秒内未回复任何内容 2、开发者回复了异常数据,比如JSON数据等
另外,请注意,回复图片(不支持gif动图)等多媒体消息时需要预先通过素材管理接口上传临时素材到微信服务器,可以使用素材管理中的临时素材,也可以使用永久素材。
各消息类型需要的XML数据包结构如下:
回复文本消息
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>
参数 | 是否必须 | 描述 |
---|---|---|
ToUserName | 是 | 接收方账号(收到的OpenID) |
FromUserName | 是 | 开发者微信号 |
CreateTime | 是 | 消息创建时间 (整型) |
MsgType | 是 | 消息类型,文本为text |
Content | 是 | 回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示) |
推送代码示例
pom.xml配置
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-cp</artifactId>
<version>4.6.0</version>
</dependency>
package com.ruoyi.moviemonitor.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.moviemonitor.domain.DoubanInfo;
import com.ruoyi.moviemonitor.service.IDoubanInfoService;
import com.ruoyi.moviemonitor.spider.SpiderExecution;
import com.ruoyi.moviemonitor.spider.impl.douban.DoubanBasicSpider;
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.message.WxMpXmlOutTextMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@RestController
@Anonymous
@RequestMapping("/weixin")
public class WeixinController {
@Autowired
private WxMpService wxService;
@Autowired
private IDoubanInfoService doubanInfoService;
Logger logger = LoggerFactory.getLogger(WeixinController.class);
// @GetMapping("/auth")
// public String test() throws WxErrorException {
// // this.mpService.getWxMpConfigStorage().getAppId();
// return this.wxService.getAccessToken();
// }
@ResponseBody
@GetMapping(value = "/auth",produces = "text/plain;charset=utf-8")
public String authGet(
@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) {
this.logger.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,
timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
if (this.wxService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法请求";
}
@ResponseBody
@PostMapping(value = "/auth")
public void answer(HttpServletRequest request,@RequestBody String requestBody,
HttpServletResponse response) throws IOException, InterruptedException {
// String encType = request.getParameter("encrypt_type");
// String msgSignature = request.getParameter("msg_signature");
// String openid = request.getParameter("openid");
checkSignature(request);
WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(requestBody);
String fromUser = wxMpXmlMessage.getFromUser();
String toUser = wxMpXmlMessage.getToUser();
String content = wxMpXmlMessage.getContent();
String reply = "";
var doubanInfoList = searchDouban(content);
if (doubanInfoList.size() == 0) {
reply = "未找到相关电影信息";
}else {
reply = doubanInfoList.stream().map(DoubanInfo::constructWeixinMsg).reduce((a, b) -> a + "\n" + b).get();
}
logger.info("接收到来自微信服务器的消息:from: [{}],to: [{}],msg: [{}]",fromUser, toUser, content);
WxMpXmlOutTextMessage build = WxMpXmlOutMessage.TEXT()
.content(reply)
.fromUser(toUser)
.toUser(fromUser)
.build();
response.setContentType("text/xml;charset=utf-8");
response.setStatus(200);
try(PrintWriter out = response.getWriter()){
out.write(build.toXml());
}
}
private void checkSignature(HttpServletRequest request) {
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String signature = request.getParameter("signature");
if (!wxService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}
}
public List<DoubanInfo> searchDouban(String keyword) throws InterruptedException {
var doubanInfoList = this.doubanInfoService.searchDoubanInfoList(keyword);
if(doubanInfoList.size() > 0) {
return doubanInfoList;
}
Thread getDoubanInfoThread = new Thread(() ->{
List<DoubanInfo> updateList = new SpiderExecution(new DoubanBasicSpider(keyword)).search().getDoubanInfoList();
// 插入或更新基本信息
doubanInfoService.upsertDoubanInfoList(updateList);
});
getDoubanInfoThread.start();
getDoubanInfoThread.join();
return doubanInfoService.searchDoubanInfoList(keyword);
}
}