源码地址:
一.申请微信开发者账号
- 注册账号
- 申请测试号
- 这里接口配置信息暂时不填,后面再填
二.开发平台与Java端绑定
- 基本开发环境
- springboot 2.7.2
- mysql 8.0
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- https://hutool.cn/docs/index.html#/-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 引入微信公众号开发依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
- 配置微信公众号测试号相关参数
- app-id和secret为第一步中测试号提供的appid和appsecret
- token为接口配置信息中的Token,自定义就好
wx:
mp:
app-id: your-app-id
secret: your-appsecret
token: your-token
aes-key:
- 配置类,配置WxMpService
/**
* 常量类,读取配置文件application.properties中的配置
*/
@Component
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wx.mp.app-id}")
private String appid;
@Value("${wx.mp.secret}")
private String appsecret;
@Value("${wx.mp.token}")
private String token;
@Value("${wx.mp.aes-key}")
private String aes_key;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
public static String TOKEN;
public static String AES_KEY;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = appid;
ACCESS_KEY_SECRET = appsecret;
TOKEN = token;
AES_KEY = aes_key;
}
}
/**
* @author niumazlb
*/
@Configuration
@Data
public class WeChatMpConfig {
@Autowired
private ConstantPropertiesUtil constantPropertiesUtil;
@Bean
public WxMpService wxMpService(){
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage(){
WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);
wxMpConfigStorage.setToken(ConstantPropertiesUtil.TOKEN);
wxMpConfigStorage.setAesKey(ConstantPropertiesUtil.AES_KEY);
return wxMpConfigStorage;
}
}
- 搭建微信公众号服务
- check 接口,用于微信开发者平台检测接入我们的Java端,如果检验通过,原样返回开发者平台发过来的echostr即可。
- 接入概述 | 微信开放文档 (qq.com)
/**
* 微信公众号相关接口
*
* @author niumazlb
*/
@RestController
@RequestMapping("/")
@Slf4j
@CrossOrigin
public class WxMpController {
@Resource
private WxMpService wxMpService;
@GetMapping
public String check(String timestamp, String nonce, String signature, String echostr) {
log.info("check,timestamp:{},nonce:{},signature:{},echostr:{}", timestamp, nonce, signature, echostr);
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
log.info("check success,echostr:{}", echostr);
return echostr;
} else {
return "";
}
}
}
三.配置内网穿透
什么是内网穿透?说白了就是让其他人能够在网上访问到你电脑本地的接口。
我使用的是natapp,网上有很多教程,遇到大问题可以去搜一搜。
NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
配置完成后,可以在一些api工具中测试一下这个域名,看看请求能不能成功发送到本地
- 在微信开发者平台填入相关信息
- 能够成功配置,即表示接入成功!就可以开发后续功能了
踩坑
如果这里内网穿透你使用的是自定义域名,并且域名在除了阿里云的其他厂商备案,那么你在发请求到该域名时,会被阿里云安全拦截!导致配置失败!
- 要么在阿里云再备案一次,较麻烦
- 花点小钱在natapp买一个二级域名就可以了,推荐
四.设置公众号菜单
在公众号中我们常常能看到一些按钮,通过点击按钮能够满足不同的需求
我们也能通过代码来更加个性化的定制我们的菜单(只有认证用户可使用接口定制菜单)
编写controller
- 可以通过wxMenuButton.setSubButtons(List<WxMenuButton> subButtons);方法来设置某个菜单的子菜单
- 可以通过wxMenuButton.setUrl(String url)方法来设置点击菜单跳转的url
/**
* 设置公众号菜单
*
* @return
* @throws WxErrorException
*/
@GetMapping("/setMenu")
public String setMenu() throws WxErrorException {
log.info("setMenu");
WxMenu wxMenu = new WxMenu();
// 菜单一
WxMenuButton wxMenuButton1 = new WxMenuButton();
wxMenuButton1.setType(MenuButtonType.CLICK);
wxMenuButton1.setName("今日课程");
wxMenuButton1.setKey(WxMpConstant.CLICK_COURSE_KEY);
// 菜单二
WxMenuButton wxMenuButton2 = new WxMenuButton();
wxMenuButton2.setType(MenuButtonType.CLICK);
wxMenuButton2.setName("作业");
wxMenuButton2.setKey(WxMpConstant.CLICK_HOMEWORK_KEY);
// 设置主菜单
wxMenu.setButtons(Arrays.asList(wxMenuButton1, wxMenuButton2));
wxMpService.getMenuService().menuCreate(wxMenu);
return "ok";
}
五.消息的接收与处理
流程图
- 可以看出我们Java后端需要编写
- 一个接口用来接收微信发给我们的消息,校验签名
- 配置路由来路由不同类型的消息
- 对于每种消息的处理器
消息处理器编写接口
我们只需要实现WxMpMessageHandler就可以编写一个微信消息的处理器,下面给出几个示例:
/**
* 事件处理器
**/
@Component
public class EventHandler implements WxMpMessageHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService,WxSessionManager wxSessionManager) throws WxErrorException {
// 拿到点击的按钮的key
String eventKey = wxMpXmlMessage.getEventKey();
//返回的内容
String content = "";
switch (eventKey) {
case 某个key:
// 做些什么
break;
}
// 调用接口,返回消息
return WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
}
/**
* 消息处理器
**/
@Component
@Slf4j
public class MessageHandler implements WxMpMessageHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
// 拿到用户发的消息
String content = wxMpXmlMessage.getContent();
// 返回的消息
String res = "我是复读机哇:"+content;
//...做些什么...
return WxMpXmlOutMessage.TEXT().content(res)
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
}
/**
* 关注处理器
*
* @author niumazlb
*/
@Component
public class SubscribeHandler implements WxMpMessageHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
final String content = "感谢关注";
// 调用接口,返回验证码
return WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMpXmlMessage.getToUser())
.toUser(wxMpXmlMessage.getFromUser())
.build();
}
}
注册路由
将刚刚写的处理器注册到路由中
/**
* 微信公众号路由
*/
@Configuration
public class WxMpMsgRouter {
@Resource
private WxMpService wxMpService;
@Resource
private EventHandler eventHandler;
@Resource
private MessageHandler messageHandler;
@Resource
private SubscribeHandler subscribeHandler;
@Bean
public WxMpMessageRouter getWxMsgRouter() {
WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
// 消息
router.rule()
.async(false)
.msgType(XmlMsgType.TEXT) //文本消息
.handler(messageHandler)
.end();
// 关注
router.rule()
.async(false)
.msgType(XmlMsgType.EVENT) //事件消息
.event(EventType.SUBSCRIBE) // 关注事件
.handler(subscribeHandler)
.end();
// 点击"课程"按钮
router.rule()
.async(false)
.msgType(XmlMsgType.EVENT)
.event(EventType.CLICK) //点击事件
.eventKey(WxMpConstant.CLICK_COURSE_KEY) // 点击按钮的key
.handler(eventHandler)
.end();
// 点击"作业"按钮
router.rule()
.async(false)
.msgType(XmlMsgType.EVENT)
.event(EventType.CLICK)
.eventKey(WxMpConstant.CLICK_HOMEWORK_KEY)
.handler(eventHandler)
.end();
return router;
}
}
编写接口
- 先校验参数的正确性
- 判断加密类型
- 路由消息
- 返回信息
@Resource
private WxMpMessageRouter router;
/**
* 接收微信发来的消息
* @param request
* @param response
* @param requestBody
* @return
*/
@PostMapping("/")
public String receiveMessage(HttpServletRequest request, HttpServletResponse response, @RequestBody String requestBody) {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
// 校验消息签名,判断是否为公众平台发的消息
String signature = request.getParameter("signature");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "非法的请求!");
}
// 加密类型
String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ? "raw" : request.getParameter("encrypt_type");
String out = null;
// 明文消息
if ("raw".equals(encryptType)) {
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
log.info("message content = {}", inMessage.getContent());
// 路由消息并处理
WxMpXmlOutMessage outMessage = router.route(inMessage);
if (outMessage == null) {
return "";
} else {
out = outMessage.toXml();
}
}
// aes 加密消息
if ("aes".equals(encryptType)) {
// 解密消息
String msgSignature = request.getParameter("msg_signature");
WxMpXmlMessage inMessage = WxMpXmlMessage
.fromEncryptedXml(requestBody, wxMpService.getWxMpConfigStorage(), timestamp,
nonce,
msgSignature);
log.info("message content = {}", inMessage.getContent());
// 路由消息并处理
WxMpXmlOutMessage outMessage = router.route(inMessage);
if (outMessage == null) {
return "";
} else {
out = outMessage.toXml();
}
}
log.info("\n组装回复信息:{}", out);
return out;
}
测试
在测试号中发送消息,后端打好断点,可以看到发过来的消息
消息也成功的路由到处理器中
同样的,点击按钮也可以触发响应的处理器。
六.群发消息
一、前言 | 微信开放文档 (qq.com)
这里提供一个群发消息的方法,群发消息也需要微信认证才行
public void sendAllMsg(String text) {
String accessToken = this.getAccessToken();
String reqUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=" + accessToken;
Map<String, Object> param = new HashMap<>();
param.put("msgtype", "text");
Map<String, Object> content = new HashMap<>();
content.put("content", text);
param.put("text", content);
Map<String, Object> filter = new HashMap<>();
filter.put("is_to_all", true);
filter.put("tag_id", "");
param.put("filter", filter);
String json = JSONUtil.toJsonStr(param);
String body = HttpRequest.post(reqUrl)
.body(json)
.execute()
.body();
log.info("群发消息返回:{}", body);
}