前言
最近在做“村民意见反馈”小程序,需要实现:村民提交意见后,网格员能立刻收到微信通知。微信小程序提供了“订阅消息”能力,用户授权一次后,服务端就能主动推送消息。本文将完整记录从申请模板、后端开发到前端调试的全过程,并提供可直接运行的代码。
首先说一下微信小程序关于额度以及订阅消息的简略信息(消息订阅相关信息):
微信小程序的订阅消息发送额度规则如下:
对于一次性订阅消息,用户每次授权仅可触发一次消息发送,无总量限制,但必须在用户授权后立即使用。
对于长期订阅消息,需满足特定条件(如政务、医疗、交通等公共服务场景)并申请特殊模板,经平台审核后方可使用,普通企业主体通常无法申请。
目前,微信平台不对企业主体的小程序设置独立的订阅消息总量额度。只要用户完成授权,且消息内容符合模板规范,即可发送。发送成功率主要取决于以下因素:
- 用户是否已授权对应模板id的消息订阅。
- 消息内容是否符合模板字段要求。
- 是否在用户授权后的有效时间内调用发送接口(通常为7天内)。
因此,企业小程序的订阅消息发送能力主要由用户授权行为驱动,而非平台分配的固定额度。
一、为什么需要订阅消息?
微信早期有“模板消息”,但限制较多且容易骚扰用户。后来推出了订阅消息,核心特点是:
用户主动订阅:每次发送前必须获得用户授权(一次性订阅)或长期授权(长期订阅)。
服务端主动推送:用户授权后,你可以在业务触发时(如订单状态变更、意见处理)向用户推送服务通知。
适合场景:订单提醒、物流通知、政务办事进度、意见处理反馈等。
二、前置准备
2.1 小程序账号与类目
订阅消息对类目基本无限制(一次性订阅),但长期订阅只对政务、医疗、交通等民生类目开放。
本文以一次性订阅为例,实现一个简单的“意见提交 → 通知网格员”场景。
2.2 申请订阅消息模板
1.登录小程序后台 → 功能 → 订阅消息 → 从公共模板库添加模板(或自定义模板)。


2.选择一个适合的模板,例如“监理报告提交通知”,模板字段可能包含:
- 1.服务名称
{{thing1.data}}
- 2.检测结果
{{thing2.data}}
- 3.提交时间
{{time3.data}}
3.记下模板id。
2.3 后端技术选型
spring boot 2.x
redis:缓存
access_token和用户订阅状态hutool:简化 http 请求和 json 处理
jdk 8+
三、整体流程(看图理解)
text
用户(网格员)进入小程序
│
├─ 点击“订阅消息”按钮
│ └─ wx.requestsubscribemessage() 弹窗授权
│ └─ 允许 → 前端调用后端接口 /subscribe/record
│ └─ 后端存储 openid + templateid(redis,30天有效期)
│
村民提交意见
│
├─ 后端根据业务找到对应的网格员 openid
├─ 检查该 openid 是否已订阅(redis 查询)
├─ 若已订阅 → 获取 access_token(带缓存的)
├─ 调用微信发送消息接口 https://api.weixin.qq.com/cgi-bin/message/subscribe/send
└─ 网格员在微信“服务通知”中收到消息
四、后端核心实现(java)
获取appid以及secret以及templateid(模板id)

4.1 配置类:配置 appid / secret / templateid
#小程序appid wechat.appid=xxxxx #小程序密钥 wechat.secret=xxxxxx #订阅消息的模板id wechat.templateid=xxxxxxxxx #登录wx登录验证 wechat.loginurl=https://api.weixin.qq.com/sns/jscode2session #wx订阅消息发送 wechat.sendurl=https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=
4.2 redis 工具类(简化版)
@component
public class redisutils {
@autowired
private redistemplate<string, string> redistemplate;
public string get(string key) {
return redistemplate.opsforvalue().get(key);
}
public void set(string key, string value, long expireseconds) {
redistemplate.opsforvalue().set(key, value, duration.ofseconds(expireseconds));
}
public boolean setifabsent(string key, string value, long expireseconds) {
return boolean.true.equals(redistemplate.opsforvalue()
.setifabsent(key, value, duration.ofseconds(expireseconds)));
}
public long executelua(string script, string key, string value) {
defaultredisscript<long> redisscript = new defaultredisscript<>();
redisscript.setscripttext(script);
redisscript.setresulttype(long.class);
return redistemplate.execute(redisscript, collections.singletonlist(key), value);
}
}4.3 获取 access_token(redis 缓存 + 分布式锁)
@service
public class accesstokenservice {
@autowired
private redisutils redisutils;
private static final string token_key = "wechat:access_token";
private static final string lock_key = "wechat:appeal_token_lock";
private static final long lock_expire_seconds = 5;
private static final string subscribe_prefix = "subscribe:";
private static final string lua_release_script =
"if redis.call('get', keys[1]) == argv[1] then return redis.call('del', keys[1]) else return 0 end";
/**
* 获取accesstoken
*
* @return
*/
public string getaccesstoken() {
//先取缓存
string token = (string) redisutils.get(token_key);
if (token != null && !token.isempty()) {
return token;
}
//缓存失效,尝试加锁
string lockvalue = string.valueof(system.currenttimemillis());
boolean locked = redisutils.setifabsent(lock_key, lockvalue, lock_expire_seconds);
if (locked) {
try {
// 双重检查
token = (string) redisutils.getwechat(token_key);
if (token != null) {
return token;
}
return refreshaccesstoken();
} finally {
redisutils.executelua(lua_release_script, lock_key, lockvalue);
}
} else {
// 未获得锁,等待后重试
try {
thread.sleep(100);
} catch (interruptedexception e) {
thread.currentthread().interrupt();
}
return getaccesstoken();
}
}
/**
* 刷新accesstoken
*/
private string refreshaccesstoken() {
string url = string.format(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
appid, secret
);
try {
string response = httprequest.get(url).timeout(5000).execute().body();
jsonobject json = jsonutil.parseobj(response);
if (json.containskey("errcode")) {
throw new commonexception("微信获取accesstoken失败: " + json.getstr("errmsg"));
}
string token = json.getstr("access_token");
integer expiresin = json.getint("expires_in");
// 存入 redis,有效期设为 7000 秒(微信是7200秒)
redisutils.setwechat(token_key, token, expiresin - 200);
return token;
} catch (exception e) {
throw new commonexception("获取accesstoken网络异常", e);
}
}
}4.4 发送订阅消息
@service
@slf4j
public class subscribemessageservice {
@value("${wechat.appid}")
private string appid;
@value("${wechat.secret}")
private string secret;
@value("${wechat.templateid}")
private string templateid;
@value("${wechat.sendurl}")
private string sendurl;
private static final string subscribe_prefix = "subscribe:";
@autowired
private redisutils redisutils;
/**
* 记录用户订阅
*/
public void record() {
// 获取登录用户获取对应的openid
sabaseloginuser loginuser = null;
try {
loginuser = stploginuserutil.getloginuser();
string openid = bizuserservice.getopenidbyid(loginuser.getid());
string key = subscribe_prefix + templateid + ":" + openid;
redisutils.setwechat(key, "1", 30 * 24 * 3600l); // 存储30天
} catch (exception e) {
throw new commonexception(e.getmessage());
}
}
/**
* 判断用户是否订阅
*/
public boolean issubscribed(string openid, string templateid) {
string key = subscribe_prefix + templateid + ":" + openid;
return "1".equals(redisutils.getwechat(key));
}
private void getuserlistbygridid(string openid) {
// 此处代码可替换为具体的业务实现 就是获取需要推送用户的openid
// 推送消息给网格员
// 判断用户是否订阅了消息
if (issubscribed(openid, templateid)) {
map<string, string> datamap = new hashmap<>();
datamap.put("thing1", "意见提醒");
datamap.put("thing2", "这是一条测试信息");
datamap.put("time3", localdatetime.now().format(datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss")));
boolean b = sendsubscribemessage(openid, templateid, null, datamap);
if (!b) {
throw new commonexception("推送消息失败,请重试");
}
}
}
/**
* 发送订阅消息
*/
public boolean sendsubscribemessage(string openid, string templateid,
string page, map<string, string> data) {
string accesstoken = getaccesstoken();
string url = sendurl + accesstoken;
jsonobject requestbody = new jsonobject();
requestbody.set("touser", openid);
requestbody.set("template_id", templateid);
if (page != null && !page.isempty()) {
requestbody.set("page", page);
}
requestbody.set("miniprogram_state", "formal");
jsonobject datajson = new jsonobject();
for (map.entry<string, string> entry : data.entryset()) {
jsonobject item = new jsonobject();
item.set("value", entry.getvalue());
datajson.set(entry.getkey(), item);
}
requestbody.set("data", datajson);
try {
string response = httprequest.post(url)
.body(requestbody.tostring())
.timeout(5000)
.execute()
.body();
jsonobject result = jsonutil.parseobj(response);
integer errcode = result.getint("errcode");
return errcode == null || errcode == 0;
} catch (exception e) {
log.error("调用微信发送消息接口异常", e);
return false;
}
}
}4.5 提供 rest 接口
@restcontroller
@requestmapping("/api/subscribe")
public class subscribecontroller {
@autowired
private subscribemessageservice subservice;
/**
* 前端需要订阅消息推送
*/
@postmapping("/record")
public result record(@requestbody map<string, string> req) {
subservice.record(req.get("openid"),req.get("templateid"));
return result.success("订阅成功");
}
}五、小程序前端(简单示例)
获取 openid 的标准流程:wx.login 获取 code → 传给后端 → 后端调用 jscode2session 接口换取 openid。
page({
subscribe() {
const templateid = '你的模板id'; // 与后端配置一致
wx.requestsubscribemessage({
tmplids: [templateid],
success(res) {
if (res[templateid] === 'accept') {
// 用户同意,上报后端
wx.request({
url: 'https://你的域名/api/subscribe/record',
method: 'post',
data: { openid: '当前用户的openid' }, // openid 需提前通过 wx.login 获取
success() { wx.showtoast({ title: '订阅成功' }); }
});
}
}
});
}
});六、踩坑经验与常见问题
| 错误码 | 含义 | 解决方案 |
|---|---|---|
43101 | 用户拒绝接收 | 用户未订阅或订阅已过期,需要前端重新引导订阅 |
40037 | template_id 无效 | 检查模板id是否正确,且已在后台添加到“我的模板” |
40003 | openid 无效 | 确认 openid 与当前小程序 appid 匹配 |
48001 | api 未授权 | 调用了公众号的接口?检查 url 是否正确 |
access_token 失效(40001) | token 过期 | 检查缓存刷新逻辑,确保提前200秒刷新 |
其他注意点:
真机调试时,
wx.requestsubscribemessage必须在用户点击事件中同步调用,不能异步(比如在settimeout里调用会失败)。开发者工具模拟器可能无法弹出授权窗口,请用手机真机预览测试。
一次性订阅:用户授权一次,你只能发一条消息。如果需要多次发送,需每次重新授权或申请长期订阅。
七、总结
小程序订阅消息是实现服务端主动推送的官方推荐方式。核心要点:
用户授权是前提:前端必须调用
wx.requestsubscribemessage且用户同意。后端缓存 access_token:避免频繁调用微信接口。
记录用户订阅状态:发送前检查,减少无效调用。
错误处理:针对常见错误码(43101、40037)做友好提示。
ok,结束。
到此这篇关于微信小程序订阅消息推送(java spring boot+redis)的文章就介绍到这了,更多相关微信小程序订阅消息推送内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论