java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > 微信小程序订阅消息推送

微信小程序订阅消息推送实战图文教程(Java Spring Boot + Redis)

作者:晴天sir

订阅消息是微信小程序提供的一种消息推送方式,用户可以订阅某个公众号或小程序的消息,当有新消息时,系统会自动推送通知给用户,这篇文章主要介绍了微信小程序订阅消息推送(Java Spring Boot+Redis)的相关资料,需要的朋友可以参考下

前言

最近在做“村民意见反馈”小程序,需要实现:村民提交意见后,网格员能立刻收到微信通知。微信小程序提供了“订阅消息”能力,用户授权一次后,服务端就能主动推送消息。本文将完整记录从申请模板、后端开发到前端调试的全过程,并提供可直接运行的代码。

首先说一下微信小程序关于额度以及订阅消息的简略信息(消息订阅相关信息):

微信小程序的订阅消息发送额度规则如下:

对于一次性订阅消息,用户每次授权仅可触发一次消息发送,无总量限制,但必须在用户授权后立即使用。
对于长期订阅消息,需满足特定条件(如政务、医疗、交通等公共服务场景)并申请特殊模板,经平台审核后方可使用,普通企业主体通常无法申请。
目前,微信平台不对企业主体的小程序设置独立的订阅消息总量额度。只要用户完成授权,且消息内容符合模板规范,即可发送。发送成功率主要取决于以下因素:

一、为什么需要订阅消息?

微信早期有“模板消息”,但限制较多且容易骚扰用户。后来推出了订阅消息,核心特点是:

适合场景:订单提醒、物流通知、政务办事进度、意见处理反馈等。

二、前置准备

2.1 小程序账号与类目

2.2 申请订阅消息模板

1.登录小程序后台 → 功能 → 订阅消息 → 从公共模板库添加模板(或自定义模板)。

2.选择一个适合的模板,例如“监理报告提交通知”,模板字段可能包含:

3.记下模板ID

2.3 后端技术选型

三、整体流程(看图理解)

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用户拒绝接收用户未订阅或订阅已过期,需要前端重新引导订阅
40037template_id 无效检查模板ID是否正确,且已在后台添加到“我的模板”
40003openId 无效确认 openId 与当前小程序 appid 匹配
48001API 未授权调用了公众号的接口?检查 URL 是否正确
access_token 失效(40001)token 过期检查缓存刷新逻辑,确保提前200秒刷新

其他注意点

七、总结

小程序订阅消息是实现服务端主动推送的官方推荐方式。核心要点:

  1. 用户授权是前提:前端必须调用 wx.requestSubscribeMessage 且用户同意。

  2. 后端缓存 access_token:避免频繁调用微信接口。

  3. 记录用户订阅状态:发送前检查,减少无效调用。

  4. 错误处理:针对常见错误码(43101、40037)做友好提示。

ok,结束。

到此这篇关于微信小程序订阅消息推送(Java Spring Boot+Redis)的文章就介绍到这了,更多相关微信小程序订阅消息推送内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文