本文档详细说明微信小程序订阅消息推送功能的完整实现流程,包括后端服务实现、前端调用方式、配置要求以及完整的代码示例。
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 前端小程序 │ ──────> │ 后端 Controller │ ──────> │ WxService │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ 微信 API 服务器 │
└─────────────┘
│
▼
┌─────────────┐
│ 用户微信 │
└─────────────┘
位置: src/main/java/com/loan/system/service/Impl/WxServiceImpl.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;
}
特点:
@Override
public boolean sendTemplateMessage(String openid, TemplateMessage message) {
// 参数验证
// 获取 Access Token
// 调用微信 API 发送消息
// 返回发送结果
}
特点:
位置: src/main/java/com/loan/system/controller/wechat/MessageSendController.java
/wechat/message/sendPOST/wechat/message/send/batchPOST位置: src/main/java/com/loan/system/domain/dto/TemplateMessageSendDTO.java
用于接收前端发送消息的请求参数。
重要说明:订阅消息授权是纯前端操作,不需要后端参与。用户在小程序中通过 wx.requestSubscribeMessage API 进行授权,授权结果由微信服务器直接返回给前端。
授权流程:
wx.requestSubscribeMessage 弹出授权弹窗在发送消息之前,用户需要先授权订阅消息。
// 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'
});
}
});
}
});
// 发送模板消息
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'
});
}
});
}
// 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'
});
}
});
}
});
system:
wechat:
appid: your_appid # 小程序 AppID
secret: your_secret # 小程序 AppSecret
spring:
redis:
host: localhost
port: 6379
password: your_password
database: 0
根据微信官方文档,模板消息的 data 字段格式如下:
{
"data": {
"thing1": {
"value": "内容1"
},
"thing2": {
"value": "内容2"
},
"time3": {
"value": "2024-01-15 10:30:00"
}
}
}
注意:
thing1)必须与模板中定义的字段名一致/wechat/message/sendPOSTapplication/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
}
/wechat/message/send/batchPOSTapplication/jsonBody:
{
"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; "
}
}
@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);
}
}
}
用户在小程序中会收到订阅消息,点击消息会跳转到指定的页面。
// pages/loan/detail.js
Page({
onLoad(options) {
const loanId = options.id;
// 加载贷款详情
this.loadLoanDetail(loanId);
}
});
答:不需要。订阅消息授权是纯前端操作,使用微信小程序提供的 wx.requestSubscribeMessage API 即可完成。后端只需要提供发送消息的接口,不需要参与授权流程。
授权流程:
wx.requestSubscribeMessage → 用户授权 → 微信返回授权结果 → 前端根据结果决定是否调用后端发送消息答:会报错。如果用户未授权订阅消息,后端调用微信API发送消息时,微信服务器会返回错误码 43101,错误信息为 "user refuse to accept the msg"(用户拒绝接受消息)。
具体行为:
43101 和错误信息false,表示发送失败示例日志:
WARN 发送订阅消息失败: openid=xxx, templateId=xxx, errcode=43101, errmsg=user refuse to accept the msg
WARN 用户未授权订阅消息: openid=xxx, templateId=xxx, 用户需要先通过前端授权订阅
解决方案:
wx.requestSubscribeMessage 获取用户授权原因: 模板 ID 不存在或已删除
解决方案:
原因: AppID 或 AppSecret 配置错误
解决方案:
application.yml 中的配置是否正确原因: data 字段格式不符合模板要求
解决方案:
可能原因:
解决方案:
订阅消息有效期: 用户授权订阅后,每个模板消息只能发送一次,有效期根据模板类型而定(通常为永久或一定次数)
消息发送频率: 避免频繁发送消息,以免被微信限制
模板审核: 新申请的模板需要审核通过后才能使用
错误处理: 建议在生产环境中实现重试机制和消息队列,确保消息可靠发送
日志记录: 建议记录所有消息发送的日志,便于问题排查
文档维护: 如有问题或建议,请联系开发团队。