SpringBoot对接微信公众号并实现消息发送功能详解

SpringBoot对接微信公众号并实现消息发送功能详解

一、微信公众号开发概述

微信公众号开发是企业在微信生态中进行客户服务、营销推广的重要方式。通过对接微信公众号,企业可以实现自动回复、菜单定制、消息推送等多种功能,极大地提升了与用户的互动体验。

1.1 微信公众号类型

微信公众号主要分为三种类型:

  1. 订阅号:主要用于信息传播,每天可以群发一条消息,适合媒体和个人使用。
  2. 服务号:提供更多高级接口和能力,每月可群发4条消息,适合企业客户服务。
  3. 企业号(现称企业微信):专注于企业内部管理和办公协同。

1.2 开发模式

微信公众号开发有两种模式:

  1. 编辑模式:通过微信公众平台提供的可视化界面进行配置,适合简单需求。
  2. 开发模式:通过API接口与自有服务器对接,实现更复杂的功能。

本文主要讲解基于SpringBoot的开发模式对接。

二、开发前准备工作

2.1 申请微信公众号

  1. 访问微信公众平台(mp.weixin.qq.com/)注册账号%25E6%25B3%25A8%25E5%2586%258C%25E8%25B4%25A6%25E5%258F%25B7 "https://mp.weixin.qq.com/)%E6%B3%A8%E5%86%8C%E8%B4%A6%E5%8F%B7")
  2. 选择账号类型(服务号或订阅号)
  3. 完成资质认证(企业用户需要)

2.2 服务器环境准备

  1. 公网可访问的服务器(建议使用云服务器)
  2. 备案域名(微信要求)
  3. SSL证书(必须启用HTTPS)

2.3 开发工具准备

  1. JDK 1.8+
  2. Maven或Gradle
  3. IDE(IntelliJ IDEA或Eclipse)
  4. 内网穿透工具(开发调试用,如ngrok)

三、SpringBoot项目搭建

3.1 创建SpringBoot项目

使用Spring Initializr创建项目,添加以下依赖:

xml 复制代码
<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 微信开发Java SDK -->
    <dependency>
        <groupId>com.github.binarywang</groupId>
        <artifactId>weixin-java-mp</artifactId>
        <version>4.1.0</version>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- 其他可能需要的依赖 -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
</dependencies>

3.2 配置微信公众号参数

application.propertiesapplication.yml中添加配置:

bash 复制代码
# 微信公众号配置
wx:
  mp:
    configs:
      - appId: ${微信公众号AppID}
        secret: ${微信公众号AppSecret}
        token: ${微信公众号Token}
        aesKey: ${微信公众号EncodingAESKey}

3.3 创建配置类

arduino 复制代码
@Configuration
@RequiredArgsConstructor
public class WxMpConfiguration {
    private final WxMpProperties properties;

    @Bean
    public WxMpService wxMpService() {
        WxMpService service = new WxMpServiceImpl();
        WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
        config.setAppId(properties.getAppId());
        config.setSecret(properties.getSecret());
        config.setToken(properties.getToken());
        config.setAesKey(properties.getAesKey());
        service.setWxMpConfigStorage(config);
        return service;
    }
}

@Data
@ConfigurationProperties(prefix = "wx.mp")
public class WxMpProperties {
    private String appId;
    private String secret;
    private String token;
    private String aesKey;
}

四、微信公众号服务器配置

4.1 服务器URL验证

微信服务器会发送GET请求验证服务器有效性,需要实现校验逻辑:

less 复制代码
@RestController
@RequestMapping("/wx")
@RequiredArgsConstructor
public class WxPortalController {
    private final WxMpService wxMpService;

    @GetMapping(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) {
        
        if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
            // 签名校验失败
            return "非法请求";
        }
        
        return echostr;
    }
}

4.2 配置服务器地址

在微信公众平台后台:

  1. 进入"开发"->"基本配置"
  2. 点击"修改配置"
  3. 填写服务器URL、Token和EncodingAESKey
  4. 选择消息加解密方式(推荐安全模式)
  5. 提交并启用

五、接收用户消息

5.1 消息接收控制器

less 复制代码
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(
        @RequestBody String requestBody,
        @RequestParam("signature") String signature,
        @RequestParam("timestamp") String timestamp,
        @RequestParam("nonce") String nonce,
        @RequestParam(name = "encrypt_type", required = false) String encType,
        @RequestParam(name = "msg_signature", required = false) String msgSignature) {
    
    // 校验签名
    if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
        throw new IllegalArgumentException("非法请求");
    }
    
    // 处理消息
    WxMpXmlMessage inMessage;
    if (encType == null) {
        // 明文模式
        inMessage = WxMpXmlMessage.fromXml(requestBody);
    } else if ("aes".equalsIgnoreCase(encType)) {
        // AES加密模式
        inMessage = WxMpXmlMessage.fromEncryptedXml(
            requestBody, wxMpService.getWxMpConfigStorage(), 
            timestamp, nonce, msgSignature);
    } else {
        throw new IllegalArgumentException("不支持的加密类型");
    }
    
    // 处理消息并返回响应
    WxMpXmlOutMessage outMessage = handleMessage(inMessage);
    if (outMessage == null) {
        return "";
    }
    
    if (encType == null) {
        return outMessage.toXml();
    } else {
        return outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
    }
}

private WxMpXmlOutMessage handleMessage(WxMpXmlMessage inMessage) {
    try {
        // 根据消息类型进行不同处理
        switch (inMessage.getMsgType()) {
            case TEXT:
                return handleTextMessage(inMessage);
            case IMAGE:
                return handleImageMessage(inMessage);
            case VOICE:
                return handleVoiceMessage(inMessage);
            case VIDEO:
            case SHORTVIDEO:
                return handleVideoMessage(inMessage);
            case LOCATION:
                return handleLocationMessage(inMessage);
            case LINK:
                return handleLinkMessage(inMessage);
            case EVENT:
                return handleEventMessage(inMessage);
            default:
                return null;
        }
    } catch (Exception e) {
        log.error("处理微信消息异常", e);
        return null;
    }
}

5.2 处理文本消息示例

scss 复制代码
private WxMpXmlOutMessage handleTextMessage(WxMpXmlMessage message) {
    String content = message.getContent();
    String fromUser = message.getFromUser();
    String toUser = message.getToUser();
    
    // 简单回复
    if ("你好".equals(content)) {
        return WxMpXmlOutMessage.TEXT()
            .content("您好,有什么可以帮您?")
            .fromUser(toUser)
            .toUser(fromUser)
            .build();
    }
    
    // 关键词回复
    if (content.contains("天气")) {
        return WxMpXmlOutMessage.TEXT()
            .content("您想查询哪个城市的天气?")
            .fromUser(toUser)
            .toUser(fromUser)
            .build();
    }
    
    // 默认回复
    return WxMpXmlOutMessage.TEXT()
        .content("感谢您的消息,我们会尽快回复您!")
        .fromUser(toUser)
        .toUser(fromUser)
        .build();
}

六、发送消息给用户

6.1 模板消息发送

模板消息是公众号向用户发送的重要通知消息,需要事先申请模板。

6.1.1 申请模板
  1. 在公众号后台"功能"->"模板消息"中添加模板
  2. 获取模板ID
6.1.2 发送模板消息
typescript 复制代码
@Service
@RequiredArgsConstructor
public class WxTemplateMessageService {
    private final WxMpService wxMpService;

    public void sendTemplateMessage(String openId, String templateId, 
            String url, Map<String, String> data) {
        // 构建模板消息
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
            .toUser(openId)
            .templateId(templateId)
            .url(url)
            .build();
        
        // 添加模板数据
        data.forEach((key, value) -> {
            templateMessage.addData(new WxMpTemplateData(key, value, "#FF0000"));
        });
        
        try {
            // 发送消息
            wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        } catch (WxErrorException e) {
            log.error("发送模板消息失败", e);
            throw new RuntimeException("发送模板消息失败", e);
        }
    }
}
6.1.3 使用示例
kotlin 复制代码
// 发送订单支付成功通知
Map<String, String> data = new HashMap<>();
data.put("first", "尊敬的客户,您的订单已支付成功");
data.put("keyword1", "ORD123456789");
data.put("keyword2", "¥199.00");
data.put("keyword3", "2023-05-20 14:30:45");
data.put("remark", "感谢您的购买,点击查看订单详情");

wxTemplateMessageService.sendTemplateMessage(
    "oX8Y35E-7Q2j3k9l0i1n2m3k4l5o6p7q", 
    "TEMPLATE_ID", 
    "https://yourdomain.com/order/ORD123456789", 
    data);

6.2 客服消息发送

客服消息是公众号在48小时内与用户进行互动的消息类型。

6.2.1 发送文本消息
typescript 复制代码
public void sendTextMessage(String openId, String content) {
    try {
        wxMpService.getKefuService().sendText(openId, content);
    } catch (WxErrorException e) {
        log.error("发送客服文本消息失败", e);
        throw new RuntimeException("发送客服文本消息失败", e);
    }
}
6.2.2 发送图文消息
typescript 复制代码
public void sendNewsMessage(String openId, String title, String description, 
        String url, String picUrl) {
    WxMpKefuMessage.WxArticle article = new WxMpKefuMessage.WxArticle();
    article.setTitle(title);
    article.setDescription(description);
    article.setUrl(url);
    article.setPicUrl(picUrl);
    
    try {
        wxMpService.getKefuService().sendNews(openId, Collections.singletonList(article));
    } catch (WxErrorException e) {
        log.error("发送客服图文消息失败", e);
        throw new RuntimeException("发送客服图文消息失败", e);
    }
}

6.3 群发消息

群发消息是公众号向大量用户同时发送消息的功能。

6.3.1 按标签群发
scss 复制代码
public void massSendByTag(String content, int tagId) {
    WxMpMassTagMessage message = new WxMpMassTagMessage();
    message.setContent(content);
    message.setTagId(tagId);
    message.setMsgType(WxConsts.MassMsgType.TEXT);
    
    try {
        wxMpService.getMassMessageService().massGroupMessageSend(message);
    } catch (WxErrorException e) {
        log.error("按标签群发消息失败", e);
        throw new RuntimeException("按标签群发消息失败", e);
    }
}
6.3.2 按OpenID列表群发
scss 复制代码
public void massSendByOpenIds(String content, List<String> openIds) {
    WxMpMassOpenIdsMessage message = new WxMpMassOpenIdsMessage();
    message.setContent(content);
    message.setMsgType(WxConsts.MassMsgType.TEXT);
    message.setToUsers(openIds);
    
    try {
        wxMpService.getMassMessageService().massOpenIdsMessageSend(message);
    } catch (WxErrorException e) {
        log.error("按OpenID列表群发消息失败", e);
        throw new RuntimeException("按OpenID列表群发消息失败", e);
    }
}

七、用户管理

7.1 获取用户基本信息

typescript 复制代码
public WxMpUser getUserInfo(String openId) {
    try {
        return wxMpService.getUserService().userInfo(openId);
    } catch (WxErrorException e) {
        log.error("获取用户信息失败", e);
        throw new RuntimeException("获取用户信息失败", e);
    }
}

7.2 获取用户列表

ini 复制代码
public List<WxMpUser> getUserList() {
    try {
        List<WxMpUser> users = new ArrayList<>();
        String nextOpenId = null;
        
        do {
            WxMpUserList userList = wxMpService.getUserService().userList(nextOpenId);
            users.addAll(userList.getOpenids().stream()
                .map(this::getUserInfo)
                .collect(Collectors.toList()));
            nextOpenId = userList.getNextOpenid();
        } while (nextOpenId != null);
        
        return users;
    } catch (WxErrorException e) {
        log.error("获取用户列表失败", e);
        throw new RuntimeException("获取用户列表失败", e);
    }
}

八、自定义菜单管理

8.1 创建自定义菜单

scss 复制代码
public void createMenu() {
    WxMpMenu menu = new WxMpMenu();
    
    // 第一个按钮
    WxMenuButton button1 = new WxMenuButton();
    button1.setType(WxConsts.MenuButtonType.VIEW);
    button1.setName("官网首页");
    button1.setUrl("https://yourdomain.com");
    
    // 第二个按钮 - 带子菜单
    WxMenuButton button2 = new WxMenuButton();
    button2.setName("产品中心");
    
    // 子菜单1
    WxMenuButton subButton1 = new WxMenuButton();
    subButton1.setType(WxConsts.MenuButtonType.VIEW);
    subButton1.setName("产品介绍");
    subButton1.setUrl("https://yourdomain.com/products");
    
    // 子菜单2
    WxMenuButton subButton2 = new WxMenuButton();
    subButton2.setType(WxConsts.MenuButtonType.CLICK);
    subButton2.setName("最新活动");
    subButton2.setKey("LATEST_ACTIVITY");
    
    button2.getSubButtons().add(subButton1);
    button2.getSubButtons().add(subButton2);
    
    // 第三个按钮
    WxMenuButton button3 = new WxMenuButton();
    button3.setType(WxConsts.MenuButtonType.CLICK);
    button3.setName("联系我们");
    button3.setKey("CONTACT_US");
    
    menu.getButtons().add(button1);
    menu.getButtons().add(button2);
    menu.getButtons().add(button3);
    
    try {
        wxMpService.getMenuService().menuCreate(menu);
    } catch (WxErrorException e) {
        log.error("创建菜单失败", e);
        throw new RuntimeException("创建菜单失败", e);
    }
}

8.2 处理菜单点击事件

在消息处理中添加对EVENT类型消息的处理:

java 复制代码
private WxMpXmlOutMessage handleEventMessage(WxMpXmlMessage message) {
    switch (message.getEvent()) {
        case WxConsts.EventType.CLICK:
            return handleMenuClickEvent(message);
        case WxConsts.EventType.VIEW:
            return handleMenuViewEvent(message);
        case WxConsts.EventType.SUBSCRIBE:
            return handleSubscribeEvent(message);
        case WxConsts.EventType.UNSUBSCRIBE:
            return handleUnsubscribeEvent(message);
        // 其他事件处理...
        default:
            return null;
    }
}

private WxMpXmlOutMessage handleMenuClickEvent(WxMpXmlMessage message) {
    String eventKey = message.getEventKey();
    String fromUser = message.getFromUser();
    String toUser = message.getToUser();
    
    switch (eventKey) {
        case "LATEST_ACTIVITY":
            return WxMpXmlOutMessage.TEXT()
                .content("当前最新活动:五一特惠,全场5折起!")
                .fromUser(toUser)
                .toUser(fromUser)
                .build();
        case "CONTACT_US":
            return WxMpXmlOutMessage.TEXT()
                .content("客服电话:400-123-4567\n工作时间:9:00-18:00")
                .fromUser(toUser)
                .toUser(fromUser)
                .build();
        default:
            return null;
    }
}

九、消息加解密

微信提供了三种消息加解密模式:

  1. 明文模式
  2. 兼容模式
  3. 安全模式(推荐)

9.1 配置加解密

ini 复制代码
@Bean
public WxMpService wxMpService(WxMpProperties properties) {
    WxMpService service = new WxMpServiceImpl();
    WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
    config.setAppId(properties.getAppId());
    config.setSecret(properties.getSecret());
    config.setToken(properties.getToken());
    config.setAesKey(properties.getAesKey());
    service.setWxMpConfigStorage(config);
    return service;
}

9.2 消息解密

在接收消息时已经包含了消息解密逻辑:

less 复制代码
if ("aes".equalsIgnoreCase(encType)) {
    // AES加密模式
    inMessage = WxMpXmlMessage.fromEncryptedXml(
        requestBody, wxMpService.getWxMpConfigStorage(), 
        timestamp, nonce, msgSignature);
}

9.3 消息加密

在返回消息时:

kotlin 复制代码
if (encType == null) {
    return outMessage.toXml();
} else {
    return outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
}

十、常见问题与解决方案

10.1 签名验证失败

可能原因:

  1. Token配置不一致
  2. 时间戳差异过大
  3. URL编码问题

解决方案:

  1. 检查公众号后台配置的Token与代码中是否一致
  2. 确保服务器时间正确
  3. 检查URL是否完整包含参数

10.2 消息发送失败

常见错误码:

  • 45015:回复时间超过48小时
  • 40003:无效的OpenID
  • 40037:无效的模板ID

解决方案:

  1. 对于超过48小时未互动的用户,使用模板消息
  2. 检查OpenID是否正确获取
  3. 确保模板消息已申请并正确配置

10.3 性能优化建议

  1. 异步处理:对于耗时操作,使用异步处理避免阻塞
  2. 缓存AccessToken:AccessToken获取频率有限制,应缓存使用
  3. 消息队列:高并发场景下使用消息队列削峰

十一、高级功能扩展

11.1 微信网页授权

获取用户详细信息需要网页授权:

typescript 复制代码
public String getAuthUrl(String redirectUri, String scope) {
    return wxMpService.getOAuth2Service().buildAuthorizationUrl(
        redirectUri, 
        scope, 
        null);
}

public WxOAuth2AccessToken getAccessToken(String code) {
    try {
        return wxMpService.getOAuth2Service().getAccessToken(code);
    } catch (WxErrorException e) {
        log.error("获取网页授权access_token失败", e);
        throw new RuntimeException("获取网页授权access_token失败", e);
    }
}

public WxOAuth2UserInfo getUserInfo(String accessToken, String openId) {
    try {
        return wxMpService.getOAuth2Service().getUserInfo(accessToken, openId);
    } catch (WxErrorException e) {
        log.error("获取用户信息失败", e);
        throw new RuntimeException("获取用户信息失败", e);
    }
}

11.2 微信JS-SDK配置

前端页面使用微信JS-SDK:

arduino 复制代码
public Map<String, String> getJsApiConfig(String url) {
    try {
        WxJsapiSignature signature = wxMpService.createJsapiSignature(url);
        Map<String, String> config = new HashMap<>();
        config.put("appId", signature.getAppId());
        config.put("timestamp", String.valueOf(signature.getTimestamp()));
        config.put("nonceStr", signature.getNonceStr());
        config.put("signature", signature.getSignature());
        return config;
    } catch (WxErrorException e) {
        log.error("生成JS-SDK配置失败", e);
        throw new RuntimeException("生成JS-SDK配置失败", e);
    }
}

11.3 素材管理

上传和管理多媒体素材:

typescript 复制代码
public String uploadMedia(String mediaType, String filePath) {
    try {
        WxMediaUploadResult result = wxMpService.getMaterialService()
            .mediaUpload(mediaType, new File(filePath));
        return result.getMediaId();
    } catch (WxErrorException e) {
        log.error("上传媒体文件失败", e);
        throw new RuntimeException("上传媒体文件失败", e);
    }
}

public File downloadMedia(String mediaId) {
    try {
        return wxMpService.getMaterialService().mediaDownload(mediaId);
    } catch (WxErrorException e) {
        log.error("下载媒体文件失败", e);
        throw new RuntimeException("下载媒体文件失败", e);
    }
}

十二、安全与监控

12.1 接口调用监控

less 复制代码
@Aspect
@Component
@Slf4j
public class WxApiMonitorAspect {
    @Around("execution(* me.chanjar.weixin.mp.api.impl..*.*(..))")
    public Object monitorWxApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            long elapsed = System.currentTimeMillis() - startTime;
            log.info("微信API调用成功 - 方法: {}, 耗时: {}ms", methodName, elapsed);
            return result;
        } catch (WxErrorException e) {
            log.error("微信API调用失败 - 方法: {}, 错误码: {}, 错误信息: {}", 
                methodName, e.getError().getErrorCode(), e.getError().getErrorMsg());
            throw e;
        }
    }
}

12.2 敏感数据保护

  1. 加密存储用户OpenID等敏感信息
  2. 使用HTTPS传输数据
  3. 实现数据访问权限控制

十三、测试与部署

13.1 测试策略

  1. 单元测试:测试核心业务逻辑
  2. 集成测试:测试微信API调用
  3. 端到端测试:模拟用户完整操作流程

13.2 部署建议

  1. 使用Docker容器化部署
  2. 配置负载均衡应对高并发
  3. 设置健康检查接口

十四、总结

本文详细介绍了如何使用SpringBoot对接微信公众号并实现消息发送功能,涵盖了从基础配置到高级功能的完整流程。通过合理的设计和实现,可以构建出稳定、高效的微信公众号后台服务,为用户提供优质的交互体验。

在实际开发中,还需要根据业务需求进行适当的调整和扩展,同时注意微信公众平台的接口调用频率限制和安全规范。希望本文能为开发者提供有价值的参考,助力微信公众号开发工作。

相关推荐
Victor3561 小时前
Redis(25)Redis的RDB持久化的优点和缺点是什么?
后端
Victor3561 小时前
Redis(24)如何配置Redis的持久化?
后端
ningqw8 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友8 小时前
vi编辑器命令常用操作整理(持续更新)
后端
胡gh8 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫9 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong9 小时前
技术人如何对客做好沟通(上篇)
后端
颜如玉10 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment10 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源
why技术11 小时前
在我眼里,这就是天才般的算法!
后端·面试