前一篇阐述了,判断用户是否关注服务号的逻辑实现及部分代码。今天说一下判断是否关注订阅号如何实现。
先回顾一下流程:
- 将订阅号绑定到服务号上。在一个服务号的不同应用,虽然openid不同,但unionid是一样的
- 通过微信提供的接口,将订阅号的用户同步到本地,比如mysql中。同步过来的数据只有openid。
- 通过调用接口查询出所有用户的unionid
- 订阅号配置服务。如果有用户订阅,取消订阅将信息推送给你的服务
- 用户通过服务号授权,可以获取到用户的unionid,拿关这个unionid去mysql查询,是否能查到。如果能查到说明已关注过了;如果查询不到,说明没有关注。
准备
首先要有服务号和订阅号,且将订阅号与服务号关联起来
然后建表用来存关注用户信息,我们简单一点(一看就明白)
CREATE TABLE `t_subscribe_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`open_id` varchar(555) NOT NULL,
`union_id` varchar(255) DEFAULT '',
`created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_x` (`union_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;;
获取订阅号用户信息
获取access_token
准备好订阅号的appid和secret,调用微信接口
/**
* access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。
* 开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。
* access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
* https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/Nontax_Bill/API_list.html#1.1
* @param grantType
* @param appid
* @return
*/
@RequestMapping(value = "/cgi-bin/token", method = RequestMethod.GET)
String getToken(@RequestParam("grant_type") String grantType,
@RequestParam("appid") String appid, @RequestParam("secret") String secret);
这里使用feign client, 依赖如下(springboot 2.7.15)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
这里需要特别注意的是,access_token这个需要缓存到本地的,比如内存,redis或MySql里,都可以。因access_token有效期是7200秒,且这个接口每天访问次数是有限制的。没必要用一次请求一次。记录下申请的时间,可以用job在快到期时刷新一下,或使用access_token时,快过期了(比如7100秒的时候),再申请一下。如果单实例的,更新时锁一下,避免多线程同时刷新(浪费次数);多实例的可以考虑使用全局锁。如果访问者不多,不锁也没啥,最多,多申请一次。
查询订阅用户信息
这是另一个接口了,官方提供的接口,一次可以获取到10000个用户和next_openid信息,这个next_openid是为了下次一请求使用,所以查询时,可以写一个递归一次性拉完数据并且在上面创建的表中。参考代码如下:
查询接口
/**
* 获取用户列表
* 公众号可通过本接口来获取账号的关注者列表,关注者列表由一串OpenID(加密后的微信号,
* 每个用户对每个公众号的OpenID是唯一的)组成。一次拉取调用最多拉取10000个关注者的OpenID,
* 可以通过多次拉取的方式来满足需求。
* https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_a_User_List.html
* @param token
* @param openId
* @return
*/
@RequestMapping(value = "/cgi-bin/user/get", method = RequestMethod.GET)
String getSubscribe(@RequestParam("access_token") String token,
@RequestParam("next_openid") String openId);
递归实现
/**
* 查询用户
* @param nextOpenid
*/
private void getNext(String nextOpenid) {
//这里缓存了,如果过期会自动更新,否则继续使用之前申请的
final AccessToken ac = weiXinService.getGlobalToken(subAppid, subSecret);
final String res = feignClient.getSubscribe(ac.getAccessToken(), nextOpenid);
final WeChatUser weChatUser = JSON.parseObject(res, WeChatUser.class);
if (weChatUser == null) {
return;
}
log.info("next openid {} ", weChatUser.getNext_openid());
final List<SubscribeUser> uses = weChatUser.getData().getOpenid().stream().map(openid -> {
SubscribeUser u = new SubscribeUser();
u.setOpenId(openid);
u.setCreatedDate(LocalDateTime.now());
return u;
}).collect(Collectors.toList());
this.subscribeUserMapper.insertBatch(uses, 0);
String next = weChatUser.getNext_openid();
if (StringUtils.isNotEmpty(next)) {
getNext(next);
}
}
2点说明:
- 代码中access_token是存在数据库里的
- 查询到用户创建DO后,使用insertBatch保存(并非一个一个保存,如果用户非常多话非常慢)。我使用的是Mybatis-flex。默认是1000条/次保存。
更新union_id
有了open id后,我们还要查询相应的union id。官方提供2个接口:单个查询和批量(100/次)查询使用的接口如下:
/**
* 批量查询
* @param accessToken
* @param json
* @return
*/
@RequestMapping(value = "/cgi-bin/user/info/batchget?access_token={token}", method = RequestMethod.POST)
String userInfoExtBatch(@PathVariable("token") String accessToken, @RequestBody String json);
依然要用access_token.
用户数据有了,接口也有了,我们可以查询DB中union id为空的,全查出来之后,分个组,然后调用这个接口,参考代码如下:
@Async
public void fillUnion() {
QueryWrapper wrapper = QueryWrapper.create()
.select()
.from("t_subscribe_user")
.where("union_id is null");
final List<SubscribeUser> allUsers = subscribeUserMapper.selectListByQuery(wrapper);
for (List<PhotoSubscribeUser> sub : Lists.partition(allUsers, 100)) {
QueryUnionIdBatch req = new QueryUnionIdBatch();
req.setOpenids(sub.stream().map(it -> it.getOpenId()).collect(Collectors.toList()));
req.setAccessToken("");
final QueryUnionIdRes unionId = getUnionId(req);
final Map<String, String> map = unionId.getUserInfoList().stream().collect(Collectors.toMap(QueryUnionIdRes.UserInfoListDTO::getOpenid, QueryUnionIdRes.UserInfoListDTO::getUnionid, (v1, v2) -> v1));
sub.forEach(u -> {
u.setUnionId(map.getOrDefault(u.getOpenId(), ""));
subscribeUserMapper.update(u);
});
}
}
- QueryUnionId类结构是按接口要求创建的。使用了异步方法。
- 将List切分成100个一块使用提guava提供的方法。
getUnionId代码如下:
/**
* 批量获取union id
* @param req
* @return
*/
public QueryUnionIdRes getUnionId(QueryUnionIdBatch req) {
final AccessToken ac = weiXinService.getGlobalToken(subAppid, subSecret);
String accessToken = ac.getAccessToken();
QueryUnionId q = new QueryUnionId();
final List<QueryUnionId.UserListDTO> data = req.getOpenids().stream().map(it -> new QueryUnionId.UserListDTO(it)).collect(Collectors.toList());
q.setUserList(data);
Map<String,String> header = new HashMap<>();
header.put("Content-Type","application/json");
header.put("Host","api.weixin.qq.com");
final String res = feignClient.userInfoExtBatch(accessToken, JSON.toJSONString(q));
log.info("union {}", res);
return JSON.parseObject(res, QueryUnionIdRes.class);
}
至此,用户信息与unionid已经准备完毕!
判断用户是否关注订阅号
核心代码是,构建授权请求(准备好正确的redirect)
redirect处理
参考代码如下:
/**
* 微信授权回调接口
*
* @param request
* @return
* @throws IOException
*/
@GetMapping("/auth")
@ResponseBody
public ApiResponse authCallBack(HttpServletRequest request) {
String code = request.getParameter("code");
String state = request.getParameter("state");
if (StringUtils.isEmpty(code)) {
return new ApiResponse().ofFailure(11, "code为空");
}
log.info("微信授权回调, code = {} ,state(race id) = {}", code, state);
try {
//获取微信授权access_token
final LocalAccessToken localAccessToken = weiXinService.getLocalAccessToken(code);
if (Objects.isNull(localAccessToken)) {
return new ApiResponse().ofFailure(11, "微信授权失败:");
}
log.info("微信授权access_token返回参数:{}", JSON.toJSONString(localAccessToken));
String accessToken = localAccessToken.getAccessToken();
String openId = localAccessToken.getOpenid();
String unionId = localAccessToken.getUnionId();
log.info("微信授权: " + "获取授权元数据:access token: {}, openid: {}, unionid: {}", accessToken, openId, unionId);
//如果微信的返回access_token为空
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(openId)) {
return new ApiResponse().ofFailure(11, "微信授权失败");
}
//这里需要改,使用上面unionid去库里查询,用户是否关注过“订阅号”
boolean hasSub = true;
//这里就是使用union id查询DB中是否有记录
hasSub = subscribeService.checkSubscribeByUnionid(unionId);
//业务处理
if (hasSub) {
log.info("用户授权登录成功跳转地址:{}", dest);
return new ApiResponse().ofSuccess("成功", race.getThirdUrl());
} else {
return new ApiResponse().ofFailure(10, "需要订阅公众号");
}
} catch (Exception e) {
log.info("#微信授权# 失败", e);
return new ApiResponse().ofFailure(11, "微信授权失败");
}
}
基本就是这么多。
订阅与取消订单
这时你可能会想,数据同步之后,如果有新用户关注了,DB中没有呀,不就出错了吗?确实如此,这时就用到订阅号事件推送的功能。
配置好,启用之后,只有事件都会通过/token这个入口推送过来,我们可以根据事件来增加或删除DB中的记录。推送过来的数据,会有用户的openid,如果是关注事件,就用这个openid再去查一下unionid,现将数据存到DB中。
获取单个union id的接口:
/**
* 用户信息
* @param token
* @param openId
* @return
*/
@RequestMapping(value = "/cgi-bin/user/info", method = RequestMethod.GET)
String userInfoExt(@RequestParam("access_token") String token,
@RequestParam("openid") String openId,
@RequestParam("lang") String lang);
写在最后
其实整个过程很简单,基本没有难点。按部就班即可。初之接触的人可能主要卡在,订阅号与服务号区别。还有什么时候用哪种类型的号。
最后的最后,如果你在google或baidu之后,一直没有找到解决的问题,突然发现此篇文章而且能够解决你的问题。你可以打个赏吗?