Java微信小程序获取关联公众号用户是否已关注标识


前言

如果公众号不是认证的服务号就无需看这篇文章,需要先认证,可以参考这篇文章五分钟!手把手教你快速完成微信公众号认证!

你们是否经常在小程序看到这种关注标识
在这里插入图片描述
实现的方法很简单,后端只需要返回给前端给前端当前用户是否有关注公众号就行了,整个逻辑很简单,微信官方也有对应的API,但实际上整个流程就很复杂


一、绑定相关数据

微信官方有很多限制,咱们需要做好一些关联

1.小程序绑定公众号

根据微信官方文档,微信小程序需要关联对应的公众号才能够引导关注公众号
咱们先登录微信公众平台,扫码登录公众号
在这里插入图片描述
然后点击【广告与服务】->【小程序管理】添加关联小程序,这步很简单只需要后台管理员扫码一下就行了。
在这里插入图片描述

2.小程序和公众号关联微信开放平台

这一步比较重要,这是为了获取unionId,关于unionId介绍可以看一下官方说明,UnionID 机制说明,简单来说就是同一个微信开放平台用户的unionId都是唯一的

先注册微信开放平台,然后关联小程序和公众号,关联流程参考文章微信开放平台绑定公众号,小程序也同理,这个时候你们需要改造原来的微信登录接口,会发现登录接口返回的参数多了unionId,把对应用户的unionId存到数据库里,后面会用得上。

二、公众号操作回调

这个是公众号的相关操作会有回调,相当强大,咱们服务器能立马得知用户做了什么操作,这里咱们只需要配公众号关注取消关注的回调。

1.服务器配置

同样登录微信公众平台,选择公众号账号登录,进入【服务与配置】->【基本配置】,会看到有一个服务器配置
在这里插入图片描述
点击修改配置
在这里插入图片描述
这个URL实际上就是咱们要给微信的回调地址,需要外网可访问;
Token咱们随意填写,只要跟服务器对得上;
EncodingAESKey随机生成就好,然后消息加解密选择用明文模式,安全模式可以看看别人写的加解密方式;

2.回调接口(URL)

RequestMethodEnum枚举类是为了判断什么请求方式,如果是get请求实际上就是验证token合法性

public enum RequestMethodEnum {
    /**
     * get
     */
    GET(1,"GET"),
    /**
     * post
     */
    POST(2,"POST");

    private final Integer code;
    private final String desc;

    RequestMethodEnum(Integer id, String name){
        this.code = id;
        this.desc = name;
    }

    public Integer getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

MsgTypeEnum枚举类是为了判断微信公众号执行了什么操作

public enum MsgTypeEnum {
    /**
     * 关注/取消关注事件
     */
    EVENT(1, "event");

    private final Integer code;
    private final String desc;

    MsgTypeEnum(Integer id, String name){
        this.code = id;
        this.desc = name;
    }

    public Integer getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

checkSignature验证回调地址是否合法的工具方法

public static boolean checkSignature(String signature, String timestamp,String nonce, String token) {
        // 1.将token、timestamp、nonce三个参数进行字典序排序
        String[] arr = new String[]{token, timestamp, nonce};
        Arrays.sort(arr);
        // 2. 将三个参数字符串拼接成一个字符串进行sha1加密
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }
        MessageDigest md = null;
        String tmpStr = null;
        try {
            md = MessageDigest.getInstance("SHA-1");
            // 将三个参数字符串拼接成一个字符串进行sha1加密
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = byteToStr(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        content = null;
        // 3.将sha1加密后的字符串可与signature对比,标识该请求来源于微信
        log.info("tmpStr:{},signature:{}",tmpStr,signature.toUpperCase());
        return tmpStr != null && tmpStr.equals(signature.toUpperCase());
    }

然后验证完整方法如下

public void callback(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String method = request.getMethod();
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        if (RequestMethodEnum.GET.getDesc().equals(method)) {
            //验证回调地址
            String signature = request.getParameter("signature");
            String timestamp = request.getParameter("timestamp");
            String nonce = request.getParameter("nonce");
            String echostr = request.getParameter("echostr");
            boolean checkSignature = WechatOffiAccountUtil.checkSignature(signature, timestamp, nonce, token); // 这个token要跟服务器配置的token一致
            if (checkSignature) {
                log.info("签名验证成功");
                response.getWriter().write(echostr);
            }
        }
}

写好接口后,回去填写服务器配置,就会验证成功。


三、获取微信公众号用户是否已关注标识

1.获取公众号accessToken

咱们要先清楚,小程序的accessToken和公众号的accessToken是不同的,所以公众号的accessToken要重新获取一下,先查阅一下微信公众号API获取 Access token
Java获取方法如下:

    public String getOfficialAccessToken() {
        String accessToken = redisCache.getCacheObject("official_access_token"); // 这里会用到redisCache工具
        if (Objects.isNull(accessToken)) {
            String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + OFFICIAL_APP_ID + "&secret=" + OFFICIAL_SECRET;

            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
            String responseBody = response.getBody();

            // 解析返回结果,提取 access_token 字段的值
            accessToken = parseAccessToken(responseBody);
            redisCache.setCacheObject("official_access_token", accessToken, 1, TimeUnit.HOURS);
        }
        if(!isAccessTokenValid(accessToken)){ // 这是为了解析accessToken是否还有效,因为官方的过期时间会变动
            String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + OFFICIAL_APP_ID + "&secret=" + OFFICIAL_SECRET;

            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
            String responseBody = response.getBody();

            // 解析返回结果,提取 access_token 字段的值
            accessToken = parseAccessToken(responseBody);
            redisCache.setCacheObject("official_access_token", accessToken, 1, TimeUnit.HOURS);
        }
        return accessToken;
    }
    
	private String parseAccessToken(String responseBody) {
	        JSONObject jsonObject = new JSONObject(responseBody);
	        return jsonObject.getString("access_token");
	    }

	public boolean isAccessTokenValid(String accessToken) {
	        String url = "https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=" + accessToken;
	
	        RestTemplate restTemplate = new RestTemplate();
	        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
	        String responseBody = response.getBody();
	
	        // 解析返回结果,提取错误信息
	        String errorCode = parseErrorCode(responseBody);
	
	        // 如果错误码为空,则表示 access_token 有效;否则,表示无效
	        return errorCode == null;
	    }

2.feign远程调用微信公众号获取用户基本信息(UnionID机制)

远程调用我是用了feign,这样可以节省很多建远程调用步骤,然后查阅一下微信公众号API获取用户基本信息(UnionID机制)
请求结果实体类

@Data
public class PublicUserInfo {
    /**
     * 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。
     */
    private int subscribe;

    /**
     * 用户的标识,对当前公众号唯一
     */
    private String openid;

    /**
     * 用户的语言,简体中文为zh_CN
     */
    private String language;

    /**
     * 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间
     */
    private long subscribe_time;

    /**
     * 只有在用户将公众号绑定到微信开放平台账号后,才会出现该字段。
     */
    private String unionid;

    /**
     * 公众号运营者对粉丝的备注,公众号运营者可在微信公众平台用户管理界面对粉丝添加备注
     */
    private String remark;

    /**
     * 用户所在的分组ID(兼容旧的用户分组接口)
     */
    private int groupid;

    /**
     * 用户被打上的标签ID列表
     */
    private List<Integer> tagid_list;

    /**
     * 返回用户关注的渠道来源
     */
    private String subscribe_scene;

    /**
     * 二维码扫码场景(开发者自定义)
     */
    private int qr_scene;

    /**
     * 二维码图片
     */
    private String qr_scene_str;
    private String qrSceneImage;

    // 添加构造方法、getter和setter等
}

编写Feign接口方法

@FeignClient(name = "officialAccount",url = "https://api.weixin.qq.com/cgi-bin")
public interface OfficialAccountFeign {
    @GetMapping("/user/info?access_token={accessToken}&openid={openId}&lang=zh_CN")
    ResponseEntity<PublicUserInfo> getUserInfo(@PathVariable("accessToken") String accessToken, @PathVariable("openId") String openId);
}

3.回调接口补充更新用户关注标识

补充上面写的callBack回调方法

    public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String method = request.getMethod();
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        if (RequestMethodEnum.GET.getDesc().equals(method)) {
            //验证回调地址
            String signature = request.getParameter("signature");
            String timestamp = request.getParameter("timestamp");
            String nonce = request.getParameter("nonce");
            String echostr = request.getParameter("echostr");
            boolean checkSignature = WechatOffiAccountUtil.checkSignature(signature, timestamp, nonce, token);
            if (checkSignature) {
                log.info("签名验证成功");
                response.getWriter().write(echostr);
            }
        } else {
            //接收回调xml;
            Document document = XmlUtil.readXML(request.getReader());

            String toUserName = (String) XmlUtil.getByXPath("//xml/ToUserName", document, XPathConstants.STRING);
            String fromUserName = (String) XmlUtil.getByXPath("//xml/FromUserName", document, XPathConstants.STRING);
            Long createTime = Long.valueOf((String) XmlUtil.getByXPath("//xml/CreateTime", document, XPathConstants.STRING));
            String msgType = (String) XmlUtil.getByXPath("//xml/MsgType", document, XPathConstants.STRING);

            log.info("FromUserName:{}", fromUserName);
            log.info("CreateTime:{}", createTime);
            log.info("MsgType:{}", msgType);
            if (MsgTypeEnum.EVENT.getDesc().equals(msgType)) {
                //是否事件为关注/取消关注事件
                String event = (String) XmlUtil.getByXPath("//xml/Event", document, XPathConstants.STRING);
                log.info("Event:{}", event);
                if (EventEnum.SUBSCRIBE.getDesc().equals(event)) {
                    //关注
                    //新增用户
                    Boolean insert = updateUserSubscribe(fromUserName, 1);
                    if (insert) {
                        log.info("新增关注用户成功");
                        response.getWriter().write("success");
                    }
                } else {
                    //取关
                    //更新用户状态
                    Boolean update = updateUserSubscribe(fromUserName, 0);
                    if (update) {
                        log.info("更新关注用户状态成功");
                        response.getWriter().write("success");
                    }
                }
            }
        }
    }
    
    private Boolean updateUserSubscribe(String fromUserName, Integer subscribe) {
        QueryWrapper<WechatUser> userQueryWrapper = new QueryWrapper<>(); // 这里用了mybatis-plus方法,匹配用户的公众号public_openid,匹配上就查出来,否则就根据unionId重新匹配
        userQueryWrapper.eq("public_openid", fromUserName);
        WechatUser user = this.getOne(userQueryWrapper);
        if (Objects.nonNull(user)){
            user.setSubscribe(subscribe);
            this.updateById(user);
            return true;
        }else{
            PublicUserInfo publicUserInfo = officialAccountFeign.getUserInfo(loginService.getOfficialAccessToken(), fromUserName).getBody();
            userQueryWrapper.clear();
            userQueryWrapper.eq("unionid", publicUserInfo.getUnionid());
            user = this.getOne(userQueryWrapper);
            if (Objects.nonNull(user)) {
                user.setPublicOpenid(fromUserName);
                this.updateById(user);
                return true;
            }
        }
        return false;
    }

到这里就大功告成了,先注册小程序账号,然后在公众号上点击关注和取消关注操作,关注数据库显示的内容。


总结

很多功能看似很简单实现,但是如果在第三方平台上开发,需要遵从官方的要求,才能把整个环节实现。

参考博客:ruoyi的springboot微信小程序登录实现方式(我自己写的,感兴趣的可以去看一下)