源码地址:

一.申请微信开发者账号

  1. 注册账号
  2. 申请测试号
  • 这里接口配置信息暂时不填,后面再填

微信公众号谁的java知识 javalibrary公众号_spring boot

二.开发平台与Java端绑定

  1. 基本开发环境
  • 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>
  1. 引入微信公众号开发依赖
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>wx-java-mp-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>
  1. 配置微信公众号测试号相关参数
  • app-id和secret为第一步中测试号提供的appid和appsecret
  • token为接口配置信息中的Token,自定义就好
wx:
  mp:
    app-id: your-app-id
    secret: your-appsecret
    token: your-token
    aes-key:

微信公众号谁的java知识 javalibrary公众号_微信公众号谁的java知识_02

  1. 配置类,配置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;
    }
}
  1. 搭建微信公众号服务
/**
 * 微信公众号相关接口
 *
 * @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工具中测试一下这个域名,看看请求能不能成功发送到本地

  • 在微信开发者平台填入相关信息
  • 能够成功配置,即表示接入成功!就可以开发后续功能了

微信公众号谁的java知识 javalibrary公众号_spring boot_03

踩坑

如果这里内网穿透你使用的是自定义域名,并且域名在除了阿里云的其他厂商备案,那么你在发请求到该域名时,会被阿里云安全拦截!导致配置失败!

  • 要么在阿里云再备案一次,较麻烦
  • 花点小钱在natapp买一个二级域名就可以了,推荐

微信公众号谁的java知识 javalibrary公众号_微信公众平台_04

四.设置公众号菜单

在公众号中我们常常能看到一些按钮,通过点击按钮能够满足不同的需求


微信公众号谁的java知识 javalibrary公众号_微信公众平台_05

我们也能通过代码来更加个性化的定制我们的菜单(只有认证用户可使用接口定制菜单

编写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后端需要编写
  • 一个接口用来接收微信发给我们的消息,校验签名
  • 配置路由来路由不同类型的消息
  • 对于每种消息的处理器

微信公众号谁的java知识 javalibrary公众号_spring boot_06

消息处理器编写接口

我们只需要实现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;
    }

测试

在测试号中发送消息,后端打好断点,可以看到发过来的消息

微信公众号谁的java知识 javalibrary公众号_微信_07

消息也成功的路由到处理器中

微信公众号谁的java知识 javalibrary公众号_spring boot_08

同样的,点击按钮也可以触发响应的处理器。

六.群发消息

一、前言 | 微信开放文档 (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);

    }