在Spring Boot中实现微信公众号模板消息推送

428 阅读3分钟

在Spring Boot中实现微信公众号模板消息推送,主要分为以下步骤:

一、准备工作

  1. 微信公众号配置

    • 认证服务号(订阅号无模板消息权限)
    • 在公众号后台申请模板消息,获取模板ID
    • 配置IP白名单(服务器公网IP)
  2. 获取关键信息

    • AppID 和 AppSecret
    • 用户OpenID(通过网页授权获取)

二、Spring Boot 实现步骤

1. 添加依赖

<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- HTTP Client -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.13</version>
    </dependency>
    
    <!-- JSON 处理 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
</dependencies>

2. 配置文件 (application.yml)

wechat:
  appId: your_app_id
  appSecret: your_app_secret
  templateId: your_template_id  # 申请的模板消息ID

3. 配置类读取参数

@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatConfig {
    private String appId;
    private String appSecret;
    private String templateId;
    
    // Getters & Setters
}

4. AccessToken 管理(带缓存)

@Service
public class AccessTokenService {
    @Autowired
    private WechatConfig wechatConfig;

    private String accessToken;
    private long expireTime;

    // 获取AccessToken(2小时有效)
    public String getAccessToken() throws IOException {
        if (accessToken == null || System.currentTimeMillis() > expireTime) {
            refreshToken();
        }
        return accessToken;
    }

    private void refreshToken() throws IOException {
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" +
                     "&appid=" + wechatConfig.getAppId() + 
                     "&secret=" + wechatConfig.getAppSecret();

        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet(url);
        CloseableHttpResponse response = client.execute(get);
        
        String result = EntityUtils.toString(response.getEntity());
        JSONObject json = JSON.parseObject(result);
        
        if (json.containsKey("access_token")) {
            accessToken = json.getString("access_token");
            // 提前5分钟刷新
            expireTime = System.currentTimeMillis() + (json.getLong("expires_in") - 300) * 1000;
        } else {
            throw new RuntimeException("获取AccessToken失败: " + json.getString("errmsg"));
        }
    }
}

5. 模板消息推送服务

@Service
public class TemplateMessageService {
    @Autowired
    private AccessTokenService tokenService;
    @Autowired
    private WechatConfig wechatConfig;

    public void sendTemplateMessage(String openId, Map<StringString> data) throws IOException {
        String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" 
                   + tokenService.getAccessToken();

        // 构建请求体
        JSONObject body = new JSONObject();
        body.put("touser", openId);
        body.put("template_id", wechatConfig.getTemplateId());
        body.put("url""https://yourdomain.com"); // 跳转链接(可选)
        
        // 构建模板数据
        JSONObject templateData = new JSONObject();
        data.forEach((key, value) -> {
            JSONObject item = new JSONObject();
            item.put("value", value);
            item.put("color""#173177"); // 文字颜色
            templateData.put(key, item);
        });
        body.put("data", templateData);

        // 发送请求
        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost post = new HttpPost(url);
        post.setHeader("Content-Type""application/json");
        post.setEntity(new StringEntity(body.toJSONString(), "UTF-8"));
        
        CloseableHttpResponse response = client.execute(post);
        String result = EntityUtils.toString(response.getEntity());
        JSONObject json = JSON.parseObject(result);
        
        if (json.getInteger("errcode") != 0) {
            throw new RuntimeException("模板消息发送失败: " + json.getString("errmsg"));
        }
    }
}

6. 控制器示例

@RestController
@RequestMapping("/wechat")
public class WechatController {
    @Autowired
    private TemplateMessageService messageService;

    @PostMapping("/send")
    public String sendMessage(@RequestParam String openId) {
        try {
            // 示例模板数据(根据实际模板字段修改)
            Map<String, String> data = new HashMap<>();
            data.put("first""您好,您有新的订单!");
            data.put("orderNo""202306270001");
            data.put("amount""¥99.00");
            data.put("remark""点击查看详情");
            
            messageService.sendTemplateMessage(openId, data);
            return "消息发送成功";
        } catch (Exception e) {
            return "发送失败: " + e.getMessage();
        }
    }
}

三、模板消息格式说明

微信模板消息结构示例:

{
  "touser": "OPENID",
  "template_id": "TEMPLATE_ID",
  "url": "https://example.com",
  "data": {
    "first": {"value": "通知内容", "color": "#173177"},
    "key1": {"value": "值1"},
    "key2": {"value": "值2"},
    "remark": {"value": "备注信息"}
  }
}

四、关键注意事项

  1. AccessToken管理

    • 必须全局缓存,避免频繁刷新
    • 推荐使用Redis存储(分布式环境必备)
  2. 模板字段匹配

    • 模板内容需与公众号申请的模板完全一致
    • 字段名(如 firstkeyword1)必须匹配
  3. 用户OpenID获取

    • 通过网页授权获取(snsapi_userinfo
    • 或用户与公众号交互时收集
  4. 错误处理

    • 处理微信返回的 errcode(如40001 token失效)
    • 实现重试机制
  5. 性能优化

    • 使用连接池管理HTTP请求
    • 异步发送消息(结合Spring Async)

五、常见错误码

错误码说明解决方案
40001Invalid credentialAccessToken过期或无效
41028Form id expired表单ID过期(小程序场景)
45009API freq out of limit接口调用频率超限

六、扩展建议

  1. 消息队列:使用RabbitMQ/Kafka异步处理发送请求
  2. 发送记录:数据库存储发送状态便于追踪
  3. 多公众号支持:通过不同配置实现多账号管理

完整实现需结合具体业务场景调整模板内容和用户OpenID获取逻辑。建议先使用微信公众平台接口测试工具(微信官方调试工具)验证消息格式是否正确。