2023.3.1 ——达人探店
实现发表文章及其照片
上传文件的请求
实现上传文件
@Slf4j @RestController @RequestMapping("upload") public class UploadController { /** * * @param image 接收文件的地址 * @return */ @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); } } private String createNewFileName(String originalFilename) { // 获取后缀 String suffix = StrUtil.subAfter(originalFilename, ".", true); // 生成目录 String name = UUID.randomUUID().toString(); int hash = name.hashCode(); int d1 = hash & 0xF; int d2 = (hash >> 4) & 0xF; // 判断目录是否存在 File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2)); if (!dir.exists()) { dir.mkdirs(); } // 生成文件名 return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix); } }
对于保存文件的地址
// 保存文件 image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName)); 这个需要我们自己去修改成为本地的地址
public static final String IMAGE_UPLOAD_DIR = "N:\\hepre\\nginx-1.18.0\\html\\hmdp\\imgs\\";
然后就可以点击发布博客【接口: /blog】
@RestController @RequestMapping("/blog") public class BlogController { @Resource private IBlogService blogService; @Resource private IUserService userService; @PostMapping public Result saveBlog(@RequestBody Blog blog) { // 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 保存探店博文 blogService.save(blog); // 返回id return Result.ok(blog.getId()); } }
查看文章
接口:【点击文章就是显示文章的id】
然后根据文章的id查询文章的详细信息,最后返回
@RestController @RequestMapping("/blog") public class BlogController { @GetMapping("/hot") public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) { // 根据用户查询 Page<Blog> page = blogService.query() .orderByDesc("liked") .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); // 获取当前页数据 List<Blog> records = page.getRecords(); // 查询用户 records.forEach(blog ->{ Long userId = blog.getUserId(); User user = userService.getById(userId); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); }); return Result.ok(records); } @GetMapping("/{id}") public Result queryBlogById(@PathVariable("id") Long id){ return blogService.queryBlogById(id); } }
文章点赞功能实现(基于redis实现)
接口
需求分析
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
实现步骤:
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
- 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
- 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
实现点赞功能
接口:
//todo 点赞功能实现 @PutMapping("/like/{id}") public Result likeBlog(@PathVariable("id") Long id) { return blogService.likeBlog(id); }
service层实现功能 :
@Override public Result likeBlog(Long id) { //1. 获取当前用户 Long userId = UserHolder.getUser().getId(); //2. 判断当前用户是否点赞 String key = "blog:liked" + id; Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString()); //3.如果未点赞,可以实现点赞 if(BooleanUtil.isFalse(isMember)){ //3.1 数据库点赞数 + 1 boolean isSuccess = update() .setSql("liked = liked + 1").eq("id", id).update(); //3.2 保存用户到Redis的set集合中 if(isSuccess){ stringRedisTemplate.opsForSet().add(key,userId.toString()); } } //4. 如果已经点过赞,点击的话就会 取消点赞 else { //4.1数据库点赞数 - 1 boolean isSuccess = update() .setSql("liked = liked - 1").eq("id", id).update(); //4.2 将用户从点赞列表中移除 if(isSuccess){ stringRedisTemplate.opsForSet().remove(key,userId.toString()); } } return Result.ok(); }
点赞了就会出现红色,同时redis中就会保存点赞用户的id及其作品
当我们再次按点赞按钮的时候,它的点赞数就会减一,同时redis中保存的数据也会同步的删除
点赞排行榜功能
仿微信实现早点赞的先排在前面
需求:按照点赞时间先后排序,返回Top5的用户
功能实现
- 根据传入的blog的id查询出前几名的用户id
- 根据用户id查询出用户
- 再将user转换成为userDTO为了保护用户的信息安全
/** * 点赞用户列表查询 * @param id * @return */ @Override public Result queryBlogLikes(Long id) { //1. 查询点赞前 top5的用户 String key = "blog:liked" + id; Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); if(top5 == null || top5.isEmpty()){ return Result.ok(Collections.emptyList()); } //解析出用户id List<Long> userIds = top5.stream().map(Long::valueOf).collect(Collectors.toList()); /** * 针对点赞用户排序的问题的改进 */ String idStr = StrUtil.join(",", userIds); //根据用户id查询用户 //List<UserDTO> userDTOs = userService.listByIds(userIds).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); List<UserDTO> userDTOS = userService.query().in("id", userIds).last("ORDER BY FIELD(id," + idStr + ")").list(). stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); //返回 return Result.ok(userDTOS); }
2023.3.5 —–关注
实现接口 :
实现关注和取关的接口,对于博主和关注者之间使用一张关联表
@RestController @RequestMapping("/follow") public class FollowController { @Resource private IFollowService followService; //todo 关注还是取关 @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long followUserId ,@PathVariable("isFollow") boolean isFollow){ return followService.follow(followUserId,isFollow); } //todo 判断是否关注 @GetMapping("/or/not/{id}") public Result isFollow(@PathVariable("id") Long followUserId){ return followService.isFollow(followUserId); } }
service层实现
@Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { //todo 关注还是取关 @Override public Result follow(Long followUserId, boolean isFollow) { //1. 获取登录的用户 Long userId = UserHolder.getUser().getId(); //1。 判断到底是关注还是取关 if(isFollow){ Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); save(follow); } //取消关注 else{ remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",followUserId)); } return Result.ok(); } /** * 查询用户是否关注 * @param followUserId * @return */ @Override public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count(); return Result.ok(count > 0); //如果大于0就代表关注 } }
共同关注
点击用户头像查看个人主页
点击头像查看用户笔记实现
J
/** * 根据当前用户查询它的博客 * @param current * @param id * @return */ @GetMapping("/of/user") public Result queryBolgByUserId(@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); }
/** * 根据用户id查询用户 * @param userId * @return */ @GetMapping("/{id}") public Result queryUserById(@PathVariable("id") Long userId){ //查询用户 User user = userService.getById(userId); if(user == null){ return Result.ok(); } //转换为UserDTO UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); return Result.ok(userDTO); }
共同关注查看
需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
两个用户的交集,也就是两个set集合的交集
首先将关注的用户存入Redis中
@Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { @Resource private StringRedisTemplate stringRedisTemplate; //todo 关注还是取关 @Override public Result follow(Long followUserId, boolean isFollow) { //1. 获取登录的用户 Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; //1。 判断到底是关注还是取关 /** * 当需要关注时,加入redis */ if(isFollow){ Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSecc = save(follow); if(isSecc){ //把当前用户的id,放入redis的set集合中 stringRedisTemplate.opsForSet().add(key,followUserId.toString()); } } //取消关注 else{ boolean isSucc = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId)); if (isSucc){ //把关注的用户id从redis中移除 stringRedisTemplate.opsForSet().remove(key,followUserId.toString()); } } return Result.ok(); } }
实现共同关注
实现接口:
/** * 查找目标用户和当前用户(此时登录的)的(共同关注)交集 * @param id * @return */ @GetMapping("/common/{id}") public Result followCommons(@PathVariable("id")Long id){ return followService.followCommons(id); }
service层实现
/** * 查询当前用户和目标用户的共同关注 * @param id 目标用户的id * @return */ @Override public Result followCommons(Long id) { //1. 获取当前用户 Long userId = UserHolder.getUser().getId(); String key1 = "follows:" + userId;//当前用户 String key2 = "follows:" + id; //目标用户的key //2. 求交集 Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2); //没有交集的情况 if(intersect == null || intersect.isEmpty()){ return Result.ok(Collections.emptyList()); } //有交集 List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); List<UserDTO> userDTOS = service.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class) ).collect(Collectors.toList()); return Result.ok(userDTOS); }
关注推送实现
关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流模式
拉模式
张三、李四、王五三个人假设都发了微博,赵六关注了李四和张三,当赵六刷新信息的时候。它的收件箱就会拉去他关注的人发的微博
然后再收件箱中对赵六关注的人发的微博按照时间戳进行排序,最终得到按照时间的微博
如果赵六关注的人比较多,那么拉去微博就会很慢,非常耗内存
读模式
将微博直接全部推送给每一个粉丝,然后获取
但是这样对于粉丝多的来说就会非常耗内存
推拉混合模式
推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。
使用两种结合的方式,就可以完美解决两种冲突的问题
需求实现:
1.修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
@PostMapping public Result saveBlog(@RequestBody Blog blog) { return blogService.saveBlog(blog); }
service层实现
2.收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
//如果数据有变化的话,最好不要使用list来做排序 /** * 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱 * @param blog * @return */ @Override public Result saveBlog(Blog blog) { //1. 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); //2. 保存探店博文 boolean succ = save(blog); if(!succ){ return Result.fail("笔记保存失败!"); } //3. 查询笔记作者的所有粉丝 //sql语句 : select * from tb_follow where follow_user_id = ? List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list(); //4. 推送笔记id个所有的粉丝 for (Follow follow : follows) { //获取每一个粉丝 Long userId = follow.getUserId(); //推送,收件箱 key粉丝的id String key = "feeds:" + userId; //推送笔记,按时间戳排序 stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis()); } // 返回id return Result.ok(blog.getId()); }
3.查询收件箱数据时,可以实现分页查询
实现接口 :
两个重点:
- 每次查询完要分析出本次查询的最小时间戳 ,这将作为下一次查询的起始(最大值)
- 找到查询出的最小元素的个数,并将其跳过,从下一个开始查
2023.3.6—–实现用户签到
对于redis中的GEO数据结构
例题: 搜索天安门( 116.397904 39.909005 )附近10km内的所有火车站,并按照距离升序排序
实现方法:
官网文档:
查询的中心点由以下强制选项之一提供:
- FROMMEMBER:使用给定的现有在排序集中的位置。<member>
- FROMLONLAT:使用给定的和位置。<longitude>``<latitude>
查询的形状由以下强制选项之一提供:
- BYRADIUS:与GEORADIUS类似,根据给定的圆形区域搜索。<radius>
- BYBOX:在轴对齐的矩形内搜索,由 和 确定。<height>``<width>
该命令可选择使用以下选项返回其他信息:
- WITHDIST:同时返回返回的项目与指定中心点的距离。距离以为半径或高度和宽度参数指定的相同单位返回。
- WITHCOORD:同时返回匹配项的经度和纬度。
- WITHHASH:还以 52 位无符号整数的形式返回项目的原始地理哈希编码排序集分数。这仅对低级黑客或调试有用,否则对一般用户兴趣不大。
默认情况下,匹配项返回时未排序。要对它们进行排序,请使用以下两个选项之一:
- ASC:相对于中心点,从最近到最远对返回的项目进行排序。
- DESC:相对于中心点,从最远到最近对返回的项目进行排序。
默认情况下,将返回所有匹配项。若要将结果限制为前 N 个匹配项,请使用 COUNT<count> 选项。 使用该选项时,一旦找到足够的匹配项,该命令就会返回。这意味着返回的结果可能不是最接近指定点的结果,但服务器为生成它们而投入的工作量要少得多。 如果未提供,该命令将执行与指定区域匹配的项目数成比例的工作并对其进行排序, 因此,即使只返回几个结果,使用非常小的选项查询非常大的区域也可能很慢。ANY``ANY``COUNT
GEO数据结构用于解决附近商户查询的问题,但是由于项目目前使用的数据都是虚假数据。对于真实业务的逻辑及其实现思路都不太清除,所以次模块未完成
用bitMap实现用户签到
bitMap的用法
实现用户签到
@PostMapping("/sign") public Result sign(){ return userService.sign(); }
service层实现
/** * todo 实现用户签到 * @return */ @Override public Result sign() { //1. 获取用户 Long userId = UserHolder.getUser().getId(); //2. 获取日期 LocalDateTime now = LocalDateTime.now(); //3. 拼接key String key ="sign:" + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM")); //4. 获取今天是本月的第几天 int nowDayOfMonth = now.getDayOfMonth(); //5. 写入redis setbit key offset 1 stringRedisTemplate.opsForValue().setBit(key,nowDayOfMonth - 1,true); return Result.ok(); }
统计当前用户连续签到天数
@PostMapping("/sign/count") public Result signCount(){ return userService.signCount(); }
service层
@Override public Result signCount(){ //1. 获取用户 Long userId = UserHolder.getUser().getId(); //2. 获取日期 LocalDateTime now = LocalDateTime.now(); //3. 拼接key String key ="sign:" + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM")); //4. 获取今天是本月的第几天 int nowDayOfMonth = now.getDayOfMonth(); //5. 获取本月到今天为止所有的签到记录 List<Long> result = stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(nowDayOfMonth)).valueAt(0) ); if(result == null || result.isEmpty()){ return Result.ok(0); } Long num = result.get(0); if(num == null || num == 0){ return Result.ok(0); } int count = 0; //循环遍历 while(true){ if((num & 1)==0){ //未签到,跳出循环 break; }else{ //已签到 ,计数器+1 count++; } num >>>= 1;//二进制右移 } return Result.ok(count); }