Redis从入门到精通(十)Redis实战(七)达人探店、点赞与点赞排行榜

↑↑↑请在文章开头处下载测试项目源代码↑↑↑

前言

Redis实战系列文章:

Redis从入门到精通(四)Redis实战(一)短信登录
Redis从入门到精通(五)Redis实战(二)商户查询缓存
Redis从入门到精通(六)Redis实战(三)优惠券秒杀
Redis从入门到精通(七)Redis实战(四)库存超卖、一人一单与Redis分布式锁
Redis从入门到精通(八)Redis实战(五)分布式锁误删与原子性问题、Redisson
Redis从入门到精通(九)Redis实战(六)基于Redis队列实现异步秒杀下单

4.8 达人探店

4.8.1 发布探店笔记

探店笔记类似点评网站的评价,往往是图文结合。

4.8.1.1 创建表和实体类
-- 探店笔记表,包含笔记中的标题、问题、图片等
CREATE TABLE `tb_blog`  (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
  `shop_id` BIGINT(20) NOT NULL COMMENT '商户id',
  `user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '用户id',
  `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',
  `images` VARCHAR(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '探店的照片,最多9张,多张以逗号隔开',
  `content` VARCHAR(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '探店的文字描述',
  `liked` INT(8) UNSIGNED NULL DEFAULT 0 COMMENT '点赞数量',
  `comments` INT(8) UNSIGNED NULL DEFAULT NULL COMMENT '评论数量',
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 23 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

创建表对应的实体类:

// com.star.redis.dzdp.pojo.Blog

@Data
@EqualsAndHashCode(callSuper = false)
@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;
    /**
     * 标题
     */
    private String title;

    /**
     * 探店的照片,最多9张,多张以","隔开
     */
    private String images;
    /**
     * 探店的文字描述
     */
    private String content;
    /**
     * 点赞数量
     */
    private Integer liked;
    /**
     * 评论数量
     */
    private Integer comments;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;
}
4.8.1.2 实现图片上传功能

创建一个UploadController类,编写uploadImage()方法实现图片上传功能。其接口文档及代码如下:

项目说明
请求方法POST
请求路径/upload/image
请求参数file<MultipartFile>
返回值文件名<String>
// com.star.redis.dzdp.controller.UploadController

@Slf4j
@RestController
@RequestMapping("/upload")
public class UploadController {

    /**
     * 上传图片
     * @author hsgx
     * @since 2024/4/7 18:31
     * @param image 
     * @return com.star.redis.dzdp.pojo.BaseResult<java.lang.String>
     */
    @PostMapping("/image")
    public BaseResult<String> uploadResult(@RequestParam("file")MultipartFile image) {
        try {
            // 1.获取原始文件名称
            String originalFilename = image.getOriginalFilename();
            log.info("originalFilename = {}", originalFilename);
            // 2.生成新文件名
            String newFileName = createNewFileName(originalFilename);
            log.info("newFileName = {}", newFileName);
            // 3.保存文件
            image.transferTo(new File("D:\\document", newFileName));
            // 4.返回结果
            log.info("文件上传成功...");
            return BaseResult.setOkWithData(newFileName);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("文件上传失败", e);
        }
    }

    /**
     * 生成新文件名
     * @author hsgx
     * @since 2024/4/7 18:24
     * @param originalFilename
     * @return java.lang.String
     */
    private String createNewFileName(String originalFilename) {
        // 1.获取后缀
        String suffix = StrUtil.subAfter(originalFilename, ".", true);
        // 2.判断目录是否存在,不存在时创建目录
        String name = UUID.randomUUID().toString();
        int hash = name.hashCode();
        int d1 = hash & 0xF;
        int d2 = (hash >> 4) & 0xF;
        // 一般会把目录路径配置到配置文件
        File dir = new File("D:\\document", StrUtil.format("/blogs/{}/{}", d1, d2));
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // 3.生成文件名
        return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);
    }

}

注意,在代码中把目录路径进行了硬编码,实际开发中应配置到配置文件中。下面进行功能测试:

日志打印如下:

[http-nio-8081-exec-3] originalFilename = 9001.png
[http-nio-8081-exec-3] newFileName = /blogs/7/3/a0518076-2e4f-45bf-8f77-0731fdfede00.png
[http-nio-8081-exec-3] 文件上传成功...

此时在文件夹下可以看到这张上传的图片:

4.8.1.3 实现新增探店笔记功能

创建实体类Blog对应的BlogController类-IBlogService接口-BlogServiceImpl实现类-BlogMapper类。详见测试项目代码。

在BlogController类中创建一个add()方法,用于新增探店笔记。其接口文档及代码如下:

项目说明
请求方法POST
请求路径/blog/add
请求参数blog<Blog>
返回值笔记ID<Long>
// com.star.redis.dzdp.controller.BlogController

@Slf4j
@RestController
@RequestMapping("/blog")
public class BlogController {

    @Resource
    private IBlogService blogService;

    /**
     * 新增探店笔记
     * @author hsgx
     * @since 2024/4/7 18:46
     * @param blog
     * @param request
     * @return com.star.redis.dzdp.pojo.BaseResult<java.lang.Long>
     */
    @PostMapping("/add")
    public BaseResult<Long> add(@RequestBody Blog blog, HttpServletRequest request) {
        log.info("add {}", blog.toString());
        // 1.设置探店笔记为登录用户的笔记
        Long userId = (Long) request.getAttribute("userId");
        blog.setUserId(userId);
        // 2.保存探店笔记
        blogService.save(blog);
        // 3.返回
        return BaseResult.setOkWithData(blog.getId());
    }
}

功能测试:

日志打印如下:

add Blog(id=null, shopId=1, userId=null, title=太好吃了吧!!!, images=/blogs/7/3/a0518076-2e4f-45bf-8f77-0731fdfede00.png, content=这家店太好吃了,强烈推荐大家!!, liked=null, comments=null, createTime=null, updateTime=null)
[http-nio-8081-exec-1] ==>  Preparing: INSERT INTO tb_blog ( shop_id, user_id, title, images, content ) VALUES ( ?, ?, ?, ?, ? )
[http-nio-8081-exec-1] ==> Parameters: 1(Long), 1012(Long), 太好吃了吧!!!(String), /blogs/7/3/a0518076-2e4f-45bf-8f77-0731fdfede00.png(String), 这家店太好吃了,强烈推荐大家!!(String)
[http-nio-8081-exec-1] <==    Updates: 1

4.8.2 查看探店笔记

在BlogController类中创建一个query()方法,用于查询探店笔记。其接口文档及代码如下:

项目说明
请求方法GET
请求路径/blog/query/{id}
请求参数id<Long>,笔记的ID
返回值blog<Blog>,笔记信息
// com.star.redis.dzdp.controller.BlogController

/**
 * 根据ID查询笔记
 * @author hsgx
 * @since 2024/4/8 9:17
 * @param id 
 * @return com.star.redis.dzdp.pojo.BaseResult<com.star.redis.dzdp.pojo.Blog>
 */
@GetMapping("/query/{id}")
public BaseResult<Blog> query(@PathVariable Long id) {
    log.info("query Blog, id = {}", id);
    Blog blog = blogService.getById(id);
    if(blog == null) {
        return BaseResult.setFail("笔记不存在!");
    }
    return BaseResult.setOkWithData(blog);
}

测试:

4.8.3 实现点赞功能

4.8.3.1 代码实现点赞功能

在BlogController类中创建一个likes()方法,用于实现点赞功能。其接口文档及代码如下:

项目说明
请求方法POST
请求路径/blog/likes/{id}
请求参数id<Long>,笔记的ID
返回值
// com.star.redis.dzdp.controller.BlogController

/**
 * 点赞功能
 * @author hsgx
 * @since 2024/4/8 9:27
 * @param id 
 * @return com.star.redis.dzdp.pojo.BaseResult<com.star.redis.dzdp.pojo.Blog>
 */
@GetMapping("/likes/{id}")
public BaseResult likes(@PathVariable Long id) {
    log.info("like Blog, id = {}", id);
    blogService.update().setSql("liked = liked + 1").eq("id", id).update();
    return BaseResult.setOk();
}

测试:

[http-nio-8081-exec-1] like Blog, id = 4
[http-nio-8081-exec-1] ==>  Preparing: UPDATE tb_blog SET liked = liked + 1 WHERE (id = ?)
[http-nio-8081-exec-1] ==> Parameters: 4(Long)
[http-nio-8081-exec-1] <==    Updates: 1
4.8.3.2 无限点赞问题解决

以上代码的问题在于,一个用户可以无限点赞,因为现在的逻辑仅仅是给点赞数+1。而真实的需求应该是:同一个用户只能点赞一次,再次点击则取消点赞。

我们可以采用Redis的Set集合来判断是否点赞过,因为Set集合的数据是不能重复的。其Key值为blog:liked:{id},Value值为用户ID,则集合中保存了点赞过某一id的笔记的所有用户ID。

在IBlogService接口中定义一个likeBlog()方法,作用是根据ID给笔记点赞,并在BlogServiceImpl实现类中具体实现:

// com.star.redis.dzdp.service.impl.BlogServiceImpl

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public BaseResult likeBlog(Long id, Long userId) {
    log.info("likeBlog => id = {}, userId = {}", id, userId);
    // 1.判断当前用户是否已经点赞过该笔记
    String key = "blog:liked:" + id;
    Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
    log.info("isMember in Set, key = {}, value = {}, result = {}",
            key, userId, isMember);
    if(BooleanUtil.isFalse(isMember)) {
        // 2.没有点赞过,则现在可以点赞
        // 2.1 点赞数+1
        boolean update = update().setSql("liked = liked + 1").eq("id", id).update();
        // 2.2 保存用户ID到Set集合
        if(update) {
            stringRedisTemplate.opsForSet().add(key, userId.toString());
            log.info("add to Set, key = {}, value = {}", key, userId);
            return BaseResult.setOk("点赞成功!");
        }
    } else {
        // 3.已经点赞过了,则取消点赞
        // 3.1 点赞数-1
        boolean update = update().setSql("liked = liked - 1").eq("id", id).update();
        // 2.2 从Set集合移除用户ID
        if(update) {
            stringRedisTemplate.opsForSet().remove(key, userId.toString());
            log.info("remove from Set, key = {}, value = {}", key, userId);
            return BaseResult.setOk("取消点赞成功!");
        }
    }
    return BaseResult.setFail("操作失败!");
}

修改BlogController类中创建一个likes()方法,直接调用IBlogService接口的likeBlog()方法:

// com.star.redis.dzdp.controller.BlogController

/**
 * 点赞功能
 * @author hsgx
 * @since 2024/4/8 9:27
 * @param id
 * @return com.star.redis.dzdp.pojo.BaseResult<com.star.redis.dzdp.pojo.Blog>
 */
@PostMapping("/likes/{id}")
public BaseResult likes(@PathVariable Long id, HttpServletRequest request) {
    log.info("like Blog, id = {}", id);
    // blogService.update().setSql("liked = liked + 1").eq("id", id).update();
    // return BaseResult.setOk();
    
    // userId的传递详见com.star.redis.dzdp.interceptor.LoginInterceptor
    return blogService.likeBlog(id, (Long) request.getAttribute("userId"));
}

测试:

第一次发送请求,点赞成功:

[http-nio-8081-exec-2] like Blog, id = 4
[http-nio-8081-exec-2] likeBlog => id = 4, userId = 1012
[http-nio-8081-exec-2] isMember in Set, key = blog:liked:4, value = 1012, result = false
[http-nio-8081-exec-2] ==>  Preparing: UPDATE tb_blog SET liked = liked + 1 WHERE (id = ?)
[http-nio-8081-exec-2] ==> Parameters: 4(Long)
[http-nio-8081-exec-2] <==    Updates: 1
[http-nio-8081-exec-2] add to Set, key = blog:liked:4, value = 1012

此时Redis中保存了Set集合:

再次发送请求,取消点赞成功:

[http-nio-8081-exec-3] like Blog, id = 4
[http-nio-8081-exec-3] likeBlog => id = 4, userId = 1012
[http-nio-8081-exec-3] isMember in Set, key = blog:liked:4, value = 1012, result = true
[http-nio-8081-exec-3] ==>  Preparing: UPDATE tb_blog SET liked = liked - 1 WHERE (id = ?)
[http-nio-8081-exec-3] ==> Parameters: 4(Long)
[http-nio-8081-exec-3] <==    Updates: 1
[http-nio-8081-exec-3] remove from Set, key = blog:liked:4, value = 1012

4.8.4 点赞排行榜

在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜。

4.8.4.1 改用可排序的SortedSet集合

前面的已点赞信息是放到Set集合,但是Set集合是不能排序的,所以可改用支持排序的SortedSet集合

修改BlogServiceImpl实现类的likeBlog()方法,如下:

// com.star.redis.dzdp.service.impl.BlogServiceImpl

@Override
public BaseResult likeBlog(Long id, Long userId) {
    log.info("likeBlog => id = {}, userId = {}", id, userId);
    // 优化:改用可排序的SortedSet集合
    // 1.判断当前用户是否已经点赞过该笔记
    String key = "blog:liked:" + id;
    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
    if(score == null) {
        // 2.没有点赞过,则现在可以点赞
        // 2.1 点赞数+1
        boolean update = update().setSql("liked = liked + 1").eq("id", id).update();
        // 2.2 保存用户ID到Set集合
        if(update) {
            // 参数1:key
            // 参数2:value
            // 参数3:score
            long addScore = System.currentTimeMillis();
            stringRedisTemplate.opsForZSet().add(key, userId.toString(), addScore);
            log.info("add to ZSet, key = {}, value = {}, score = {}", key, userId, addScore);
            return BaseResult.setOk("点赞成功!");
        }
    } else {
        // 3.已经点赞过了,则取消点赞
        // 3.1 点赞数-1
        boolean update = update().setSql("liked = liked - 1").eq("id", id).update();
        // 2.2 从Set集合移除用户ID
        if(update) {
            stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            log.info("remove from ZSet, key = {}, value = {}", key, userId);
            return BaseResult.setOk("取消点赞成功!");
        }
    }
    return BaseResult.setFail("操作失败!");
}

测试:

第一次发送请求,点赞成功:

[http-nio-8081-exec-2] like Blog, id = 4
[http-nio-8081-exec-2] likeBlog => id = 4, userId = 1012
[http-nio-8081-exec-2] query score from ZSet, key = blog:liked:4, value = 1012, result = null
[http-nio-8081-exec-2] ==>  Preparing: UPDATE tb_blog SET liked = liked + 1 WHERE (id = ?)
[http-nio-8081-exec-2] ==> Parameters: 4(Long)
[http-nio-8081-exec-2] <==    Updates: 1
[http-nio-8081-exec-2] add to ZSet, key = blog:liked:4, value = 1012, score = 1712544279745

此时Redis中保存了SortedSet集合:

再次发送请求,取消点赞成功:

[http-nio-8081-exec-4] like Blog, id = 4
[http-nio-8081-exec-4] likeBlog => id = 4, userId = 1012
[http-nio-8081-exec-4] query score from ZSet, key = blog:liked:4, value = 1012, result = 1.712544869232E12
[http-nio-8081-exec-4] ==>  Preparing: UPDATE tb_blog SET liked = liked - 1 WHERE (id = ?)
[http-nio-8081-exec-4] ==> Parameters: 4(Long)
[http-nio-8081-exec-4] <==    Updates: 1
[http-nio-8081-exec-4] remove from ZSet, key = blog:liked:4, value = 1012
4.8.4.2 实现点赞列表查询

首先定义一个实体类BlogLike,表示点赞用户的信息:

// com.star.redis.dzdp.pojo.BlogLike

@Data
@EqualsAndHashCode(callSuper = false)
public class BlogLike implements Serializable {
    /**
     * 笔记ID
     */
    private Long blogId;
    /**
     * 用户ID
     */
    private Long userId;
    /**
     * 用户图标
     */
    private String icon;
    /**
     * 用户姓名
     */
    private String name;
}

然后在BlogController类中编写一个listLiked()方法,用于查询点赞排行榜。其接口文档与代码如下:

项目说明
请求方法GET
请求路径/blog/query/liked/{id}
请求参数id<Long>,笔记的ID
返回值List<BlogLike>,点赞用户信息列表
// com.star.redis.dzdp.controller.BlogController

/**
 * 查询笔记的点赞排行榜
 * @author hsgx
 * @since 2024/4/8 11:12
 * @param id
 * @return com.star.redis.dzdp.pojo.BaseResult<java.util.List<com.star.redis.dzdp.pojo.BlogLike>>
 */
@GetMapping("/query/liked/{id}")
public BaseResult<List<BlogLike>> listLiked(@PathVariable Long id) {
    return blogService.queryBlogLikes(id);
}

在IBlogService接口中定义queryBlogLikes()方法,并在BlogServiceImpl实现类中具体实现:

// com.star.redis.dzdp.service.impl.BlogServiceImpl

@Override
public BaseResult<List<BlogLike>> queryBlogLikes(Long id) {
    log.info("queryBlogLikes, id = {}", id);
    // 1.查询top5的点赞用户 zrange key 0 4
    String key = "blog:liked:" + id;
    Set<String> range = stringRedisTemplate.opsForZSet().range(key, 0, 4);
    log.info("range 0,4 from ZSet, key = {}, result.size = {}",
            key, (range == null || range.isEmpty()) ? 0 : range.size());
    if(range == null || range.isEmpty()) {
        return BaseResult.setOkWithData(Collections.emptyList());
    }
    // 2.解析出其中的用户ID
    List<Long> ids = range.stream().map(Long::valueOf).collect(Collectors.toList());
    String idStr = StrUtil.join(",", ids);
    log.info("idStr = {}", idStr);
    // 3.根据用户ID查询用户信息 WHERE id IN (id1, id2) ORDER BY FIELD(id, id1, id2)
    List<BlogLike> blogLikeList = userService.query().in("id", ids)
            .last("ORDER BY FIELD(id," + idStr + ")").list()
            .stream().map(user -> {
                BlogLike blogLike = new BlogLike();
                blogLike.setBlogId(id);
                blogLike.setUserId(user.getId());
                blogLike.setIcon(user.getIcon());
                blogLike.setName(user.getNickName());
                return blogLike;
            })
            .collect(Collectors.toList());
    log.info("blogLikeList.size = {}", blogLikeList.size());
    return BaseResult.setOkWithData(blogLikeList);
}

测试:

[http-nio-8081-exec-1] queryBlogLikes, id = 4
[http-nio-8081-exec-1] range 0,4 from ZSet, key = blog:liked:4, result.size = 3
[http-nio-8081-exec-1] idStr = 11,12,16
[http-nio-8081-exec-1] ==>  Preparing: SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE (id IN (?,?,?)) ORDER BY FIELD(id,11,12,16)
[http-nio-8081-exec-1] ==> Parameters: 11(Long), 12(Long), 16(Long)
[http-nio-8081-exec-1] <==      Total: 3
[http-nio-8081-exec-1] blogLikeList.size = 3

本节完,更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • 36
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值