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对接微信公众号并实现消息发送功能,涵盖了从基础配置到高级功能的完整流程。通过合理的设计和实现,可以构建出稳定、高效的微信公众号后台服务,为用户提供优质的交互体验。

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

相关推荐
Nejosi_念旧2 小时前
解读 Go 中的 constraints包
后端·golang·go
风无雨2 小时前
GO 启动 简单服务
开发语言·后端·golang
小明的小名叫小明2 小时前
Go从入门到精通(19)-协程(goroutine)与通道(channel)
后端·golang
斯普信专业组2 小时前
Go语言包管理完全指南:从基础到最佳实践
开发语言·后端·golang
一只叫煤球的猫4 小时前
【🤣离谱整活】我写了一篇程序员掉进 Java 异世界的短篇小说
java·后端·程序员
你的人类朋友5 小时前
🫏光速入门cURL
前端·后端·程序员
今日热点7 小时前
小程序主体变更全攻略:流程、资料与异常处理方案
经验分享·微信·小程序·企业微信·微信公众平台·微信开放平台
aramae7 小时前
C++ -- STL -- vector
开发语言·c++·笔记·后端·visual studio
lifallen7 小时前
Paimon 原子提交实现
java·大数据·数据结构·数据库·后端·算法
舒一笑8 小时前
PandaCoder重大产品更新-引入Jenkinsfile文件支持
后端·程序员·intellij idea