最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

微信小程序訂閱消息推送實(shí)戰(zhàn)圖文教程(Java?Spring?Boot?+?Redis)

 更新時(shí)間:2026年04月14日 10:02:21   作者:晴天sir  
訂閱消息是微信小程序提供的一種消息推送方式,用戶(hù)可以訂閱某個(gè)公眾號(hào)或小程序的消息,當(dāng)有新消息時(shí),系統(tǒng)會(huì)自動(dòng)推送通知給用戶(hù),這篇文章主要介紹了微信小程序訂閱消息推送(Java Spring Boot+Redis)的相關(guān)資料,需要的朋友可以參考下

前言

最近在做“村民意見(jiàn)反饋”小程序,需要實(shí)現(xiàn):村民提交意見(jiàn)后,網(wǎng)格員能立刻收到微信通知。微信小程序提供了“訂閱消息”能力,用戶(hù)授權(quán)一次后,服務(wù)端就能主動(dòng)推送消息。本文將完整記錄從申請(qǐng)模板、后端開(kāi)發(fā)到前端調(diào)試的全過(guò)程,并提供可直接運(yùn)行的代碼。

首先說(shuō)一下微信小程序關(guān)于額度以及訂閱消息的簡(jiǎn)略信息(消息訂閱相關(guān)信息):

微信小程序的訂閱消息發(fā)送額度規(guī)則如下:

對(duì)于一次性訂閱消息,用戶(hù)每次授權(quán)僅可觸發(fā)一次消息發(fā)送,無(wú)總量限制,但必須在用戶(hù)授權(quán)后立即使用。
對(duì)于長(zhǎng)期訂閱消息,需滿(mǎn)足特定條件(如政務(wù)、醫(yī)療、交通等公共服務(wù)場(chǎng)景)并申請(qǐng)?zhí)厥饽0?,?jīng)平臺(tái)審核后方可使用,普通企業(yè)主體通常無(wú)法申請(qǐng)。
目前,微信平臺(tái)不對(duì)企業(yè)主體的小程序設(shè)置獨(dú)立的訂閱消息總量額度。只要用戶(hù)完成授權(quán),且消息內(nèi)容符合模板規(guī)范,即可發(fā)送。發(fā)送成功率主要取決于以下因素:

  • 用戶(hù)是否已授權(quán)對(duì)應(yīng)模板ID的消息訂閱。
  • 消息內(nèi)容是否符合模板字段要求。
  • 是否在用戶(hù)授權(quán)后的有效時(shí)間內(nèi)調(diào)用發(fā)送接口(通常為7天內(nèi))。
    因此,企業(yè)小程序的訂閱消息發(fā)送能力主要由用戶(hù)授權(quán)行為驅(qū)動(dòng),而非平臺(tái)分配的固定額度。

一、為什么需要訂閱消息?

微信早期有“模板消息”,但限制較多且容易騷擾用戶(hù)。后來(lái)推出了訂閱消息,核心特點(diǎn)是:

  • 用戶(hù)主動(dòng)訂閱:每次發(fā)送前必須獲得用戶(hù)授權(quán)(一次性訂閱)或長(zhǎng)期授權(quán)(長(zhǎng)期訂閱)。

  • 服務(wù)端主動(dòng)推送:用戶(hù)授權(quán)后,你可以在業(yè)務(wù)觸發(fā)時(shí)(如訂單狀態(tài)變更、意見(jiàn)處理)向用戶(hù)推送服務(wù)通知。

適合場(chǎng)景:訂單提醒、物流通知、政務(wù)辦事進(jìn)度、意見(jiàn)處理反饋等。

二、前置準(zhǔn)備

2.1 小程序賬號(hào)與類(lèi)目

  • 注冊(cè)小程序

  • 訂閱消息對(duì)類(lèi)目基本無(wú)限制(一次性訂閱),但長(zhǎng)期訂閱只對(duì)政務(wù)、醫(yī)療、交通等民生類(lèi)目開(kāi)放。
    本文以一次性訂閱為例,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的“意見(jiàn)提交 → 通知網(wǎng)格員”場(chǎng)景。

2.2 申請(qǐng)訂閱消息模板

1.登錄小程序后臺(tái) → 功能 → 訂閱消息 → 從公共模板庫(kù)添加模板(或自定義模板)。

2.選擇一個(gè)適合的模板,例如“監(jiān)理報(bào)告提交通知”,模板字段可能包含:

  • 1.服務(wù)名稱(chēng)

    {{thing1.DATA}}

  • 2.檢測(cè)結(jié)果

    {{thing2.DATA}}

  • 3.提交時(shí)間

    {{time3.DATA}}

3.記下模板ID。

2.3 后端技術(shù)選型

  • Spring Boot 2.x

  • Redis:緩存 access_token 和用戶(hù)訂閱狀態(tài)

  • Hutool:簡(jiǎn)化 HTTP 請(qǐng)求和 JSON 處理

  • JDK 8+

三、整體流程(看圖理解)

text

用戶(hù)(網(wǎng)格員)進(jìn)入小程序
    │
    ├─ 點(diǎn)擊“訂閱消息”按鈕
    │     └─ wx.requestSubscribeMessage() 彈窗授權(quán)
    │           └─ 允許 → 前端調(diào)用后端接口 /subscribe/record
    │                       └─ 后端存儲(chǔ) openId + templateId(Redis,30天有效期)
    │
村民提交意見(jiàn)
    │
    ├─ 后端根據(jù)業(yè)務(wù)找到對(duì)應(yīng)的網(wǎng)格員 openId
    ├─ 檢查該 openId 是否已訂閱(Redis 查詢(xún))
    ├─ 若已訂閱 → 獲取 access_token(帶緩存的)
    ├─ 調(diào)用微信發(fā)送消息接口 https://api.weixin.qq.com/cgi-bin/message/subscribe/send
    └─ 網(wǎng)格員在微信“服務(wù)通知”中收到消息

四、后端核心實(shí)現(xiàn)(Java)

獲取APPID以及secret以及templateId(模板id)

4.1 配置類(lèi):配置 appid / secret / templateId

#小程序appid
wechat.appid=xxxxx
#小程序密鑰
wechat.secret=xxxxxx
#訂閱消息的模板id
wechat.templateId=xxxxxxxxx
#登錄wx登錄驗(yàn)證
wechat.loginUrl=https://api.weixin.qq.com/sns/jscode2session
#wx訂閱消息發(fā)送
wechat.sendUrl=https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=

4.2 Redis 工具類(lèi)(簡(jiǎn)化版)

@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,有效期設(shè)為 7000 秒(微信是7200秒)
            redisUtils.setWechat(TOKEN_KEY, token, expiresIn - 200);
            return token;
        } catch (Exception e) {
            throw new CommonException("獲取AccessToken網(wǎng)絡(luò)異常", e);
        }
    }
}

4.4 發(fā)送訂閱消息

@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;
    /**
     * 記錄用戶(hù)訂閱
     */
    public void record() {
        // 獲取登錄用戶(hù)獲取對(duì)應(yīng)的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); // 存儲(chǔ)30天
        } catch (Exception e) {
            throw new CommonException(e.getMessage());
        }
    }
    /**
     * 判斷用戶(hù)是否訂閱
     */
    public boolean isSubscribed(String openId, String templateId) {
        String key = SUBSCRIBE_PREFIX + templateId + ":" + openId;
        return "1".equals(redisUtils.getWechat(key));
    }
    private void getUserListByGridId(String openId) {
        // 此處代碼可替換為具體的業(yè)務(wù)實(shí)現(xiàn)  就是獲取需要推送用戶(hù)的openId
        // 推送消息給網(wǎng)格員
        // 判斷用戶(hù)是否訂閱了消息
        if (isSubscribed(openId, templateId)) {
            Map<String, String> dataMap = new HashMap<>();
            dataMap.put("thing1", "意見(jiàn)提醒");
            dataMap.put("thing2", "這是一條測(cè)試信息");
            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("推送消息失敗,請(qǐng)重試");
            }
        }
    }
    /**
     * 發(fā)送訂閱消息
     */
    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("調(diào)用微信發(fā)送消息接口異常", 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("訂閱成功");
    }
}

五、小程序前端(簡(jiǎn)單示例)

獲取 openId 的標(biāo)準(zhǔn)流程:wx.login 獲取 code → 傳給后端 → 后端調(diào)用 jscode2session 接口換取 openId。

Page({
  subscribe() {
    const templateId = '你的模板ID'; // 與后端配置一致
    wx.requestSubscribeMessage({
      tmplIds: [templateId],
      success(res) {
        if (res[templateId] === 'accept') {
          // 用戶(hù)同意,上報(bào)后端
          wx.request({
            url: 'https://你的域名/api/subscribe/record',
            method: 'POST',
            data: { openId: '當(dāng)前用戶(hù)的openId' }, // openId 需提前通過(guò) wx.login 獲取
            success() { wx.showToast({ title: '訂閱成功' }); }
          });
        }
      }
    });
  }
});

六、踩坑經(jīng)驗(yàn)與常見(jiàn)問(wèn)題

錯(cuò)誤碼含義解決方案
43101用戶(hù)拒絕接收用戶(hù)未訂閱或訂閱已過(guò)期,需要前端重新引導(dǎo)訂閱
40037template_id 無(wú)效檢查模板ID是否正確,且已在后臺(tái)添加到“我的模板”
40003openId 無(wú)效確認(rèn) openId 與當(dāng)前小程序 appid 匹配
48001API 未授權(quán)調(diào)用了公眾號(hào)的接口?檢查 URL 是否正確
access_token 失效(40001)token 過(guò)期檢查緩存刷新邏輯,確保提前200秒刷新

其他注意點(diǎn)

  • 真機(jī)調(diào)試時(shí),wx.requestSubscribeMessage 必須在用戶(hù)點(diǎn)擊事件中同步調(diào)用,不能異步(比如在 setTimeout 里調(diào)用會(huì)失?。?。

  • 開(kāi)發(fā)者工具模擬器可能無(wú)法彈出授權(quán)窗口,請(qǐng)用手機(jī)真機(jī)預(yù)覽測(cè)試。

  • 一次性訂閱:用戶(hù)授權(quán)一次,你只能發(fā)一條消息。如果需要多次發(fā)送,需每次重新授權(quán)或申請(qǐng)長(zhǎng)期訂閱。

七、總結(jié)

小程序訂閱消息是實(shí)現(xiàn)服務(wù)端主動(dòng)推送的官方推薦方式。核心要點(diǎn):

  1. 用戶(hù)授權(quán)是前提:前端必須調(diào)用 wx.requestSubscribeMessage 且用戶(hù)同意。

  2. 后端緩存 access_token:避免頻繁調(diào)用微信接口。

  3. 記錄用戶(hù)訂閱狀態(tài):發(fā)送前檢查,減少無(wú)效調(diào)用。

  4. 錯(cuò)誤處理:針對(duì)常見(jiàn)錯(cuò)誤碼(43101、40037)做友好提示。

ok,結(jié)束。

到此這篇關(guān)于微信小程序訂閱消息推送(Java Spring Boot+Redis)的文章就介紹到這了,更多相關(guān)微信小程序訂閱消息推送內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

崇州市| 鹤岗市| 射阳县| 永济市| 揭阳市| 屏边| 鄂托克旗| 修水县| 安阳县| 栾城县| 瓦房店市| 日土县| 曲麻莱县| 牟定县| 成都市| 海阳市| 甘泉县| 乌什县| 屯留县| 滕州市| 雅安市| 个旧市| 寻甸| 布尔津县| 临泽县| 通化市| 岳阳市| 吉首市| 嘉善县| 金溪县| 栖霞市| 绥德县| 武冈市| 蕲春县| 双鸭山市| 农安县| 喜德县| 聂荣县| 饶平县| 布尔津县| 盐边县|