2022黑马Redis跟学笔记.实战篇(六)

news2024/11/16 0:33:29

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 {

    @Resource
    private IBlogService blogService;

    @PostMapping
    public Result saveBlog(@RequestBody Blog blog) {
        //获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUpdateTime(user.getId());
        //保存探店博文
        blogService.saveBlog(blog);
        //返回id
        return 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 {
    @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);
    }

    /**
     * @param
     * @return com.hmdp.dto.Result
     * @description //查看笔记详情页面
     * @param: id 笔记id
     * @date 2023/2/17 22:07
     * @author wty
     **/
    @Override
    public Result queryBlogById(Long id) {
        // 1.查询blog
        Blog 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 {
    @Resource
    private IUserService userService;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @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(new Consumer<Blog>() {
            @Override
            public 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
     **/
    @Override
    public Result queryBlogById(Long id) {
        // 1.查询blog
        Blog 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:" + id
        String 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
     **/
    @Override
    public Result likeBlog(Long id) {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.判断当前登录用户是否点赞 key  "blog:liked:" + id
        String 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数据库点赞数 -1
            boolean 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 {
    @Resource
    private IUserService userService;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @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(new Consumer<Blog>() {
            @Override
            public 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
     **/
    @Override
    public Result queryBlogById(Long id) {
        // 1.查询blog
        Blog 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:" + id
        String 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:" + id
        String 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:" + id
        String 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数据库点赞数 -1
            boolean 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
     **/
    @Override
    public Result likeBlog(Long id) {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.判断当前登录用户是否点赞 key  "blog:liked:" + id
        String 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 member
                stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
            }

        } else {
            // 4.如果已经点赞,取消点赞
            // 4.1数据库点赞数 -1
            boolean 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
     **/
    @Override
    public Result queryBlogLikes(Long id) {
        String key = RedisConstants.BLOG_LIKED_KEY + id;
        // 1.查询top5的点赞用户 zrange key 0 4
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (null == top5 || top5.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }

        // 2.解析出其中的用户id
        List<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 {
    @Autowired
    private 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
     **/
    @Override
    public 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
     **/
    @Override
    public Result isFollow(Long followUserId) {
        // 1.获取用户id
        Long 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
     **/
    @Override
    public 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 followUserId
                stringRedisTemplate.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 followUserId
                stringRedisTemplate.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
     **/
    @Override
    public 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

    @PostMapping
    public 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
     **/
    @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 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获取粉丝id
            Long fansId = follow.getUserId();
            // 4.2推送给粉丝
            String key = RedisConstants.FEED_KEY + fansId;
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
        }


        // 5.返回id
        return 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
     **/
    @Override
    public 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 5
        Set<ZSetOperations.TypedTuple<String>> set = stringRedisTemplate
                .opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2L);

        // 3.非空判断
        if (null == set || set.isEmpty()) {
            return Result.ok();
        }

        // 4.解析收件箱的数据:blogId,minTime(时间戳),offset
        ArrayList<Long> ids = new ArrayList<>(set.size());
        long minTime = Long.MAX_VALUE;
        int off = 1;
        for (ZSetOperations.TypedTuple<String> tuple : set) {
            // 4.1获取id
            ids.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查询blog
        List<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);
    }

重启应用,看到可可发的动态了
在这里插入图片描述

再换一个人关注发个动态,一样也查询到了
在这里插入图片描述

滚动分页也实现了
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA 双亲委派

双亲委派 问题&#xff1f; 什么是双亲委派&#xff1f;为什么需要双亲委派&#xff0c;不委派有什么问题&#xff1f;"父加载器"和"子加载器"之间的关系是继承的吗&#xff1f;双亲委派是怎么实现的&#xff1f;我能不能主动破坏这种双亲委派机制&#x…

nginx平滑升级

1.平滑升级操作1.1 备份安装目录下的nginxcd /usr/local/nginx/sbin mv nginx nginx.bak1.2 复制objs目录下的nginx到当前sbin目录下cp /opt/software/nginx/nginx-1.20.2/objs/nginx /usr/local/nginx/sbin/1.3 发送信号user2给nginx老版本对应的进程kill -user2 more /usr/lo…

【TypeScript】TypeScript的基础类型(string,number,boolean,void,null,undefined):

文章目录一、安装【1】安装npm install typescript -g【2】基础类型&#xff1a;Boolean、Number、String、null、undefined 以及 ES6 的 Symbol 和 ES10 的 BigInt二、字符串类型(string)三、数字类型(number)四、布尔类型(boolean)五、空值类型(void)六、null和undefined类型…

初探Mysql反向读取文件

前言 Mysql反向读取文件感觉蛮有意思的&#xff0c;进行了解过后&#xff0c;简单总结如下&#xff0c;希望能对在学习Mysql反向读取文件的师傅有些许帮助。 前置知识 在Mysql中存在这样一条语句 LOAD DATA INFILE它的作用是读取某个文件中的内容并放置到要求的表中&#x…

IOS崩溃文件符号化实践

1.背景与项目难点 1.1 背景 由于公司之前使用的友盟要收费&#xff0c;filebase服务由谷歌提供&#xff0c;存在数据合规风险。需要实现稳定性分析功能&#xff0c;通过支持app崩溃信息实时采集、实时上报、实时自动解析并定位出代码问题&#xff0c;帮助研发同学及时定位崩溃…

Java 布隆过滤器

你在么&#xff1f;在&#xff01;一定在么&#xff1f;不在&#xff01;一定不在么&#xff1f; 你想要100%的准去性&#xff0c;还是99%的准确性附带较高的速度和较小的资源消耗。 任何算法&#xff0c;任何经营收到的背后&#xff0c;都是时间效益 资源消耗 准确性的平衡&am…

ASO优化之如何更新APP

ASO是一个持续的迭代过程&#xff0c;应用商店排名和热门关键词每天都在变化&#xff0c;为了跟上应用行业快节奏的性质&#xff0c;我们必须灵活地制定应用商店的优化策略&#xff0c;并时常的更新我们的应用。 那我们该如何做到这一点呢&#xff1f; 如果是刚上新的应用&am…

93.【Vue-细刷-02】

Vue-02(十六)、基本列表渲染 (v-for)1.使用v-for遍历数组2.使用v-for遍历对象3.使用v-for遍历字符串(十七)、列表过滤 (filter())1.⭐JS中Change属性的原生状态⭐2.使用watch监听实现3.const {xxx} this 在Vue的作用⭐⭐4.JS箭头函数参数的简写⭐5.使用computed进行计算实现(最…

15_FreeRtos计数信号量优先级翻转互斥信号量

目录 计数型信号量 计数型信号量相关API函数 计数型信号量实验源码 优先级翻转简介 优先级翻转实验源码 互斥信号量 互斥信号量相关API函数 互斥信号量实验源码 计数型信号量 计数型信号量相当于队列长度大于1的队列&#xff0c;因此计数型信号量能够容纳多个资源,这在…

家庭理财,轻松记账修改收支记录这样操作

我们在记账的时候难免会出现记错或者想修改的地方&#xff0c;又或者是想将之前太久没有用的记账记录删除掉&#xff0c;今天&#xff0c;小编就教大家如何修改收支记录&#xff0c;一起接着往下看吧&#xff01; 第一步&#xff0c;运行【晨曦记账本】在软件主界面中&#xff…

分享111个HTML娱乐休闲模板,总有一款适合您

分享111个HTML娱乐休闲模板&#xff0c;总有一款适合您 111个HTML娱乐休闲模板下载链接&#xff1a;https://pan.baidu.com/s/1mqmJLctj9oQbJt6Oo8IuBA?pwdep3t 提取码&#xff1a;ep3t Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 响应式美容养生服务行业…

Yolo系列之YOLOv1 YOLOv2

一、YOLOv1 1. YOLOv1概述 YOLOv1是单阶段目标检测方法,不需要像Faster RCNN这种两阶段目标检测方法一样,需要生成先验框。Yolo算法采用一个单独的CNN模型实现end-to-end的目标检测。虽然Faster-RCNN在当时是mAP最高的算法(2015-2016年), 然而速度却很慢,相对而言,Yol…

Java智慧校园平台源码:SaaS模式智慧校园运营云平台源码

校班务管理&#xff1a;评价管理&#xff1a; 1.web端/教师端小程序编辑点评 多元化评价&#xff0c;捕捉学生闪光点全方位评价&#xff0c;自定义评价类型、 评价信息实时推送至家长、AI智能点评 班级报表一键导出&#xff0c;智能评测学生在校表现&#xff0c;老师、家长实…

算法的时间复杂度

算法在编写成可执行程序后&#xff0c;运行时需要消耗时间资源和空间&#xff08;内存&#xff09;资源&#xff0c;因此衡量一个算法的好坏&#xff0c;一般是从时间和空间两个维度来衡量的。 时间复杂度主要衡量一个算法运行的快慢&#xff0c;而空间复杂度主要衡量一个算法运…

Qt Desginer布局方法

首先将我们需要的控件拖拽到一个合适的位置&#xff0c;该例子中用到了两个label&#xff0c;两个lineEdit和两个pushButton。 然后我们需要利用弹簧来控制控件到控件之间的距离以及控件到窗体边界的距离&#xff0c;因为这里只有一组控件&#xff08;两个label&#xff0c;两个…

学板绘课程学费一般多少钱

学板绘课程学费一般多少钱&#xff1f;培训机构的费用和师资、模式有关&#xff0c;价格贵不贵要结合相同类型的机构多多对比。因为好些平台做了很多的宣传广告&#xff0c;运营成本很高&#xff0c; 终羊毛出在羊身上&#xff0c;这样的机构知名度很高&#xff0c;但是性价比不…

untiy 录制网络摄像头视频并保存到本地文件

网络摄像头使用的是海康威视的&#xff0c;关于如何使用Ump插件播放海康威视rtsp视频流&#xff0c;请参考我的这篇文章 内部有ump插件的下载链接 untiy接入 海康威视网络摄像头 录屏使用的插件是 AVPro movieCapture 4.6.3版&#xff0c; 插件和完整工程的下载链接放在本文的…

eclipse创建第一个java web项目并运行

为了能编写java web项目&#xff0c;建议安装支持javaee开发的eclipse版本。1.下载eclipse地址&#xff1a;https://www.eclipse.org/downloads/packages/release/2021-03/r2.解压后启动eclipse3.新建java web工程设置项目名称&#xff0c;指定tomcat的版本及路径4. 添加一个js…

Python快速入门系列之一:Python对象

Python对象1. 列表&#xff08;list&#xff09;2. 元组&#xff08;tuple&#xff09;3. 字典&#xff08;dict&#xff09;4. 集合&#xff08;set&#xff09;5. 字符串&#xff08;string&#xff09;6. BIF &#xff08;Built-in Function&#xff09;7. 列表、集合以及字…

小笔记:gitlab配置文件 /etc/gitlab/gitlab.rb 配置项

小笔记&#xff1a;gitlab配置文件 /etc/gitlab/gitlab.rb 配置项CSDN账号 jcLee95 &#xff1a;https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/12…