微商城手机网站制作/长沙百度提升排名
2022黑马Redis跟学笔记.实战篇 六
- 4.7.达人探店功能
- 4.7.1.分享探店图文
- 1. 达人探店-发布探店笔记
- 2. 达人探店-查看探店笔记
- 4.7.2.点赞功能
- 4.7.3.基于List实现点赞用户列表TOP10
- 4.7.4.基于SortedSet实现点赞排行榜
- 4.8.关注列表
- 4.8.1.关注列表实现原理
- 4.8.2.添加关注
- 1. 好友关注-关注和取消关注
- 4.8.3.共同关注列表
- 4.8.4.取消关注
- 1.好友关注-Feed流实现方案
- 4.8.5.探店推送功能
- 1. 好友关注-推送到粉丝收件箱
- 2.好友关注-实现分页查询收邮箱
4.7.达人探店功能
4.7.1.分享探店图文
1. 达人探店-发布探店笔记
发布探店笔记
探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:
tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
tb_blog_comments:其他用户对探店笔记的评价
具体发布流程
上传接口```java
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {@PostMapping("blog")public Result uploadImage(@RequestParam("file") MultipartFile image) {try {// 获取原始文件名称String originalFilename = image.getOriginalFilename();// 生成新文件名String fileName = createNewFileName(originalFilename);// 保存文件image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));// 返回结果log.debug("文件上传成功,{}", fileName);return Result.ok(fileName);} catch (IOException e) {throw new RuntimeException("文件上传失败", e);}}}
注意:同学们在操作时,需要修改SystemConstants.IMAGE_UPLOAD_DIR 自己图片所在的地址,在实际开发中图片一般会放在nginx上或者是云存储上。
修改图片的上传路径到本地的地址
修改类SystemConstants.java
public static final String IMAGE_UPLOAD_DIR = "F:\\javawebwork\\ty-comment-html\\nginx-1.18.0\\html\\hmdp\\imgs\\";BlogController
```java
@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {//获取登录用户UserDTO user = UserHolder.getUser();blog.setUpdateTime(user.getId());//保存探店博文blogService.saveBlog(blog);//返回idreturn Result.ok(blog.getId());}
}
点击加号上传图片
根据开发者工具看到目录
上传路径
点击发布
发布之后,可以在首页看到相关信息。
再看一下数据库
2. 达人探店-查看探店笔记
实现查看发布探店笔记的接口
点击笔记详情发现报错了。
修改BlogController.java
@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}/*** @param* @return com.hmdp.dto.Result* @description //查看笔记详情页面* @param: id 笔记id* @date 2023/2/17 22:03* @author wty**/@GetMapping("/{id}")public Result queryBlogById(@PathVariable("id") Long id) {return blogService.queryBlogById(id);}
实现代码:
修改接口IBlogService.java
/*** <p>* 服务类* </p>** @author wty* @since 2021-12-22*/
public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);/*** @param* @return com.hmdp.dto.Result* @description //查看笔记详情页面* @param: id 笔记id* @date 2023/2/17 22:05* @author wty**/Result queryBlogById(Long id);
}
修改BlogServiceImpl
/*** <p>* 服务实现类* </p>** @author wty* @since 2021-12-22*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户records.forEach(this::queryBlogUser);return Result.ok(records);}/*** @param* @return com.hmdp.dto.Result* @description //查看笔记详情页面* @param: id 笔记id* @date 2023/2/17 22:07* @author wty**/@Overridepublic Result queryBlogById(Long id) {// 1.查询blogBlog blog = getById(id);if (null == blog) {return Result.fail("笔记不存在!");}// 2.查询blog相关的用户queryBlogUser(blog);return Result.ok(blog);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
}
重启应用,发现打开成功了详情页面
4.7.2.点赞功能
初始代码
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {//修改点赞数量blogService.update().setSql("liked = liked +1 ").eq("id",id).update();return Result.ok();
}
问题分析:这种方式会导致一个用户无限点赞,明显是不合理的
造成这个问题的原因是,我们现在的逻辑,发起请求只是给数据库+1,所以才会出现这个问题。
完善点赞功能
需求:
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
实现步骤:
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
- 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
- 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
为什么采用set集合:
因为我们的数据是不能重复的,其次是一个集合。
具体步骤:
1、在Blog 添加一个字段
@TableField(exist = false)
private Boolean isLike;
2、修改代码
BlogController.java
@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {// 修改点赞数量 update tb_blog set liked = liked where id = ?//blogService.update().setSql("liked = liked + 1").eq("id", id).update();return blogService.likeBlog(id);}
修改IBlogService.java添加方法
/*** @param* @return com.hmdp.dto.Result* @description //点赞* @param: id* @date 2023/2/17 22:32* @author wty**/Result likeBlog(Long id);
修改BlogServiceImpl.java
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@ResourceStringRedisTemplate stringRedisTemplate;@Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户records.forEach(new Consumer<Blog>() {@Overridepublic void accept(Blog blog) {queryBlogUser(blog);isBlogLiked(blog);}});//records.forEach(this::queryBlogUser);return Result.ok(records);}/*** @param* @return com.hmdp.dto.Result* @description //查看笔记详情页面* @param: id 笔记id* @date 2023/2/17 22:07* @author wty**/@Overridepublic Result queryBlogById(Long id) {// 1.查询blogBlog blog = getById(id);if (null == blog) {return Result.fail("笔记不存在!");}// 2.查询blog相关的用户queryBlogUser(blog);// 3.查询blog是否被点赞isBlogLiked(blog);return Result.ok(blog);}/*** @param* @return void* @description //当前笔记是否被当前用户点赞* @param: blog* @date 2023/2/17 22:50* @author wty**/private void isBlogLiked(Blog blog) {Long id = blog.getId();// 1.获取当前登录用户Long userId = UserHolder.getUser().getId();// 2.判断当前登录用户是否点赞 key "blog:liked:" + idString key = RedisConstants.BLOG_LIKED_KEY + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());boolean flag = BooleanUtil.isTrue(isMember);blog.setIsLike(flag);}/*** @param* @return com.hmdp.dto.Result* @description // 点赞* @param: id* @date 2023/2/17 22:32* @author wty**/@Overridepublic Result likeBlog(Long id) {// 1.获取当前登录用户Long userId = UserHolder.getUser().getId();// 2.判断当前登录用户是否点赞 key "blog:liked:" + idString key = RedisConstants.BLOG_LIKED_KEY + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());boolean flag = BooleanUtil.isTrue(isMember);if (!flag) {// 3.如果未点赞,可以点赞// 3.1数据库点赞数+1// 修改点赞数量 update tb_blog set liked = liked where id = ?boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();if (isSuccess) {// 3.2保存用户到Redis的set集合stringRedisTemplate.opsForSet().add(key, userId.toString());}} else {// 4.如果已经点赞,取消点赞// 4.1数据库点赞数 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();if (isSuccess) {// 4.2把用户从Redis的set集合移除stringRedisTemplate.opsForSet().remove(key, userId.toString());}}return null;}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
}
重启应用点赞后,这里感觉前端代码有点问题,点赞后还需要刷新一下页面才会显示高亮。
这里点赞和取消赞后前端会报错,会的小伙伴帮忙解决一下。
4.7.3.基于List实现点赞用户列表TOP10
4.7.4.基于SortedSet实现点赞排行榜
在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:
之前的点赞是放到set集合,但是set集合是不能排序的,所以这个时候,咱们可以采用一个可以排序的set集合,就是咱们的sortedSet
我们接下来来对比一下这些集合的区别是什么
所有点赞的人,需要是唯一的,所以我们应当使用set或者是sortedSet
其次我们需要排序,就可以直接锁定使用sortedSet啦!
这里注意sortedSet中判断一个元素是否存在是没有像set集合那样的SISMEMBER的,那我们怎么判断呢,这里可以用ZSCORE,获取sorted set中的指定元素的score值,如果能获取到,说明存在,获取不到,说明不存在。
修改代码
BlogServiceImpl
点赞逻辑代码
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@ResourceStringRedisTemplate stringRedisTemplate;@Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户records.forEach(new Consumer<Blog>() {@Overridepublic void accept(Blog blog) {queryBlogUser(blog);isBlogLiked(blog);}});//records.forEach(this::queryBlogUser);return Result.ok(records);}/*** @param* @return com.hmdp.dto.Result* @description //查看笔记详情页面* @param: id 笔记id* @date 2023/2/17 22:07* @author wty**/@Overridepublic Result queryBlogById(Long id) {// 1.查询blogBlog blog = getById(id);if (null == blog) {return Result.fail("笔记不存在!");}// 2.查询blog相关的用户queryBlogUser(blog);// 3.查询blog是否被点赞isBlogLiked(blog);return Result.ok(blog);}/*** @param* @return void* @description //当前笔记是否被当前用户点赞(Set)* @param: blog* @date 2023/2/17 22:50* @author wty**/private void isBlogLikedSet(Blog blog) {Long id = blog.getId();// 1.获取当前登录用户Long userId = UserHolder.getUser().getId();if (null == userId) {return;}// 2.判断当前登录用户是否点赞 key "blog:liked:" + idString key = RedisConstants.BLOG_LIKED_KEY + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());boolean flag = BooleanUtil.isTrue(isMember);blog.setIsLike(flag);}/*** @param* @return void* @description //当前笔记是否被当前用户点赞(SortedSet)* @param: blog* @date 2023/2/17 22:50* @author wty**/private void isBlogLiked(Blog blog) {Long id = blog.getId();// 1.获取当前登录用户Long userId = UserHolder.getUser().getId();if (null == userId) {return;}// 2.判断当前登录用户是否点赞 key "blog:liked:" + idString key = RedisConstants.BLOG_LIKED_KEY + id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(null != score);}/*** @param* @return com.hmdp.dto.Result* @description // 点赞(Set集合)* @param: id* @date 2023/2/17 22:32* @author wty**/public Result likeBlogSet(Long id) {// 1.获取当前登录用户Long userId = UserHolder.getUser().getId();// 2.判断当前登录用户是否点赞 key "blog:liked:" + idString key = RedisConstants.BLOG_LIKED_KEY + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());boolean flag = BooleanUtil.isTrue(isMember);if (!flag) {// 3.如果未点赞,可以点赞// 3.1数据库点赞数+1// 修改点赞数量 update tb_blog set liked = liked where id = ?boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();if (isSuccess) {// 3.2保存用户到Redis的set集合stringRedisTemplate.opsForSet().add(key, userId.toString());}} else {// 4.如果已经点赞,取消点赞// 4.1数据库点赞数 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();if (isSuccess) {// 4.2把用户从Redis的set集合移除stringRedisTemplate.opsForSet().remove(key, userId.toString());}}return null;}/*** @param* @return com.hmdp.dto.Result* @description // 点赞(SortedSet集合)* @param: id* @date 2023/2/17 22:32* @author wty**/@Overridepublic Result likeBlog(Long id) {// 1.获取当前登录用户Long userId = UserHolder.getUser().getId();// 2.判断当前登录用户是否点赞 key "blog:liked:" + idString key = RedisConstants.BLOG_LIKED_KEY + id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if (null == score) {// 3.如果未点赞,可以点赞// 3.1数据库点赞数+1// 修改点赞数量 update tb_blog set liked = liked where id = ?boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();if (isSuccess) {// 3.2保存用户到Redis的sortedset集合 zadd key score memberstringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}} else {// 4.如果已经点赞,取消点赞// 4.1数据库点赞数 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();if (isSuccess) {// 4.2把用户从Redis的set集合移除stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return null;}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
}
重启应用,删除redis中的liked的元素,然后点赞
看redis中,存储成功
再点击一下,取消点赞
看redis中也没有了。
点赞列表查询列表
修改BlogController
@GetMapping("/likes/{id}")public Result queryBlogLikes(@PathVariable("id") Long id) {return blogService.queryBlogLikes(id);}
修改接口BlogServiceImpl.java
/*** @param* @return com.hmdp.dto.Result* @description //查询点赞的列表* @param: id* @date 2023/2/18 1:10* @author wty**/Result queryBlogLikes(Long id);
修改BlogService
/*** @param* @return com.hmdp.dto.Result* @description //查询点赞列表* @param: id* @date 2023/2/18 1:11* @author wty**/@Overridepublic Result queryBlogLikes(Long id) {String key = RedisConstants.BLOG_LIKED_KEY + id;// 1.查询top5的点赞用户 zrange key 0 4Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (null == top5 || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2.解析出其中的用户idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());// 3.根据用户id查询用户List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 4.返回return Result.ok(userDTOS);}
重启应用
加载出列表
再用另一个号点赞,发现后来的人,点赞顺序竟然是第一个,这明显是顺序反了
原因是sql语句
我们放到sqlyog中查询一下发现顺序确实会重排
sql如下,即可解决
SELECT * FROM tb_user WHERE id IN ('5','1') ORDER BY FIELD(id,5,1);
调整代码BlogServiceImpl.java
// 3.根据用户id查询用户//List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());List<UserDTO> userDTOS = userService.query()// SELECT * FROM tb_user WHERE id IN ('5','1') ORDER BY FIELD(id,5,1);.in("id", ids).last("order by field (id," + idsStr + ") ").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
重启应用,发现点赞顺序已经调整了
4.8.关注列表
4.8.1.关注列表实现原理
4.8.2.添加关注
1. 好友关注-关注和取消关注
针对用户的操作:可以对用户进行关注和取消关注功能。
实现思路:
需求:基于该表数据结构,实现两个接口:
- 关注和取关接口
- 判断是否关注的接口
关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:
注意: 这里需要把主键修改为自增长,简化开发。
修改FollowController
@RestController
@RequestMapping("/follow")
public class FollowController {@Autowiredprivate IFollowService iFollowService;/*** @param* @return com.hmdp.dto.Result* @description //关注博主* @param: followUserId* @param: isFollow* @date 2023/2/18 10:54* @author wty**/@PutMapping("/{id}/{isFollow}")public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") boolean isFollow) {return iFollowService.follow(followUserId, isFollow);}/*** @param* @return com.hmdp.dto.Result* @description //判断是否关注* @param: followUserId* @date 2023/2/18 10:54* @author wty**/@GetMapping("/or/not/{id}")public Result isFollow(@PathVariable("id") Long followUserId) {return iFollowService.isFollow(followUserId);}
}
修改接口IFollowService.java
public interface IFollowService extends IService<Follow> {Result follow(Long followUserId, boolean isFollow);Result isFollow(Long followUserId);
}
修改实现类FollowServiceImpl.java
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {/*** @param* @return com.hmdp.dto.Result* @description //关注* @param: id* @param: isFollow* @date 2023/2/18 10:58* @author wty**/@Overridepublic Result follow(Long followUserId, boolean isFollow) {Long userId = UserHolder.getUser().getId();// 1.判断是关注还是取关if (isFollow) {// 2.关注,新增数据Follow follow = new Follow();follow.setFollowUserId(followUserId);follow.setUserId(userId);// insert into tb_follow values()save(follow);} else {// 3.取关,删除// delete from follow where user_id = ? and follow_user_id = ?LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();remove(wrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId));}return Result.ok();}/*** @param* @return com.hmdp.dto.Result* @description //判断是否关注* @param: id* @param: followUserId* @date 2023/2/18 10:58* @author wty**/@Overridepublic Result isFollow(Long followUserId) {// 1.获取用户idLong userId = UserHolder.getUser().getId();// 2.查询是否关注 select * from tb_follow where user_id = ? and follow_user_id = ?Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();return Result.ok(count > 0);}
}
重启应用,点击关注
查看数据库
4.8.3.共同关注列表
想要去看共同关注的好友,需要首先进入到这个页面,这个页面会发起两个请求
1、去查询用户的详情
2、去查询用户的笔记
以上两个功能和共同关注没有什么关系,大家可以自行将笔记中的代码拷贝到idea中就可以实现这两个功能了,我们的重点在于共同关注功能。
// UserController 根据id查询用户
@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){// 查询详情User user = userService.getById(userId);if (user == null) {return Result.ok();}UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 返回return Result.ok(userDTO);
}// BlogController 根据id查询博主的探店笔记
@GetMapping("/of/user")
public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current,@RequestParam("id") Long id) {// 根据用户查询Page<Blog> page = blogService.query().eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();return Result.ok(records);
}
完成后,重启应用,点击头像
进入后,可以查看详情
点击共同关注报错
接下来我们来看看共同关注如何实现:
需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同关注呢。
当然是使用我们之前学习过的set集合咯,在set集合中,有交集并集补集的api,我们可以把两人的关注的人分别放入到一个set集合中,然后再通过api去查看这两个set集合中的交集数据。
我们先来改造当前的关注列表
改造原因是因为我们需要在用户关注了某位用户后,需要将数据放入到set集合中,方便后续进行共同关注,同时当取消关注时,也需要从set集合中进行删除
将关注的博主放入redis的set列表中
FollowServiceImpl
/*** @param* @return com.hmdp.dto.Result* @description //关注* @param: id* @param: isFollow* @date 2023/2/18 10:58* @author wty**/@Overridepublic Result follow(Long followUserId, boolean isFollow) {Long userId = UserHolder.getUser().getId();String key = RedisConstants.FOLLOW_USER_LIST + userId;// 1.判断是关注还是取关if (isFollow) {// 2.关注,新增数据Follow follow = new Follow();follow.setFollowUserId(followUserId);follow.setUserId(userId);// insert into tb_follow values()boolean isSuccess = save(follow);// 加入Redis,实现共同关注if (isSuccess) {// 把关注用户的id,加入redis的set集合 sadd userId followUserIdstringRedisTemplate.opsForSet().add(key, followUserId.toString());}} else {// 3.取关,删除// delete from follow where user_id = ? and follow_user_id = ?LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();boolean isSuccess = remove(wrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId));if (isSuccess) {// 从Redis中移除,实现取关// 把关注用户的id,从redis的set集合中移除 srem userId followUserIdstringRedisTemplate.opsForSet().remove(key, followUserId.toString());}}return Result.ok();}
重启应用,取消关注后重新关注
查看redis
查看数据库
再登录一个用户,去关注前2个账号
关注完后看redis,保存成功
看数据库,也保存成功
下面实现共同关注的功能
具体的关注代码:
修改FollowController.java
/*** @param* @return com.hmdp.dto.Result* @description //查询共同关注* @param: id* @date 2023/2/18 13:12* @author wty**/@GetMapping("/common/{id}")public Result followCommons(@PathVariable("id") Long id) {return iFollowService.followCommons(id);}
修改接口
Result followCommons(Long id);
修改实现类FollowServiceImpl.java
/*** @param* @return com.hmdp.dto.Result* @description //共同关注* @param: id* @date 2023/2/18 13:13* @author wty**/@Overridepublic Result followCommons(Long id) {// 1.获取当前用户Long userId = UserHolder.getUser().getId();String key = RedisConstants.FOLLOW_USER_LIST + userId;// 目标用户String keyFollow = RedisConstants.FOLLOW_USER_LIST + id;// 2.求交集Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, keyFollow);if (null == intersect || intersect.isEmpty()) {return Result.ok(Collections.emptyList());}// 3.解析id集合List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 查询用户return Result.ok(userDTOS);}
重启应用,此时已经可以查看共同关注了。
查看redis,也存储成功了。
4.8.4.取消关注
1.好友关注-Feed流实现方案
当我们关注了用户后,这个用户发了动态,那么我们应该把这些数据推送给用户,这个需求,其实我们又把他叫做Feed流,关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
对于传统的模式的内容解锁:我们是需要用户去通过搜索引擎或者是其他的方式去解锁想要看的内容
对于新型的Feed流的的效果:不需要我们用户再去推送信息,而是系统分析用户到底想要什么,然后直接把内容推送给用户,从而使用户能够更加的节约时间,不用主动去寻找。
Feed流的实现有两种模式:
Feed流产品有两种常见模式:
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 优点:信息全面,不会有缺失。并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
我们本次针对好友的操作,采用的就是Timeline的方式,只需要拿到我们关注用户的信息,然后按照时间排序即可
,因此采用Timeline的模式。该模式的实现方案有三种:
- 拉模式
- 推模式
- 推拉结合
拉模式:也叫做读扩散
该模式的核心含义就是:当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序
优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清楚。
缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大。
推模式:也叫做写扩散。
推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了
优点:时效快,不用临时拉取
缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去
推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。
推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。
4.8.5.探店推送功能
1. 好友关注-推送到粉丝收件箱
需求:
- 1.修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
- 2.收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
- 3.查询收件箱数据时,可以实现分页查询
Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。
传统的分页在feed流是不适用的,因为我们的数据会随时发生变化。
假设在t1 时刻,我们去读取第一页,此时page = 1 ,size = 5 ,那么我们拿到的就是10 ~ 6 这几条记录,假设现在t2时候又发布了一条记录,此时t3 时刻,我们来读取第二页,读取第二页传入的参数是page=2 ,size=5 ,那么此时读取到的第二页实际上是从6 开始,然后是6~2 ,那么我们就读取到了重复的数据,所以feed流的分页,不能采用原始方案来做。
Feed流的滚动分页
我们需要记录每次操作的最后一条,然后从这个位置开始去读取数据
举个例子:我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。我们这个地方可以采用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分页了
核心的意思:就是我们在保存完探店笔记后,获得到当前笔记的粉丝,然后把数据推送到粉丝的redis中去。
修改BlogController.java
@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {return blogService.saveBlog(blog);}
修改接口IBlogService.java
Result saveBlog(Blog blog);
接口实现类BlogServiceImpl.java
/*** @param* @return com.hmdp.dto.Result* @description //使用push模式* @param: blog* @date 2023/2/18 13:54* @author wty**/@Overridepublic Result saveBlog(Blog blog) {// 1.获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 2.保存探店博文boolean isSuccess = save(blog);if (!isSuccess) {return Result.fail("新增笔记失败,请重新发布!");}// 3.查询笔记作者的所有粉丝// select user_id from tb_follow where follow_user_id = ?LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Follow::getFollowUserId, user.getId());List<Follow> follows = followService.list(wrapper);// 4.推送笔记id给所有粉丝for (Follow follow : follows) {// 4.1获取粉丝idLong fansId = follow.getUserId();// 4.2推送给粉丝String key = RedisConstants.FEED_KEY + fansId;stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());}// 5.返回idreturn Result.ok(blog.getId());}
重启应用
用可可登录,发布博客
发布成功
看redis,可可的粉丝,收到了消息
2.好友关注-实现分页查询收邮箱
需求:在个人主页的“关注”卡片中,查询并展示推送的Blog信息:
具体操作如下:
1、每次查询完成后,我们要分析出查询出数据的最小时间戳,这个值会作为下一次查询的条件
2、我们需要找到与上一次查询相同的查询个数作为偏移量,下次查询时,跳过这些查询过的数据,拿到我们需要的数据
综上:我们的请求参数中就需要携带 lastId:上一次查询的最小时间戳 和偏移量这两个参数。
这两个参数第一次会由前端来指定,以后的查询就根据后台结果作为条件,再次传递到后台。
先来熟悉一下sortedset的指令
打开命令窗口
zadd z1 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6
zadd z1 7 m7 8 m8 9 m9
ZRANGEBYSCORE z1 0 8
存放了9个元素
假如现在我们想倒叙排列
ZREVRANGE z1 0 9
如果还想带上分数呢
ZREVRANGE z1 0 9 WITHSCORES
按照分数查询,模拟分页
比如以score = 4为分水岭,分割成2页
ZREVRANGEBYSCORE z1 1000 0 WITHSCORES LIMIT 0 3
此时来插入了一条数据
ZADD z1 10 m10
输入以下指令,
注意这里的7是上次查询的最小额score
ZREVRANGEBYSCORE z1 7 0 WITHSCORES LIMIT 1 3
现在我们把redis中2个值更改为一样的,看看会怎么样。
打开命令窗口运行以下命令
ZREVRANGEBYSCORE z1 1000 0 WITHSCORES LIMIT 0 5
接着查询下一页
ZREVRANGEBYSCORE z1 6 0 WITHSCORES LIMIT 1 5
那正确应该写为
ZREVRANGEBYSCORE z1 6 0 WITHSCORES LIMIT 2 5
最后总结规律
滚动分页
-- 第一次
ZREVRANGEBYSCORE key 设定一个最大值 0 WITHSCORES LIMIT 0 每页展示几条
-- 之后
ZREVRANGEBYSCORE key 第一条最小的角标 0 WITHSCORES LIMIT 第一页中与最小值相等的元素的个数 每页展示几条
一、定义出来具体的返回值实体类
新增实体类ScrollResult
@Data
public class ScrollResult {private List<?> list;private Long minTime;private Integer offset;
}
BlogController
注意:RequestParam 表示接受url地址栏传参的注解,当方法上参数的名称和url地址栏不相同时,可以通过RequestParam 来进行指定
@GetMapping("/of/follow")
public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){return blogService.queryBlogOfFollow(max, offset);
}
接口IBlogService.java中添加
/*** @param* @return com.hmdp.dto.Result* @description //滚动分页查询关注列表* @param: max* @param: offset* @date 2023/2/18 16:53* @author wty**/Result queryBlogOfFollow(Long max, Integer offset);
BlogServiceImpl
/*** @param* @return com.hmdp.dto.Result* @description //滚动分页查询关注列表* @param: max* @param: offset* @date 2023/2/18 16:54* @author wty**/@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {// 1.查询当前用户Long userId = UserHolder.getUser().getId();// 2.找到收件箱String key = RedisConstants.FEED_KEY + userId;// 滚动分页查询(第一次查询)//ZREVRANGEBYSCORE z1 1000 0 WITHSCORES LIMIT 0 5Set<ZSetOperations.TypedTuple<String>> set = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2L);// 3.非空判断if (null == set || set.isEmpty()) {return Result.ok();}// 4.解析收件箱的数据:blogId,minTime(时间戳),offsetArrayList<Long> ids = new ArrayList<>(set.size());long minTime = Long.MAX_VALUE;int off = 1;for (ZSetOperations.TypedTuple<String> tuple : set) {// 4.1获取idids.add(Long.valueOf(tuple.getValue()));// 4.2 获取分数(时间戳)long time = tuple.getScore().longValue();if (time == minTime) {off++;} else {minTime = minTime < tuple.getScore().longValue() ? minTime : time;off = 1;}}//List<Blog> blogs = listByIds(ids);// select * from tb_blog where id in () order by field(id,max,min)String joinStr = StrUtil.join(",", ids);// 5.根据blogId查询blogList<Blog> blogs = query().in("id", ids).last("order by field(id," + joinStr + ")").list();for (Blog blog : blogs) {// 2.查询blog相关的用户queryBlogUser(blog);// 3.查询blog是否被点赞isBlogLiked(blog);}// 6.封装并返回ScrollResult result = new ScrollResult();result.setList(blogs);result.setOffset(off);result.setMinTime(minTime);return Result.ok(result);}
重启应用,看到可可发的动态了
再换一个人关注发个动态,一样也查询到了
滚动分页也实现了
相关文章:

2022黑马Redis跟学笔记.实战篇(六)
2022黑马Redis跟学笔记.实战篇 六4.7.达人探店功能4.7.1.分享探店图文1. 达人探店-发布探店笔记2. 达人探店-查看探店笔记4.7.2.点赞功能4.7.3.基于List实现点赞用户列表TOP104.7.4.基于SortedSet实现点赞排行榜4.8.关注列表4.8.1.关注列表实现原理4.8.2.添加关注1. 好友关注-关…...

Linux-VMware常用设置(时间+网络)及网络连接激活失败解决方法-基础篇②
目录一、设置时间二、网络设置1. 激活网卡方法一:直接启动网卡(仅限当此)方法二:修改配置文件(永久)2. 将NAT模式改为桥接模式什么是是NAT模式?如何改为桥接模式?三、虚拟机网络连接…...

vue3学习总结1
一.vue3与vue2相比带来哪些变化?a.性能的提升(包括打包大小减少,初次渲染的速度加快,更新渲染速度加快,内存减少)b.源码的升级(响应式的原理发生了变化,由原来的defineProperty变成了…...

SpringBoot统一功能处理
一、统一用户登录权限验证 1.1Spring拦截器 实现拦截器需要以下两步: 1.创建自定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执行具体方法之前的预处理)方法。 2.将⾃定义拦截器加⼊ WebMvcConfigurer 的 addIntercept…...

2022年3月电子学会Python等级考试试卷(五级)答案解析
目录 一、单选题(共25题,共50分) 二、判断题(共10题,共20分) 三、编程题(共3题,共30分) 青少年软件编程(Python)等级考试试卷(五级&#...

【C++】智能指针
目录 一、先来看一下什么是智能指针 二、 auto_ptr 1、C98版本 2、C11的auto_ptr 三、boost 库中的智能指针 1. scoped_ptr 2、shared_ptr(最好的智能指针) 四、C11中新提供的智能指针 unique_ptr shared_ptr std::shared_ptr的循环引用问题…...

Seata架构篇 - AT模式
AT 模式 概述 Seata AT 模式是一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑&am…...

加油站会员管理小程序实战开发教程12
我们上一篇介绍了会员数据源的开发,本节我们介绍一下会员注册功能。 首先呢梳理一下会员注册的业务逻辑,如果用户是首次登录,那他肯定还没有给我们的小程序提交任何的信息。那么我们就在我的页面给他显示一个注册的按钮,如果他已经注册过了,那么就正常显示会员的信息,他…...

用腾讯云同步Obsidian笔记
介绍 之前用gitee同步OB笔记,同时做图床。但由于git系产品设置起来相对复杂,且后续可能有外链过审等问题。周五被同事小姐姐安利了用腾讯云COS,试了一下,果然不错。其主要优点如下: 设置简单,学习成本低&…...

浅析C++指针与引用,栈传递的关系
目录 前言 C 堆指针 栈指针 常量指针 指针常量 引用 常量引用 总结 前言 目前做了很多项目,接触到各种语言,基本上用什么学什么,语言的边际就会很模糊,实际上语言的设计大同小异,只是语言具备各自的特性区别。…...

图解LeetCode——剑指 Offer 10- II. 青蛙跳台阶问题
一、题目 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 答案需要取模 1e97(1000000007),如计算初始结果为:1000000008,请返回 1。 二、示例 2.1>…...

【Linux】用户分类+权限管理+umask+粘滞位说明
目录 1.用户分类 su指令 2.认识Linux权限 2.1 文件访问者的分类 2.2 文件类型和访问权限 a. 文件类型 file指令 b. 访问权限 2.3 文件权值的表示方法 a. 字母表示法 b. 八进制表示法 3.如何修改文件访问者的权限及相关指令 1. chmod指令 2. chown指令 3. chgrp指…...

【干货】如何打造HR无法拒绝的简历?测试开发大牛带手把手你写简历!
通过率90%,优秀的软件测试简历长什么样? 也许口才好的人会觉得简历不重要,能说就行了,那是因为你没有体会过石沉大海的感觉! 很多人觉得疑惑,为什么我投了那么多简历,都没有接到面试通知&…...

nodejs学习-4:nodejs连接mongodb和相关操作
1. express生成器生成express模板 前提需要首先下载好:express-generator,命令如下(全局安装) npm install -g express-generator生成模板命令如下: express 项目名称 --viewejs // --view 参数表示前端界面使用的引擎,这里使用…...

【博客629】Linux DNS解析原理与配置
Linux DNS解析原理与配置 1、DNS缓存 作用: 程序客户端、下游的 DNS 服务器每次查询 DNS 成功之后,通常会将该 DNS 记录缓存一段时间,避免频繁发出查询请求的耗时。 Linux下的DNS缓存: Linux 系统默认不会在本地建立 DNS 缓存…...

【CSP】202212-2 训练计划
题目 问题背景 西西艾弗岛荒野求生大赛还有 天开幕! 问题描述 为了在大赛中取得好成绩,顿顿准备在 天时间内完成“短跑”、“高中物理”以及“核裂变技术”等总共 项科目的加强训练。其中第 项( )科目编号为 ,也可简…...

java基础学习 day42(继承中构造方法的访问特点,this、super的使用总结)
继承中,构造方法的访问特点 父类的构造方法不会被子类继承,但可以通过super()调用父类的构造方法,且只能在子类调用,在测试类中是不能手动单写构造方法的。子类中所有的构造方法默认先调用父类的无参构造,再执行自己构…...

生物医药多组学与生物信息方法介绍
基因组学告诉你可能发生什么,转录组学和蛋白组学告诉你即将发生什么,而代谢组学告诉你正在发生什么 1、多组学与生信方法 生物医学技术的组学包括基因组学、转录组学、蛋白质组学、代谢组学和表观基因组学等。这些组学研究领域通过大量数据的高通量技术…...

【进阶篇】线程的硬件基础
文章目录高速缓存缓存一致性协议写缓冲区和无效化队列高速缓存 简介 高速缓存是主内存与处理器之间的硬件,其容量小于主存,但存取速率远高于主存。因此处理器在执行读写操作时,可直接和高速缓存交互,提高响应速度。 我们常见的变…...

关于 ISP Tuning的学习,分享几点看法
关于学习,分享几点看法,欢迎讨论 。1、分阶段性的,阶梯式学习。2、带目的性的,任务式学习。3、有总结性的,输出式学习。如上3条,可以依次循环去执行,下面我以 ISP Tuning 的学习为例,…...

RocketMQ源码阅读
没有用过rocketmq,但是一直对RocketMQ的实现很感兴趣,本次阅读源码基于5.0.0 一、 nameserver 通过源码阅读发现,它的作用主要是当作一个注册中心,注册broker、topic等信息,维护topic以及broker队列的路由信息&#…...

重磅 | 小O软件新品【鲸鱼地图】发布
千呼万唤始出来.......,小O系列软件又添新品【鲸鱼地图】!!! 2023年新年伊始,小O就投入到新品研发工作中,秉承“发现地理价值”理念,为用户提供更加好用、易用的地图软件产品,经过春…...

软考高级信息系统项目管理师系列之二十五:项目合同管理
软考高级信息系统项目管理师系列之二十五:项目合同管理 一、项目合同管理内容整理一、合同管理基本概念1.项目合同管理定义2.合同的分类3.合同类型选择4.合同内容二、合同管理过程1.合同管理过程的内容2.合同签订和履行管理3.合同变更和档案管理4.合同违约索赔管理项目合同管理…...

测试开发之Django实战示例 第十三章 上线
在上一章,为其他程序与我们的Web应用交互创建了RESTful API。本章将学习如何创建生产环境让我们的网站正式上线,主要内容有:配置生产环境创建自定义中间件实现自定义管理命令1创建生产环境现在该将Django项目正式部署到生产环境中了。我们将按…...

python实战应用讲解-【语法基础篇】Python中的数值类型(附示例代码)
目录 前言 数值类型 十六进制、八进制和二进制 Python 数值类型转换 数值和表达式 前言...

Git常用命令以及如何在IDEA中使用Git
前言Git是一个分布式版本控制工具,主要用于管理开发过程中的源代码文件(Java类、xml文件、html页面等)。Git在管理文件过程中会记录日志,方便回退到历史版本;Git存在分支的概念,一个项目可以有多个分支&…...

音乐播放器-- 以及数据库数据存储
运行环境 : java1.8 数据库以及代码编写工具 : sqlserver -- mysql 也可以 工具 eclipse 编码gbk窗体 : Swing使用了jaudiotagger 进行了音乐处理 图片展示 ----- 空闲时间 做出来玩的项目 部分功能还没有完善 完善了的功能 音乐 /// 主页 &a…...

[JAVA安全]Spring Messaging之CVE-2018-1270
漏洞简介 Spring 框架中通过spring-messaging 模块来实现 STOMP (Simple Text-Orientated Messaging Protocol),STOMP是一种封装 WebSocket的简单消息协议。攻击者可以通过建立WebSocket连接并发送一条消息造成远程代码执行, spring-messagin…...

CAN通信笔记-位时间、Tq及采样点同步
本文框架1.前言2. 位时间2.1 位时间定义2.2 位时间计算3. Tq3.1 Tq的计算3.1.1 举个例子3.2 位时间与Tq的换算4. 采样点同步4.1 硬同步4.2 重同步4.2.1 延长PBS1的重同步4.2.2 缩短PBS2的重同步1.前言 本篇记录些关于CAN的一些学习笔记,说实话CAN协议发展的已经非常…...