springboot优雅的实现微信小程序订阅功能

微信小程序订阅功能实现

基础环境

  1. jdk8
  2. springboot2.7
  3. 微信开发者工具

前端用户订阅实现

小程序用户订阅接口官网教程

根据官方给出的wx.requestSubscribeMessage接口文档,不难写出以下代码:

subscribe(){
	wx.requestSubscribeMessage({
		tmplIds: ['模板id,获取方式见下文'],
		success : (res) => { 

			if(res['模板id,获取方式见下文'] == "accept") {

				subscribe(); //订阅成功后要干的事
				
			}

		},

	})
}

后端发送订阅信息

服务端发送订阅信息接口官网教程

根据官方文档的描述,服务端通过发送一个https请求将订阅模板发送至用户微信。那么我们面临以下问题:

  1. springboot中怎么发送http(s)请求
  2. 官网给的接口需要传递的参数如何获取(处理)
  3. 发送成功后返回的结果如何处理

前置准备

依赖准备
  1. 使用jdk的java.net包发送http请求。太麻烦,还得自己封装工具类
  2. 本文使用第三方工具类发请求🎉️,如下:
//maven引入工具包,使用糊涂工具包发送http请求,官方教程:https://hutool.cn/docs/#/http/Http%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%B7%A5%E5%85%B7%E7%B1%BB-HttpUtil
<!--糊涂工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.5</version>
        </dependency>

  <!--        lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

 <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.23</version>
        </dependency>
微信小程序配置类
/**
 * wx配置属性,
 *
 **/
@Data 
@Configuration
@ConfigurationProperties(prefix = "wx")
public class WxConfigProperties {

    private String url;

    /**
     * wx appId
     */
    private String appId;

    /**
     *
     */
    private String appSecret;


}

//#小程序配置,在application.yml文件配置
//wx:
//  url: https://api.weixin.qq.com/sns/jscode2session
//  appId: 你自己的
//  appSecret: 你自己的
订阅模板获取

官网地址

本文测试的模板如下
image.png

开始编码

根据小程序官方的教程,发送订阅信息需要如下参数:

image.png

请求头JSON字符串的构建

使用构建者模式完成json参数的构建:

/**
 * author: xlf
* 构建请求json参数抽象类,根据自己的需求继承,编写实现类
 */
public abstract class AbstractTemplateBuilder {


    protected JSONObject product = new JSONObject();


    /**
     *跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
     */
    public void buildMiniprogramState(){
        product.put("miniprogram_state","developer");
    }

    /**
     * 进入小程序查看的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN
     */
    public void buildLang(){
        product.put("lang","zh_CN");
    }

    /**
     * 模板id
     */
    public abstract void buildTemplateId();

    /**
     * 订阅的人
     */
    public abstract void buildOpenId();

    /**
     * 跳转的页面
     */
    public abstract void buildPage();


    /**
     * 模板消息
     */
    public abstract void buildData();

    /**
     * 获取模板
     * @return
     */
    public JSONObject construct(){
        buildTemplateId();
        buildPage();
        buildOpenId();
        buildData();
        buildMiniprogramState();
        buildLang();
        return product;
    }

    /**
     * data构建者
     * @return
     */
    public DataBuilder DataBuilder(){
        DataBuilder dataBuilder = new DataBuilder();
        return dataBuilder;
    }

    /**
     *
     */
    protected class DataBuilder {

        private JSONObject data;

        public DataBuilder(){
            data = new JSONObject();
        }

        public DataBuilder addParam(String key, String value){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("value",value);
            data.put(key,jsonObject);
            return this;
        }

        public JSONObject build(){
            return data;
        }
    }
}
/**
 * 构建参数具体实现类,照猫画虎,需要根据自己的业务逻辑更改
 * author: xlf
 */
public class TalkCommentTemplateBuilderImpl extends AbstractTemplateBuilder {


//配合自己的业务逻辑传递更改实体类
    private TalkCommentDTO talkComment;

    public TalkCommentTemplateBuilderImpl(TalkCommentDTO talkComment){
        super();
        this.talkComment = talkComment;
    }

    /**
     * 订阅模板
     */
    @Override
    public void buildTemplateId() {
        product.put("template_id","w6J_8bSEtK_2Tnscws-HWKJ_SxG0ANCQIcOZwK-AsWw");
    }

    /**
     * 订阅的人
     */
    @Override
    public void buildOpenId() {
        product.put("touser",talkComment.getTouser());
    }

    /**
     * 点击订阅消息跳转的页面
     */
    @Override
    public void buildPage() {
        product.put("page","/pages/chatDetail/chatDetail?id="+talkComment.getTalkId());
    }

    /**
     * 模板消息,配合自己的模板构建data数据。
     */
    @Override
    public void buildData() {
        JSONObject data = DataBuilder()
                .addParam("thing1", talkComment.getTalkContent()) //帖子内容
                .addParam("thing4", talkComment.getNickName()) //评论用户
                .addParam("thing2", talkComment.getContent()) //评论内容
                .addParam("time3", talkComment.getData()) //评论的时间
                .build();
        product.put("data",data);
    }
}
access_token的获取方法

获取接口调用凭据,获取方法:官方入口

照猫画虎,根据官方的文档写代码

image.png

/*
    获取token的url
     */
    private String tokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=";

  /**
     * 获取AccessToken
     * @return
     */
    private String getAccessToken() {

        String url  = tokenUrl  + wxConfigProperties.getAppId() + "&secret=" + wxConfigProperties.getAppSecret();


//        access_token 的有效期通过返回的 expires_in 来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。
//        在刷新过程中,中控服务器可对外继续输出的老 access_token,此时公众平台后台会保证在5分钟内,新老 access_token 都可用,这保证了第三方业务的平滑过渡
        return redisCache.queryWithMutexAndBuild(ASSESS_TOKEN_CACHE, (k) -> redisCache.getCacheObject(k), (k) -> {
            AccessToken accessToken = JSON.parseObject(HttpUtil.get(url), AccessToken.class);
            log.info("{} + accessToken:{}",url,accessToken);
            redisCache.setCacheObject(k, accessToken.getAccess_token(), accessToken.getExpires_in(), TimeUnit.SECONDS);
            return accessToken.getAccess_token();
        });
    }
/**
     * 获取AccessToken
     * @return
     */
    private String getAccessToken() {

        String url  = tokenUrl  + wxConfigProperties.getAppId() + "&secret=" + wxConfigProperties.getAppSecret();
        AccessToken accessToken = JSON.parseObject(HttpUtil.get(url), AccessToken.class);
        log.info("{} + accessToken:{}",url,accessToken);
    
        return accessToken.getAccess_token();
    }

//返回结果的实体类dto
 @Data
    public class AccessToken implements Serializable {

        private String access_token;
        private int expires_in;
    }
access_token的获取优化

官方给的建议:

image.png

翻译一下就是:不用每次订阅的时候都去获取一次access_token,access_token的有效期是2个小时,所以可以复用之前的token
我做的优化就是使用redis储存获取的token,然后每次获取token的时候先看redis是否存在缓存,如果存在就返回,如果不存在就重新发请求获取并且把它丢到redis缓存中。整个过程要考虑缓存穿透,缓存击穿,缓存雪崩等问题。以下代码需要引入redisTemplate和redission分布式锁,适合有一定基础的同学

/**
     * 优化后的代码获取AccessToken
     * @return
     */
    private String getAccessToken() {
        String url  = tokenUrl  + wxConfigProperties.getAppId() + "&secret=" + wxConfigProperties.getAppSecret();

//        access_token 的有效期通过返回的 expires_in 来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。
//        在刷新过程中,中控服务器可对外继续输出的老 access_token,此时公众平台后台会保证在5分钟内,新老 access_token 都可用,这保证了第三方业务的平滑过渡
        return redisCache.queryWithMutexAndBuild(ASSESS_TOKEN_CACHE, (k) -> redisCache.getCacheObject(k), (k) -> {
            AccessToken accessToken = JSON.parseObject(HttpUtil.get(url), AccessToken.class);
            log.info("{} + accessToken:{}",url,accessToken);
            redisCache.setCacheObject(k, accessToken.getAccess_token(), accessToken.getExpires_in(), TimeUnit.SECONDS);
            return accessToken.getAccess_token();
        });
    }
/** 
     *author: xlf
     * 从缓存拿数据,没有就通过obj重构缓存,返回R
     * @param key 缓存key
     * @param queryByRedis 从redis查询数据
     * @param buildRedis 构建缓存
     * @param <R>
     * @return
     */
    public <R> R queryWithMutexAndBuild(
            String key,Function<String, R> queryByRedis, Function<String,R> buildRedis)
    {
       //缓存存在
        if(exist(key)){
            return queryByRedis.apply(key);
        }

        //缓存不存在
        String lockKey = LOCK_KEY + key;
        RLock lock = redissonClient.getLock(lockKey);
        //加锁防止大量数据打到数据库 缓存击穿
        lock.lock();// 加锁,拿不到锁会一直自旋等待
        //redisson加的锁默认过期时长是30s,没过10s续一次
        //redisson不会死锁,因为有过期时间
        try {  // 获取锁成功,重构缓存
            if (!exist(key)) { // double check
                //重构缓存并且返回
                return buildRedis.apply(key);
            }
            return queryByRedis.apply(key);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            lock.unlock();
        }
    }

/**
     * 判断某个key是否存在
     * @param key
     * @return
     */
    public boolean exist(String key){

        Boolean flag = false;
        try{
            flag = redisTemplate.hasKey(key);
            return flag;
        }catch(Exception e){
            e.printStackTrace();
        }
        return flag;
    }
订阅模板的发送
/*
    发订阅内容的url
     */
    private  String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=";


/**
     * 推送
     * @param access_token token,传递第二步获取的access_token
     * @param params 请求体参数,传递第一步构建出来的json参数
     */
    public void sead(String access_token,JSONObject params){

//使用糊涂工具包发送http请求
        String res = HttpUtil.post(url + access_token,params.toJSONString());

        SendRes sendRes = JSON.parseObject(res, SendRes.class);

        log.info("成功发送:{}",sendRes);
        switch (sendRes.errcode){
            case 40001:
                throw new RuntimeException("获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口");
            case 40003:
                throw new RuntimeException("不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID");
            case 40014:
                throw new RuntimeException("不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口");
            case 40037:
                throw new RuntimeException("不合法的 template_id");

        }
    }

    /**
     * 返回结果
     */
    @Data
    private class SendRes implements Serializable{

        private Integer errcode;

        private String errmsg;
    }
并发量高时的建议

使用线程池或引入消息队列(如rabbitmq)进行异步处理。

access_token的获取应当参照优化后的代码进行处理。

原文

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值