# 微信小程序订阅消息推送完整流程说明文档 ## 目录 1. [概述](#概述) 2. [系统架构](#系统架构) 3. [后端实现](#后端实现) 4. [前端实现](#前端实现) 5. [配置说明](#配置说明) 6. [API 接口文档](#api-接口文档) 7. [完整流程示例](#完整流程示例) 8. [常见问题](#常见问题) --- ## 概述 本文档详细说明微信小程序订阅消息推送功能的完整实现流程,包括后端服务实现、前端调用方式、配置要求以及完整的代码示例。 ### 功能特点 - ✅ 支持单个用户消息推送 - ✅ 支持批量用户消息推送 - ✅ Access Token 自动缓存(有效期 7000 秒) - ✅ 完善的错误处理和日志记录 - ✅ 参数验证和异常处理 --- ## 系统架构 ``` ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ 前端小程序 │ ──────> │ 后端 Controller │ ──────> │ WxService │ └─────────────┘ └──────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ 微信 API 服务器 │ └─────────────┘ │ ▼ ┌─────────────┐ │ 用户微信 │ └─────────────┘ ``` --- ## 后端实现 ### 1. 核心服务类:WxServiceImpl **位置**: `src/main/java/com/loan/system/service/Impl/WxServiceImpl.java` #### 主要功能 ##### 1.1 获取 Access Token(带缓存) ```java @Override public String getAccessToken() { // 从 Redis 缓存获取 String cacheKey = "wx_access_token_" + weChatProperties.getAppid(); String cachedToken = redisTemplate.opsForValue().get(cacheKey); if (cachedToken != null && !cachedToken.isEmpty()) { return cachedToken; // 返回缓存的 token } // 缓存不存在,从微信服务器获取 // ... 获取逻辑 // 缓存 7000 秒 redisTemplate.opsForValue().set(cacheKey, accessToken, 7000, TimeUnit.SECONDS); return accessToken; } ``` **特点**: - 自动缓存 Access Token,避免频繁请求 - 缓存时间 7000 秒(微信 token 有效期 7200 秒) - 完善的错误处理和日志记录 ##### 1.2 发送模板消息 ```java @Override public boolean sendTemplateMessage(String openid, TemplateMessage message) { // 参数验证 // 获取 Access Token // 调用微信 API 发送消息 // 返回发送结果 } ``` **特点**: - 完整的参数验证 - 详细的日志记录 - 异常处理机制 ### 2. 控制器:MessageSendController **位置**: `src/main/java/com/loan/system/controller/wechat/MessageSendController.java` #### 接口列表 ##### 2.1 单个消息发送 - **路径**: `/wechat/message/send` - **方法**: `POST` - **功能**: 向单个用户发送订阅模板消息 ##### 2.2 批量消息发送 - **路径**: `/wechat/message/send/batch` - **方法**: `POST` - **功能**: 向多个用户批量发送订阅模板消息 ### 3. DTO 类:TemplateMessageSendDTO **位置**: `src/main/java/com/loan/system/domain/dto/TemplateMessageSendDTO.java` 用于接收前端发送消息的请求参数。 --- ## 前端实现 ### 1. 小程序端订阅消息授权 **重要说明**:订阅消息授权是**纯前端操作**,**不需要后端参与**。用户在小程序中通过 `wx.requestSubscribeMessage` API 进行授权,授权结果由微信服务器直接返回给前端。 **授权流程**: 1. 前端调用 `wx.requestSubscribeMessage` 弹出授权弹窗 2. 用户选择"允许"或"拒绝" 3. 微信服务器返回授权结果给前端 4. 前端根据授权结果决定是否调用后端接口发送消息 在发送消息之前,用户需要先授权订阅消息。 #### 1.1 获取订阅消息授权 ```javascript // pages/index/index.js Page({ // 订阅消息授权 subscribeMessage() { wx.requestSubscribeMessage({ tmplIds: ['模板ID1', '模板ID2'], // 需要订阅的模板ID列表 success: (res) => { console.log('订阅成功', res); // res[模板ID] = 'accept' | 'reject' | 'ban' // 'accept' 表示用户同意订阅 if (res['模板ID1'] === 'accept') { // 用户同意订阅,可以发送消息 this.sendMessage(); } }, fail: (err) => { console.error('订阅失败', err); wx.showToast({ title: '订阅失败', icon: 'none' }); } }); } }); ``` #### 1.2 发送消息请求 ```javascript // 发送模板消息 sendTemplateMessage() { const that = this; // 1. 获取用户 openid(从登录接口获取) const openid = wx.getStorageSync('openid'); // 2. 构建消息数据 const messageData = { openid: openid, templateId: 'your_template_id', // 模板ID page: 'pages/detail/detail?id=123', // 点击消息跳转的页面 data: { thing1: { value: '贷款审批' }, // 模板参数1 thing2: { value: '您的贷款申请已通过审批' }, // 模板参数2 time3: { value: '2024-01-15 10:30:00' }, // 模板参数3 thing4: { value: '请及时查看详情' } // 模板参数4 } }; // 3. 调用后端接口 wx.request({ url: 'https://your-domain.com/api/wechat/message/send', method: 'POST', header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + wx.getStorageSync('token') // 如果有token }, data: messageData, success: (res) => { console.log('发送成功', res); if (res.data.code === 200) { wx.showToast({ title: '消息发送成功', icon: 'success' }); } else { wx.showToast({ title: res.data.msg || '发送失败', icon: 'none' }); } }, fail: (err) => { console.error('发送失败', err); wx.showToast({ title: '网络错误', icon: 'none' }); } }); } ``` ### 2. 完整的前端示例代码 ```javascript // pages/message/message.js Page({ data: { openid: '', templateId: 'your_template_id' }, onLoad() { // 获取 openid const openid = wx.getStorageSync('openid'); this.setData({ openid }); }, // 订阅消息 handleSubscribe() { wx.requestSubscribeMessage({ tmplIds: [this.data.templateId], success: (res) => { if (res[this.data.templateId] === 'accept') { wx.showToast({ title: '订阅成功', icon: 'success' }); } else { wx.showToast({ title: '需要订阅才能接收消息', icon: 'none' }); } } }); }, // 发送消息 handleSendMessage() { const app = getApp(); wx.request({ url: `${app.globalData.baseUrl}/wechat/message/send`, method: 'POST', header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${wx.getStorageSync('token')}` }, data: { openid: this.data.openid, templateId: this.data.templateId, page: 'pages/detail/detail?id=123', data: { thing1: { value: '贷款审批通知' }, thing2: { value: '您的申请已通过' }, time3: { value: new Date().toLocaleString() }, thing4: { value: '请及时查看' } } }, success: (res) => { if (res.data.code === 200) { wx.showToast({ title: '发送成功', icon: 'success' }); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); } }, fail: (err) => { wx.showToast({ title: '网络错误', icon: 'none' }); } }); } }); ``` --- ## 配置说明 ### 1. 微信小程序配置 #### 1.1 获取 AppID 和 AppSecret 1. 登录 [微信公众平台](https://mp.weixin.qq.com/) 2. 进入"开发" -> "开发管理" -> "开发设置" 3. 获取 AppID 和 AppSecret #### 1.2 配置服务器域名 1. 进入"开发" -> "开发管理" -> "开发设置" -> "服务器域名" 2. 添加后端服务器域名到 request 合法域名 #### 1.3 订阅消息模板配置 1. 进入"功能" -> "订阅消息" 2. 选择"公共模板库"或"我的模板" 3. 选择合适的模板并申请 4. 记录模板 ID(template_id) ### 2. 后端配置 #### 2.1 application.yml 配置 ```yaml system: wechat: appid: your_appid # 小程序 AppID secret: your_secret # 小程序 AppSecret spring: redis: host: localhost port: 6379 password: your_password database: 0 ``` #### 2.2 模板消息数据结构 根据微信官方文档,模板消息的 `data` 字段格式如下: ```json { "data": { "thing1": { "value": "内容1" }, "thing2": { "value": "内容2" }, "time3": { "value": "2024-01-15 10:30:00" } } } ``` **注意**: - 字段名(如 `thing1`)必须与模板中定义的字段名一致 - 字段类型必须匹配(thing、time、number 等) - value 长度不能超过模板定义的最大长度 --- ## API 接口文档 ### 1. 发送单个模板消息 #### 请求 - **URL**: `/wechat/message/send` - **方法**: `POST` - **Content-Type**: `application/json` #### 请求参数 ```json { "openid": "用户的openid", "templateId": "模板ID", "page": "pages/detail/detail?id=123", "data": { "thing1": { "value": "贷款审批" }, "thing2": { "value": "您的申请已通过" }, "time3": { "value": "2024-01-15 10:30:00" } }, "clientMsgId": "可选,防重入ID" } ``` #### 响应 **成功响应**: ```json { "code": 200, "msg": "消息发送成功", "data": null } ``` **失败响应**: ```json { "code": 500, "msg": "消息发送失败,请检查模板配置和用户订阅状态", "data": null } ``` ### 2. 批量发送模板消息 #### 请求 - **URL**: `/wechat/message/send/batch` - **方法**: `POST` - **Content-Type**: `application/json` #### 请求参数 **Body**: ```json { "templateId": "模板ID", "page": "pages/detail/detail?id=123", "data": { "thing1": { "value": "贷款审批" }, "thing2": { "value": "您的申请已通过" } } } ``` **Query Parameters**: - `openids`: 接收者 openid 数组,多个用逗号分隔 #### 响应 ```json { "code": 200, "msg": "批量发送完成", "data": { "total": 10, "successCount": 8, "failCount": 2, "failDetails": "openid1; openid2; " } } ``` --- ## 完整流程示例 ### 场景:贷款审批通过后发送通知 #### 1. 后端业务逻辑 ```java @Service public class LoanService { @Autowired private WxService wxService; @Autowired private CustomerService customerService; /** * 审批通过后发送通知 */ public void approveLoan(Long loanId) { // 1. 获取贷款信息 Loan loan = loanRepository.findById(loanId); // 2. 获取客户信息(包含 openid) Customer customer = customerService.findById(loan.getCustomerId()); String openid = customer.getOpenid(); // 3. 构建模板消息 TemplateMessage message = new TemplateMessage(); message.setTemplate_id("your_template_id"); message.setPage("pages/loan/detail?id=" + loanId); message.setData(new HashMap>() {{ put("thing1", new HashMap() {{ put("value", "贷款审批"); }}); put("thing2", new HashMap() {{ put("value", "您的贷款申请已通过审批"); }}); put("time3", new HashMap() {{ put("value", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }}); put("thing4", new HashMap() {{ put("value", "请及时查看详情"); }}); }}); // 4. 发送消息 try { boolean success = wxService.sendTemplateMessage(openid, message); if (success) { log.info("审批通知发送成功: loanId={}, openid={}", loanId, openid); } else { log.warn("审批通知发送失败: loanId={}, openid={}", loanId, openid); } } catch (Exception e) { log.error("发送审批通知异常: loanId={}, error={}", loanId, e.getMessage(), e); } } } ``` #### 2. 前端接收消息 用户在小程序中会收到订阅消息,点击消息会跳转到指定的页面。 ```javascript // pages/loan/detail.js Page({ onLoad(options) { const loanId = options.id; // 加载贷款详情 this.loadLoanDetail(loanId); } }); ``` --- ## 常见问题 ### 0. 授权订阅消息需要后端操作吗? **答**:**不需要**。订阅消息授权是纯前端操作,使用微信小程序提供的 `wx.requestSubscribeMessage` API 即可完成。后端只需要提供发送消息的接口,不需要参与授权流程。 **授权流程**: - 前端调用 `wx.requestSubscribeMessage` → 用户授权 → 微信返回授权结果 → 前端根据结果决定是否调用后端发送消息 ### 1. 用户未授权时,消息发送会报错还是没反应? **答**:**会报错**。如果用户未授权订阅消息,后端调用微信API发送消息时,微信服务器会返回错误码 `43101`,错误信息为 `"user refuse to accept the msg"`(用户拒绝接受消息)。 **具体行为**: - 后端会正常调用微信API,不会抛出异常 - 微信API返回错误码 `43101` 和错误信息 - 后端方法返回 `false`,表示发送失败 - 后端会记录警告日志,包含错误码和错误信息 - 前端会收到错误响应,提示消息发送失败 **示例日志**: ``` WARN 发送订阅消息失败: openid=xxx, templateId=xxx, errcode=43101, errmsg=user refuse to accept the msg WARN 用户未授权订阅消息: openid=xxx, templateId=xxx, 用户需要先通过前端授权订阅 ``` **解决方案**: - **前端必须**在发送消息前调用 `wx.requestSubscribeMessage` 获取用户授权 - 只有用户选择"允许"后,才能调用后端接口发送消息 - 如果用户拒绝授权,前端应该提示用户并引导其重新授权 - 用户可能勾选了"总是保持以上选择,不再询问"并拒绝,此时需要引导用户到小程序设置中手动开启订阅权限 ### 2. 消息发送失败,返回 errcode: 40037 **原因**: 模板 ID 不存在或已删除 **解决方案**: - 检查模板 ID 是否正确 - 确认模板是否已通过审核 - 检查模板是否已被删除 ### 3. Access Token 获取失败 **原因**: AppID 或 AppSecret 配置错误 **解决方案**: - 检查 `application.yml` 中的配置是否正确 - 确认 AppID 和 AppSecret 是否匹配 - 检查网络连接是否正常 ### 4. 模板数据格式错误 **原因**: data 字段格式不符合模板要求 **解决方案**: - 检查 data 中的字段名是否与模板定义一致 - 确认字段类型是否正确(thing、time、number 等) - 检查 value 长度是否超过限制 ### 5. 用户收不到消息 **可能原因**: 1. 用户未订阅消息 2. 用户在小程序设置中关闭了消息通知 3. 消息发送失败但未捕获错误 **解决方案**: - 在发送前检查用户订阅状态 - 引导用户开启消息通知 - 检查后端日志确认是否发送成功 --- ## 注意事项 1. **订阅消息有效期**: 用户授权订阅后,每个模板消息只能发送一次,有效期根据模板类型而定(通常为永久或一定次数) 2. **消息发送频率**: 避免频繁发送消息,以免被微信限制 3. **模板审核**: 新申请的模板需要审核通过后才能使用 4. **错误处理**: 建议在生产环境中实现重试机制和消息队列,确保消息可靠发送 5. **日志记录**: 建议记录所有消息发送的日志,便于问题排查 --- ## 相关文档 - [微信小程序订阅消息官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html) - [模板消息字段说明](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html) --- ## 更新日志 - **2024-01-15**: 初始版本,包含完整的消息推送功能 - 添加了 Access Token 缓存机制 - 完善了错误处理和日志记录 - 添加了批量发送功能 --- **文档维护**: 如有问题或建议,请联系开发团队。