微信小程序订阅消息推送完整流程说明.md 17 KB

微信小程序订阅消息推送完整流程说明文档

目录

  1. 概述
  2. 系统架构
  3. 后端实现
  4. 前端实现
  5. 配置说明
  6. 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(带缓存)
@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 发送模板消息
@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 获取订阅消息授权

// 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 发送消息请求

// 发送模板消息
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. 完整的前端示例代码

// 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. 登录 微信公众平台
  2. 进入"开发" -> "开发管理" -> "开发设置"
  3. 获取 AppID 和 AppSecret

1.2 配置服务器域名

  1. 进入"开发" -> "开发管理" -> "开发设置" -> "服务器域名"
  2. 添加后端服务器域名到 request 合法域名

1.3 订阅消息模板配置

  1. 进入"功能" -> "订阅消息"
  2. 选择"公共模板库"或"我的模板"
  3. 选择合适的模板并申请
  4. 记录模板 ID(template_id)

2. 后端配置

2.1 application.yml 配置

system:
  wechat:
    appid: your_appid          # 小程序 AppID
    secret: your_secret        # 小程序 AppSecret

spring:
  redis:
    host: localhost
    port: 6379
    password: your_password
    database: 0

2.2 模板消息数据结构

根据微信官方文档,模板消息的 data 字段格式如下:

{
  "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

请求参数

{
  "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"
}

响应

成功响应:

{
  "code": 200,
  "msg": "消息发送成功",
  "data": null
}

失败响应:

{
  "code": 500,
  "msg": "消息发送失败,请检查模板配置和用户订阅状态",
  "data": null
}

2. 批量发送模板消息

请求

  • URL: /wechat/message/send/batch
  • 方法: POST
  • Content-Type: application/json

请求参数

Body:

{
  "templateId": "模板ID",
  "page": "pages/detail/detail?id=123",
  "data": {
    "thing1": {
      "value": "贷款审批"
    },
    "thing2": {
      "value": "您的申请已通过"
    }
  }
}

Query Parameters:

  • openids: 接收者 openid 数组,多个用逗号分隔

响应

{
  "code": 200,
  "msg": "批量发送完成",
  "data": {
    "total": 10,
    "successCount": 8,
    "failCount": 2,
    "failDetails": "openid1; openid2; "
  }
}

完整流程示例

场景:贷款审批通过后发送通知

1. 后端业务逻辑

@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<String, Map<String, String>>() {{
            put("thing1", new HashMap<String, String>() {{
                put("value", "贷款审批");
            }});
            put("thing2", new HashMap<String, String>() {{
                put("value", "您的贷款申请已通过审批");
            }});
            put("time3", new HashMap<String, String>() {{
                put("value", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }});
            put("thing4", new HashMap<String, String>() {{
                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. 前端接收消息

用户在小程序中会收到订阅消息,点击消息会跳转到指定的页面。

// 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. 日志记录: 建议记录所有消息发送的日志,便于问题排查


相关文档


更新日志

  • 2024-01-15: 初始版本,包含完整的消息推送功能
  • 添加了 Access Token 缓存机制
  • 完善了错误处理和日志记录
  • 添加了批量发送功能

文档维护: 如有问题或建议,请联系开发团队。