上篇博客讲述了Redis实现朋友圈,微博等Feed流功能,实现Feed流微服务的业务场景、实现思路和环境搭建,本文继续讲解具体的代码实现内容。
文章目录
添加 Feed 信息
FeedsController
/**
* 添加 Feed
*
* @param feeds
* @param access_token
* @return
*/@PostMappingpublicResultInfo<String>create(@RequestBodyFeeds feeds,String access_token){
feedsService.create(feeds, access_token);returnResultInfoUtil.buildSuccess(request.getServletPath(),"添加成功");}
FeedsService
/**
* 添加 Feed
*
* @param feeds
* @param accessToken
*/@Transactional(rollbackFor =Exception.class)publicvoidcreate(Feeds feeds,String accessToken){// 校验 Feed 内容不能为空,不能太长AssertUtil.isNotEmpty(feeds.getContent(),"请输入内容");AssertUtil.isTrue(feeds.getContent().length()>255,"输入内容太多,请重新输入");// 获取登录用户信息SignInUserInfo userInfo =loadSignInUserInfo(accessToken);// Feed 关联用户信息
feeds.setFkUserId(userInfo.getId());// 添加 Feedint count = feedsMapper.save(feeds);AssertUtil.isTrue(count ==0,"添加失败");// 推送到粉丝的列表中 -- 后续这里应该采用异步消息队列解决性能问题// 先获取粉丝 id 集合List<Integer> followers =findFollowers(userInfo.getId());// 推送 Feedlong now =System.currentTimeMillis();
followers.forEach(follower ->{String key =RedisKeyConstant.following_feeds.getKey()+ follower;
redisTemplate.opsForZSet().add(key, feeds.getId(), now);});}/**
* 获取粉丝 id 集合
*
* @param userId
* @return
*/privateList<Integer>findFollowers(Integer userId){String url = followServerName +"followers/"+ userId;ResultInfo resultInfo = restTemplate.getForObject(url,ResultInfo.class);if(resultInfo.getCode()!=ApiConstant.SUCCESS_CODE){thrownewParameterException(resultInfo.getCode(), resultInfo.getMessage());}List<Integer> followers =(List<Integer>) resultInfo.getData();return followers;}
FeedsMapper
/**
* 添加 Feed
* @param feeds
* @return
*/@Insert("insert into t_feeds (content, fk_user_id, praise_amount, "+" comment_amount, fk_restaurant_id, create_date, update_date, is_valid) "+" values (#{content}, #{fkUserId}, #{praiseAmount}, #{commentAmount}, #{fkRestaurantId}, "+" now(), now(), 1)")@Options(useGeneratedKeys =true, keyProperty ="id")intsave(Feeds feeds);
ms-follow 服务新增获取粉丝列表
FollowController
/**
* 获取粉丝列表
*
* @param userId
* @return
*/@GetMapping("followers/{userId}")publicResultInfofindFollowers(@PathVariableInteger userId){returnResultInfoUtil.buildSuccess(request.getServletPath(),
followService.findFollowers(userId));}
FollowService
/**
* 获取粉丝列表
*
* @param userId
* @return
*/publicSet<Integer>findFollowers(Integer userId){AssertUtil.isNotNull(userId,"请选择要查看的用户");Set<Integer> followers = redisTemplate.opsForSet().members(RedisKeyConstant.followers.getKey()+ userId);return followers;}
ms-gateway 服务配置网关路由
spring:application:name: ms-gateway
cloud:gateway:discovery:locator:enabled:true# 开启配置注册中心进行路由功能lower-case-service-id:true# 将服务名称转小写routes:# Feed服务路由 -id: ms-feeds
uri: lb://ms-feeds
predicates:- Path=/feeds/**filters:- StripPrefix=1
启动项目测试
- 先让id等于10、9、8的用户关注id等于7的用户。
- id等于7的用户登录后发布一条动态。
让 id=10 的用户关注 id=7 的用户:
让 id=9 的用户关注 id=7 的用户:
让 id=8 的用户关注 id=7 的用户:
id=7 的用户登录系统并发送一条动态:
http://localhost/feeds?access_token=48781f97-1c3a-4737-ae55-984c0944649e
查看数据库 feeds 信息:
查看 redis 中粉丝的 feeds 信息:
可以看到用户id为8、9、10的用户都收到了这条Feed。
删除 Feed 信息
FeedsController
/**
* 删除 Feed
*
* @param id
* @param access_token
* @return
*/@DeleteMapping("{id}")publicResultInfodelete(@PathVariableInteger id,String access_token){
feedsService.delete(id, access_token);returnResultInfoUtil.buildSuccess(request.getServletPath(),"删除成功");}
FeedsService
/**
* 删除 Feed
*
* @param id
* @param accessToken
*/@Transactional(rollbackFor =Exception.class)publicvoiddelete(Integer id,String accessToken){// 请选择要删除的 FeedAssertUtil.isTrue(id ==null|| id <1,"请选择要删除的Feed");// 获取登录用户SignInUserInfo userInfo =loadSignInUserInfo(accessToken);// 获取 Feed 内容Feeds feeds = feedsMapper.findById(id);// 判断 Feed 是否已经被删除且只能删除自己的 FeedAssertUtil.isTrue(feeds ==null,"该Feed已被删除");AssertUtil.isTrue(!feeds.getFkUserId().equals(userInfo.getId()),"只能删除自己的Feed");// 删除int count = feedsMapper.delete(id);if(count ==0){return;}// 将内容从粉丝的集合中删除 -- 异步消息队列优化// 先获取我的粉丝List<Integer> followers =findFollowers(userInfo.getId());// 移除 Feed
followers.forEach(follower ->{String key =RedisKeyConstant.following_feeds.getKey()+ follower;
redisTemplate.opsForZSet().remove(key, feeds.getId());});}
FeedsMapper
启动项目测试
数据库中的feeds:
用户只能删除自己创建的Feed,测试用id为6的用户删除id为14的Feed(该Feed是id为7的用户创建的):
用id为7的用户登陆后,逻辑删除id=14的feeds:
删除后再次删除:
查看数据库中的feeds已经逻辑删除:
查看redis相关Feed也不存在了:
关注/取关时处理用户 Feed
当A用户关注B用户时,那么要实时的将B的所有Feed推送到A用户的Feed集合中,同样如果A用户取关B用户,那么要将B用户所有的Feed从A用户的Feed集合中移除。
FeedsController
/**
* 变更 Feed
*
* @return
*/@PostMapping("updateFollowingFeeds/{followingDinerId}")publicResultInfoaddFollowingFeeds(@PathVariableInteger followingDinerId,String access_token,@RequestParamint type){
feedsService.addFollowingFeed(followingDinerId, access_token, type);returnResultInfoUtil.buildSuccess(request.getServletPath(),"操作成功");}
FeedsService
/**
* 变更 Feed
*
* @param followinguserId 关注的好友 ID
* @param accessToken 登录用户token
* @param type 1 关注 0 取关
*/@Transactional(rollbackFor =Exception.class)publicvoidaddFollowingFeed(Integer followinguserId,String accessToken,int type){// 请选择关注的好友AssertUtil.isTrue(followinguserId ==null|| followinguserId <1,"请选择关注的好友");// 获取登录用户信息SignInUserInfo userInfo =loadSignInUserInfo(accessToken);// 获取关注/取关的用户的所有 FeedList<Feeds> feedsList = feedsMapper.findByUserId(followinguserId);String key =RedisKeyConstant.following_feeds.getKey()+ userInfo.getId();if(type ==0){// 取关List<Integer> feedIds = feedsList.stream().map(feed -> feed.getId()).collect(Collectors.toList());
redisTemplate.opsForZSet().remove(key, feedIds.toArray(newInteger[]{}));}else{// 关注Set<ZSetOperations.TypedTuple> typedTuples =
feedsList.stream().map(feed ->newDefaultTypedTuple<>(feed.getId(),(double) feed.getUpdateDate().getTime())).collect(Collectors.toSet());
redisTemplate.opsForZSet().add(key, typedTuples);}}
FeedsMapper
/**
* 根据用户 ID 查询 Feed
* @param userId
* @return
*/@Select("select id, content, update_date from t_feeds "+" where fk_user_id = #{userId} and is_valid = 1")List<Feeds>findByUserId(@Param("userId")Integer userId);
ms-follow 服务关注取关时变更 Feed
添加调用ms-feeds服务的请求地址项目路径
FollowService新增关注/取关时Feed逻辑
/**
* 发送请求添加或者移除关注人的Feed列表
*
* @param followUserId 关注好友的ID
* @param accessToken 当前登录用户token
* @param type 0=取关 1=关注
*/privatevoidsendSaveOrRemoveFeed(Integer followUserId,String accessToken,int type){String feedsUpdateUrl = feedsServerName +"updateFollowingFeeds/"+ followUserId +"?access_token="+ accessToken;// 构建请求头HttpHeaders headers =newHttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);// 构建请求体(请求参数)MultiValueMap<String,Object> body =newLinkedMultiValueMap<>();
body.add("type", type);HttpEntity<MultiValueMap<String,Object>> entity =newHttpEntity<>(body, headers);
restTemplate.postForEntity(feedsUpdateUrl, entity,ResultInfo.class);}
启动项目测试
用户8,9,10都关注了用户7
那么用户7推送一条feeds(朋友圈) 的时候,他的粉丝用户8,9,10应该都可以看到,测试用户7发送feed:
查看数据库和redis:
用户10取消关注用户7
用户10的feeds集合中存储了关注用户的feeds :
让用户10取消关注用户7:
用户7的所有feeds(朋友圈) 应该从用户10的feeds集合中移除:
只剩下用户8、9相关的。
用户11关注用户7
用户7的所有feeds(朋友圈) 应该都添加到用户11的feeds集合中:
分页获取关注的 Feed 数据
当前数据库用户7发布了
构建返回的FeedsVO
/**
*
* Feed显示信息
* @author zjq
*/@Getter@Setter@ApiModel(description ="Feed显示信息")publicclassFeedsVOimplementsSerializable{@ApiModelProperty("主键")privateInteger id;@ApiModelProperty("内容")privateString content;@ApiModelProperty("点赞数")privateint praiseAmount;@ApiModelProperty("评论数")privateint commentAmount;@ApiModelProperty("餐厅id")privateInteger fkRestaurantId;@ApiModelProperty("用户ID")privateInteger fkUserId;@ApiModelProperty("用户信息")privateShortUserInfo userInfo;@ApiModelProperty("显示时间")@JsonFormat(pattern ="yyyy-MM-dd HH:mm")publicDate createDate;}
FeedsController
/**
* 分页获取关注的 Feed 数据
*
* @param page
* @param access_token
* @return
*/@GetMapping("{page}")publicResultInfoselectForPage(@PathVariableInteger page,String access_token){List<FeedsVO> feedsVOS = feedsService.selectForPage(page, access_token);returnResultInfoUtil.buildSuccess(request.getServletPath(), feedsVOS);}
FeedsService
登录用户每次发送朋友圈都会向粉丝的feeds集合中推送这条朋友圈,那么当粉丝就可以获取关注的人的所有feeds。
比如用户8,9,11关注了用户7,那么用户7发的5条朋友圈,用户8,9,11都能看到,用户8同时还跟用户6是好友,那么用户8可以同时看到用户7和用户6发送的3条朋友圈。
实现逻辑如下:
- 获取登录用户信息
- 构建分页查询的参数start,end
- 从Redis的sorted sets中按照score的降序进行读取Feed的id
- 从数据库中获取Feed的信息
- 构建Feed关联的用户信息(不是循环逐条读取,而是批量获取)
/**
* 根据时间由近至远,每次查询 6 条 Feed
*
* @param page
* @param accessToken
* @return
*/publicList<FeedsVO>selectForPage(Integer page,String accessToken){if(page ==null){
page =1;}// 获取登录用户SignInUserInfo userInfo =loadSignInUserInfo(accessToken);// 我关注的好友的 FeedkeyString key =RedisKeyConstant.following_feeds.getKey()+ userInfo.getId();// SortedSet 的 ZREVRANGE 命令是闭区间long start =(page -1)*ApiConstant.PAGE_SIZE;long end = page *ApiConstant.PAGE_SIZE-1;Set<Integer> feedIds = redisTemplate.opsForZSet().reverseRange(key, start, end);if(feedIds ==null|| feedIds.isEmpty()){returnLists.newArrayList();}// 根据多主键查询 FeedList<Feeds> feeds = feedsMapper.findFeedsByIds(feedIds);// 初始化关注好友 ID 集合List<Integer> followinguserIds =newArrayList<>();// 添加用户 ID 至集合,顺带将 Feeds 转为 Vo 对象List<FeedsVO> feedsVOS = feeds.stream().map(feed ->{FeedsVO feedsVO =newFeedsVO();BeanUtil.copyProperties(feed, feedsVO);// 添加用户 ID
followinguserIds.add(feed.getFkUserId());return feedsVO;}).collect(Collectors.toList());// 远程调用获取 Feed 中用户信息ResultInfo resultInfo = restTemplate.getForObject(usersServerName +"findByIds?access_token=${accessToken}&ids={ids}",ResultInfo.class, accessToken, followinguserIds);if(resultInfo.getCode()!=ApiConstant.SUCCESS_CODE){thrownewParameterException(resultInfo.getCode(), resultInfo.getMessage());}List<LinkedHashMap> userInfoMaps =(ArrayList) resultInfo.getData();// 构建一个 key 为用户 ID,value 为 ShortuserInfo 的 MapMap<Integer,ShortUserInfo> userInfos = userInfoMaps.stream().collect(Collectors.toMap(// key
diner ->(Integer) diner.get("id"),// value
diner ->BeanUtil.fillBeanWithMap(diner,newShortUserInfo(),true)));// 循环 VO 集合,根据用户 ID 从 Map 中获取用户信息并设置至 VO 对象
feedsVOS.forEach(feedsVO ->{
feedsVO.setUserInfo(userInfos.get(feedsVO.getFkUserId()));});return feedsVOS;}
FeedsMapper
/**
* 根据多主键查询 Feed
* @param feedIds
* @return
*/@Select("<script> "+" select id, content, fk_user_id, praise_amount, "+" comment_amount, fk_restaurant_id, create_date, update_date, is_valid "+" from t_feeds where is_valid = 1 and id in "+" <foreach item=\"id\" collection=\"feedIds\" open=\"(\" separator=\",\" close=\")\">"+" #{id}"+" </foreach> order by id desc"+" </script>")List<Feeds>findFeedsByIds(@Param("feedIds")Set<Integer> feedIds);
启动项目测试
查询用户8关注好友的feeds列表:
用户8同时和用户7、用户6是好友,那么用户8可以同时看到用户7的5条朋友圈和用户6发送的3条朋友圈。
调用分页查询接口查询如下:
http://localhost/feeds/1?access_token=1d7eb176-2454-4fd4-96d2-9c2d27c0ace6
可以看到顺序是按照最新的在最上面。
本文内容到此结束了,
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。
主页:共饮一杯无的博客汇总👨💻保持热爱,奔赴下一场山海。🏃🏃🏃
版权归原作者 共饮一杯无 所有, 如有侵权,请联系我们删除。