Day81(7)-F:\code\hm-dianping\hm-dianping

黑马点评

达人探店

发布探店笔记

image-20260114133517478

image-20260114134741311

@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);
}

存储地址需要更改

package com.hmdp.utils;

public class SystemConstants {
    public static final String IMAGE_UPLOAD_DIR = "F:\\lesson\\hmdp\\nginx-1.18.0\\html\\hmdp\\imgs";
    public static final String USER_NICK_NAME_PREFIX = "user_";
    public static final int DEFAULT_PAGE_SIZE = 5;
    public static final int MAX_PAGE_SIZE = 10;
}
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
    // 获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 保存探店博文
    blogService.save(blog);
    // 返回id
    return Result.ok(blog.getId());
}

查询探店笔记

image-20260114135729297

知识点

  1. 实体类中属性加上@TableField说明该属性不属于这个实体类@TableName("tb_blog")对应的表的信息
package com.hmdp.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 * 
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_blog")
public class Blog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 商户id
     */
    private Long shopId;
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 用户图标
     */
    @TableField(exist = false)
    private String icon;
    /**
     * 用户姓名
     */
    @TableField(exist = false)
    private String name;
    /**
     * 是否点赞过了
     */
    @TableField(exist = false)
    private Boolean isLike;

    /**
     * 标题
     */
    private String title;

    /**
     * 探店的照片,最多9张,多张以","隔开
     */
    private String images;

    /**
     * 探店的文字描述
     */
    private String content;

    /**
     * 点赞数量
     */
    private Integer liked;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


}

查询blog-controller

@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
    return blogService.queryHotBlog(current);
}

@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id){
    return blogService.queryBlogById(id);
}

查询blog-service(39)

package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private IUserService userService;
    @Override
    public 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);
    }

    @Override
    public Result queryBlogById(Long id) {
        //1.查询blog
        Blog blog = getById(id);
        if (blog == null){
            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());
    }
}

点赞功能

image-20260114141955846

分页查询blog并将blog加上isLiked属性-controller

@PutMapping("/like/{id}")
    public Result likeBlog(@PathVariable("id") Long id) {
        // 修改点赞数量  update tb_blog set liked + 1 where id = ?
//        blogService.update()
//                .setSql("liked = liked + 1").eq("id", id).update();
        return blogService.likeBlog(id);
    }

分页查询blog并将blog加上isLiked属性-service

redis中存set,记录每个blog被谁点过赞,key为blog的id,value为点赞的userId

package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private IUserService userService;
    @Override
    public 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);
        records.forEach(blog -> {
            this.queryBlogUser(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    @Override
    public Result queryBlogById(Long id) {
        //1.查询blog
        Blog blog = getById(id);
        if (blog == null){
            return Result.fail("笔记不存在");
        }
        //2.查询blog有关的用户
        queryBlogUser(blog);
        //3.查询blog是否被点赞
        isBlogLiked(blog);
        return Result.ok(blog);
    }

    private void isBlogLiked(Blog blog) {
        //1.获取登录用户
        Long userId = UserHolder.getUser().getId();
        //如果未登录
        //2.判断当前登录用户是否已经点赞
        String key = BLOG_LIKED_KEY + blog.getId();
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        blog.setIsLike(BooleanUtil.isTrue(isMember));
    }

    @Override
    public Result likeBlog(Long id) {
        //1.获取登录用户
        Long userId = UserHolder.getUser().getId();
        //2.判断当前登录用户是否已经点赞
        String key = BLOG_LIKED_KEY + id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        //不要直接判断包装类
        if (BooleanUtil.isFalse(isMember)) {
            //3.如果未点赞,可以点赞
            //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());
            }
        }else {
            //4.如果已点赞,取消点赞
            //4.1.数据库点赞数-1
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            //4.2.把用户从redis的set集合里面移除
            if (isSuccess){
                stringRedisTemplate.opsForSet().remove(key,userId.toString());
            }
        }
        return Result.ok();
    }

    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}

点赞排行榜

image-20260114145754468

image-20260114145929272

将点赞时间按时间戳作为sortedset的score

通过zscore获取对应元素的score替代zset里面没有sismember的功能

通过zrange获取按照score排序的对应序列的值

技术栈

  1. 通过zset存储点赞信息,zscore判断set内有没有该元素,zrange获取按照score排序后的前几名
1.通过zset存储点赞信息(26),zscore判断set内有没有该元素(7、18),zrange获取按照score排序后的前几名
private void isBlogLiked(Blog blog) {
    //1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    //如果未登录
    //2.判断当前登录用户是否已经点赞
    String key = BLOG_LIKED_KEY + blog.getId();
    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
    blog.setIsLike(score != null);
}

@Override
public Result likeBlog(Long id) {
    //1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    //2.判断当前登录用户是否已经点赞
    String key = BLOG_LIKED_KEY + id;
    //Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
    //不要直接判断包装类
    if (score == null) {
        //3.如果未点赞,可以点赞
        //3.1.数据库点赞数+1
        boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
        //3.2.保存用户到我们的redis的zset集合 zadd key value score
        if (isSuccess){
            stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
        }
    }else {
        //4.如果已点赞,取消点赞
        //4.1.数据库点赞数-1
        boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
        //4.2.把用户从redis的zset集合里面移除
        if (isSuccess){
            stringRedisTemplate.opsForZSet().remove(key,userId.toString());
        }
    }
    return Result.ok();
}

private void queryBlogUser(Blog blog) {
    Long userId = blog.getUserId();
    User user = userService.getById(userId);
    blog.setName(user.getNickName());
    blog.setIcon(user.getIcon());
}

知识点

1.SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE id IN ( 5 , 1 )

这样就先查的1再查5

技术栈

  1. 通过自定义sql语句,改进由于WHERE id IN ( 5 , 1 )返回结果倒置问题,涉及将list集合内的数据拼接为字符串StrUtil.join
1.通过自定义sql语句,改进由于WHERE id IN ( 5 , 1 )返回结果倒置问题,涉及将list集合内的数据拼接为字符串StrUtil.join(11-18)
@Override
public Result queryBlogLikes(Long id) {
    String key = BLOG_LIKED_KEY + id;
    //1.查询top5的点赞用户  zrange key 0 4
    Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
    if (top5 == null || top5.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    //2.解析出其中的用户id
    List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
    String idStr = StrUtil.join(",", ids);
    //3.根据用户id查询用户 SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE id IN ( 5 , 1 )
    //List<UserDTO> userDTOS = userService.listByIds(ids)
    List<UserDTO> userDTOS = userService.query()
            .in("id",ids).last("ORDER BY FIELD(id,"+idStr+")").list()
            .stream()
            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
            .collect(Collectors.toList());
    //4.返回
    return Result.ok(userDTOS);
}

好友关注

image-20260114154707638

image-20260114154745201

技术栈

  1. 将表的id改为自增长
  2. new QueryWrapper<Follow>()是一个条件构造器
1.将表的id改为自增长

右键表命选择“Modify Table”,点击“id”,勾选“Auto Increment”

2.new QueryWrapper<Follow>()是一个条件构造器(37-38)

QueryWrapper:专门用于封装查询(Select)和删除(Delete)条件的一类。

<Follow>: 泛型标识,指定这个构造条件器是为Follow实体类(对应数据库的follow表)服务的。

.eq("列名", 值):eqEqual的缩写,表示“相等”。

  • 第一个参数是数据库表中的字段名(通常是下划线命名)。
  • 第二个参数是数值
package com.hmdp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.Follow;
import com.hmdp.mapper.FollowMapper;
import com.hmdp.service.IFollowService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        //1.获取登录用户
        Long userId = UserHolder.getUser().getId();

        //2.判断到底是关注还是取关
        if (isFollow){
            //3.1.关注,新增数据
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            save(follow);
        }else {
            //3.2.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
            remove(new QueryWrapper<Follow>()
                    .eq("user_id",userId).eq("follow_user_id",followUserId));
        }
        return Result.ok();
    }

    @Override
    public Result isFollow(Long followUserId) {
        //1.获取登录用户
        Long userId = UserHolder.getUser().getId();
        //2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?
        Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
        //3.判断,该登录用户有没有人关注这个人
        return Result.ok(count>0);
    }
}

优化

// 这种写法效果一样,但更推荐
remove(new LambdaQueryWrapper()
.eq(Follow::getUserId, userId)
.eq(Follow::getFollowUserId, followUserId));

共同关注

image-20260114161540762

image-20260114161958662

改造,将关注列表存在redis里面:key是当前登录的用户id,值是我当前关注的所有用户的id

改造:加上redis(17、25):redis里面存的key是关注其他人的用户,value是被关注的人,也就是key关注了哪些value

@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result follow(Long followUserId, Boolean isFollow) {
    //1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    //2.判断到底是关注还是取关
    if (isFollow){
        //3.1.关注,新增数据
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        boolean isSuccess = save(follow);
        if (isSuccess){
            //把关注用户的id,放入redis的set集合 sadd userId followerUserId
            stringRedisTemplate.opsForSet().add(key,followUserId.toString());
        }
    }else {
        //3.2.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
        boolean isSuccess = remove(new QueryWrapper<Follow>()
                .eq("user_id", userId).eq("follow_user_id", followUserId));
        //把关注用户的id从redis中移除
        if (isSuccess){
            stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
        }
    }
    return Result.ok();
}

image-20260114163625874

共同关注-controller

@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id){
    return followService.followCommons(id);
}

共同关注-service

@Override
public Result followCommons(Long id) {
    //求目标用户和当前用户的交集,因为是在目标用户主页查看的共同关注
    //1.获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    //2.求交集
    String key2 = "follows:" + id;
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
    if (intersect == null||intersect.isEmpty()){
        //无交集
        return Result.ok(Collections.emptyList());
    }
    //3.解析出id
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    //4.查询用户,返回DTO
    List<UserDTO> users = userService.listByIds(ids)
            .stream()
            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
            .collect(Collectors.toList());
    return Result.ok(users);
}

关注推送

image-20260114165623931

image-20260114170559038

image-20260114171126255

image-20260114171457079

image-20260114172434175

image-20260114172452488

推模式关注推送

image-20260114172721702

image-20260114173107302

image-20260114173412847

技术栈

  1. 将blog信息保存到数据库之后,保存到redis中对应粉丝为key的收件箱feed中(11-20)
@Override
public 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 * 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) {
        //4.1.获取粉丝id
        Long userId = follow.getUserId();
        //4.2.推送
        String key = "feed:" + userId;
        stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
    }
    // 5.返回id
    return Result.ok(blog.getId());
}

滚动分页查询

image-20260114184331841

如果中途有人修改了score导致改成上一次的最后一个score的值,分数重复,那就不能只跳过一个;需要跳过和上一次最后一个score大小一样的个数

滚动查询分页参数

max:当前时间戳|上一次查询的最小时间戳

min:0

offset:0|与上一次结果查询到的最小score的个数

count:固定

image-20260114184918023

技术栈

  1. 基于mybatis自带的listByIds()导致的倒置问题的优化
  2. minTime(时间戳)、offset的循环覆盖(设计算法)
1.基于mybatis自带的listByIds()导致的倒置问题的优化(31-34)
2.minTime(时间戳)、offset的循环覆盖(设计算法)(14-28)
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    //1.获得当前用户
    Long userId = UserHolder.getUser().getId();
    //2.获取当前用户的收件箱 ZREVANGEBYSCORE  key Max Min
    String key = FEED_KEY + userId;
    //TypedTuple<String>元组
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
            .reverseRangeByScoreWithScores(key, 0, max, offset, 3);
    //3.非空判断
    if (typedTuples == null||typedTuples.isEmpty()){
        return Result.ok();
    }
    //4.解析数据:blogId、minTime(时间戳)、offset
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0;
    int os = 1;
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
        //4.1.获取id,因为一开始存进去的value就是id的String格式
        ids.add(Long.valueOf(typedTuple.getValue()));
        //4.2.获取分数,循环覆盖
        long time = typedTuple.getScore().longValue();
        if (time == minTime){
            os++;
        }else {
            minTime = time;
            os = 1;
        }
    }
    //5.根据id查询blog
    //不能直接用mybatis自带的listByIds()里面是基于where in (?,?,?)会使得查出来的倒置
    String idStr = StrUtil.join(",", ids);
    List<Blog> blogs = query()
            .in("id",ids).last("ORDER BY FIELD(id,"+idStr+")").list();

    for (Blog blog : blogs) {
        //5.1.查询blog有关的用户
        queryBlogUser(blog);
        //5.2.查询blog是否被点赞
        isBlogLiked(blog);
    }
    //6.封装并返回
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);
    return Result.ok(r);
}
posted @ 2026-01-15 17:04  David大胃  阅读(1)  评论(0)    收藏  举报