功能04-达人探店5.功能04-达人探店5.1发布&查看探店笔记5.1.1发布探店笔记

探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:

  1. tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
  2. tb_blog_comments:其他用户对探店笔记的评价
/*表: tb_blog*/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) NOT NULL COMMENT '探店的照片,最多9张,多张以","隔开',  `content` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '探店的文字描述',  `liked` int(8) unsigned DEFAULT '0' COMMENT '点赞数量',  `comments` int(8) unsigned 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=8 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT/*表: tb_blog_comments*/CREATE TABLE `tb_blog_comments` (  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',  `blog_id` bigint(20) unsigned NOT NULL COMMENT '探店id',  `parent_id` bigint(20) unsigned NOT NULL COMMENT '关联的1级评论id,如果是一级评论,则值为0',  `answer_id` bigint(20) unsigned NOT NULL COMMENT '回复的评论id',  `content` varchar(255) NOT NULL COMMENT '回复的内容',  `liked` int(8) unsigned DEFAULT NULL COMMENT '点赞数',  `status` tinyint(1) unsigned DEFAULT NULL COMMENT '状态,0:正常,1:被举报,2:禁止查看',  `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 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT

点击首页最下方菜单栏中的“+”按钮,即可发布探店图文:

需要注意的是:

发布照片和发布笔记这两个功能是分离的。因为上传照片的功能不仅仅是发布笔记时需要用到,其他业务也有需求,因此上传照片是一个独立功能。

我们在发布笔记时,点击上传照片,会先向服务端发出一个请求,实现图片上传。上传成功以后,服务端会返回图片的地址(即上传之后可访问的该图片地址),这个地址将来会作为表单的参数,在发布笔记的时候一起提交到后台(也就是说,在提交笔记的时候,我们提交的就不是照片本身了,而是上传成功后的图片地址)。

(1)上传图片功能

上传图片的功能已经提前实现了,详见UploadController.java及其接口

上传的图片其实是放在放在前端服务器中的,这里为了模拟,放在了D盘的前端项目(nginx-1.18.0)的目录下:

同时,需要将代码中保存的目录修改为对应的目录:

(2)发布笔记功能

发布笔记的功能也提前实现了,详见BlogController.java及其接口

(3)测试

点击+号进入如下页面,点击上传照片,一次可以上传多张图片:

每次上传图片成功,后端都会返回该图片可访问的图片地址:

点击发布后,可以在个人主页中看到发布的文章:

5.1.2查看探店笔记

实现查看笔记的接口。需求:点击首页的笔记,可以进入详情页面,实现该页面的查询接口。

笔记的详情页面需要显示:

  1. 笔记信息
  2. 发布的用户信息(用户id、用户昵称、用户头像)

代码实现

(1)Blog.java,笔记实体类

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;/** * 笔记实体 * * @author 李 * @version 1.0 */@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;}

(2)BlogMapper.java

package com.hmdp.mapper;import com.hmdp.entity.Blog;import com.baomidou.mybatisplus.core.mapper.BaseMapper;/** *  Mapper 接口 * * @author 李 * @version 1.0 */public interface BlogMapper extends BaseMapper {}

(3)IBlogService.java

package com.hmdp.service;import com.hmdp.dto.Result;import com.hmdp.entity.Blog;import com.baomidou.mybatisplus.extension.service.IService;/** * 服务类 * * @author 李 * @version 1.0 */public interface IBlogService extends IService {        Result queryHotBlog(Integer current);//分页查询blog        Result queryBlogById(Long id);//根据id查询blog}

(4)BlogServiceImpl.java

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;/** * 服务实现类 * * @author 李 * @version 1.0 */@Servicepublic class BlogServiceImpl extends ServiceImpl implements IBlogService {    @Resource    private IUserService userService;        @Override    public Result queryHotBlog(Integer current) {        // 根据用户查询        Page page = query()                .orderByDesc("liked")                .page(new Page(current, SystemConstants.MAX_PAGE_SIZE));        // 获取当前页数据        List records = page.getRecords();        // 查询用户        records.forEach(this::queryBlogUser);        return Result.ok(records);    }    @Override    public Result queryBlogById(Long id) {        //1.查询blog        Blog blog = getById(id);        //2.查询blog有关的用户        if (blog == null) {            return Result.fail("笔记不存在!");        }        queryBlogUser(blog);        return Result.ok(blog);    }    public void queryBlogUser(Blog blog) {        Long userId = blog.getUserId();        User user = userService.getById(userId);        blog.setName(user.getNickName());        blog.setIcon(user.getIcon());    }}

(5)BlogController.java

package com.hmdp.controller;import com.hmdp.dto.Result;import com.hmdp.service.IBlogService;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;/** * 前端控制器 * * @author 李 * @version 1.0 */@RestController@RequestMapping("/blog")public class BlogController {    @Resource    private IBlogService blogService;        @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);    }}

(6)测试:重启项目,点击笔记,可以查看笔记详情

5.2点赞5.2.1需求分析

在首页的探店笔记排行榜和探店图文详情页面都有点赞的功能:

需求:

  • 同一个用户只能点赞一次,再次点击则取消点赞

  • 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)

实现步骤:

  1. 给Blog类中添加一个isLike字段,标识是否被当前用户点赞

  2. 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则再次点赞时点赞数-1

    blog的id作为key,点赞的用户id作为value

  3. 修改根据id查询Blog的业务,判断当前用户是否点赞过,赋值给isLike字段

    用于blog详情的点赞显示

  4. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

    用于一页blog时的所有点赞显示

5.2.2代码实现

(1)给Blog类中添加一个isLike字段,标识是否被当前用户点赞

(2)修改IBlogService,添加方法声明

Result likeBlog(Long id);

(3)修改BlogServiceImpl

  1. 实现方法likeBlog():修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则再次点赞时点赞数-1
  2. 修改BlogServiceImpl的queryBlogById()方法和queryHotBlog(),在查询blog信息的同时,查询当前用户有没有点赞过该blog

注意判断当前用户有没有登录

package com.hmdp.service.impl;import .../** * 服务实现类 * * @author 李 * @version 1.0 */@Servicepublic class BlogServiceImpl extends ServiceImpl implements IBlogService {    @Resource    private IUserService userService;    @Resource    private StringRedisTemplate stringRedisTemplate;    @Override    public Result queryHotBlog(Integer current) {        // 根据用户查询        Page page = query()                .orderByDesc("liked")                .page(new Page(current, SystemConstants.MAX_PAGE_SIZE));        // 获取当前页数据        List records = page.getRecords();        // 查询用户        records.forEach(blog -> {            //查询发布blog的user            this.queryBlogUser(blog);            //查询当前用户有没有点赞过该blog            this.isBlogLiked(blog);        });        return Result.ok(records);    }    @Override    public Result queryBlogById(Long id) {        //1.查询blog        Blog blog = getById(id);        //2.查询blog有关的用户        if (blog == null) {            return Result.fail("笔记不存在!");        }        queryBlogUser(blog);        //3.查询blog是否被点赞了        isBlogLiked(blog);        return Result.ok(blog);    }    private void isBlogLiked(Blog blog) {        //1.获取当前登录用户         if (UserHolder.getUser() == null) {        return;//如果当前用户未登录        }        Long userId = UserHolder.getUser().getId();        //2.判断当前登录用户是否已经点赞了        // (去redis的set集合中判断 SISMEMBER key member)        String key = "blog:liked:" + 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();        if (userId == null) {            return Result.fail("用户未登录");        }        //2.判断当前登录用户是否已经点赞了        // (去redis的set集合中判断 SISMEMBER key member)        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());            }        } 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();    }    public void queryBlogUser(Blog blog) {        Long userId = blog.getUserId();        User user = userService.getById(userId);        blog.setName(user.getNickName());        blog.setIcon(user.getIcon());    }}

(4)修改BlogController,添加方法likeBlog()

@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {    return blogService.likeBlog(id);}

(5)测试:启动项目,已登录用户第一次点赞时,点赞数+1,图标高亮;第二次点赞时,点赞数-1,图标变回灰色。

redis中的数据:set结构,key为blogId,value为userId

如果用户未登录,点赞时则会自动跳转到登录页面:

5.3点赞排行榜5.3.1需求分析

在探店笔记的详情页面,应该把笔记点赞的用户信息显示出来,比如最早点赞的TOP5,形成点赞排行榜:

实现查询点赞排行榜的接口:

需求:按照点赞时间先后排序,返回Top5的用户。

之前我们使用的是redis中的Set结构,对于点赞功能来说,要求数据唯一且可方便查找。在此基础上,点赞排行榜功能还要求对数据进行排序,因此我们选用SortedSet结构实现,并对之前的点赞功能进行改造。

zset的整体结构:key value score(key存储blogId,value存储点赞的userId,score可以存储当前点赞的时间戳)

zset结构没有判断元素是否存在的命令,但可以查找指定元素的score,根据这个命令,判断指定的元素的score,如果score不存在,则该元素不存在。

  ZSCORE key member  summary: Get the score associated with the given member in a sorted set    since: 1.2.0

例如:

127.0.0.1:6379> ZADD z1 1 m1 2 m2 3 m3(integer) 3127.0.0.1:6379> ZSCORE z1 m4 #侧面判断m4不存在(nil)127.0.0.1:6379> ZSCORE z1 m1"1"

查询排行(比较score)则使用 zrange 命令:

127.0.0.1:6379> ZRANGE z1 0 4 #查询排行前5名1) "m1"2) "m2"3) "m3"

5.3.2代码实现

(1)IBlogService增加方法声明queryBlogLikes

public interface IBlogService extends IService {    ...            Result queryBlogLikes(Long id);}

(2)修改BlogServiceImpl:

  1. 修改之前的点赞功能,将其用到的set结构改为zset结构(修改isBlogLiked和likeBlog方法)
  2. 实现点赞排行功能–queryBlogLikes()
//判断当前用户是否点赞过该blogprivate void isBlogLiked(Blog blog) {    //1.获取当前登录用户    if (UserHolder.getUser() == null) {        return;//如果当前用户未登录    }    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);}//进行点赞操作@Overridepublic Result likeBlog(Long id) {    //1.获取当前登录用户    Long userId = UserHolder.getUser().getId();    if (userId == null) {        return Result.fail("用户未登录");    }    //2.判断当前登录用户是否已经点赞了    String key = BLOG_LIKED_KEY + id;    Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());    //3.如果未点赞(score为null,证明该用户不存在zset中,即未点赞)    if (score == null) {        //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();}//根据blogId返回点赞该blog的top5的用户信息@Overridepublic Result queryBlogLikes(Long id) {    String key = BLOG_LIKED_KEY + id;    //1.查询top5的点赞用户 zrange key 0 4    Set top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);    if (top5 == null || top5.isEmpty()) {        return Result.ok(Collections.emptyList());    }    //2.解析出其中的用户id    List ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());    //3.根据用户id查询用户    List userDTOS = userService.listByIds(ids)            .stream()            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))            .collect(Collectors.toList());    //4.返回    return Result.ok(userDTOS);}

(3)修改 BlogController,增加方法

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

(4)测试:使用不同的用户账号给同一篇探店blog点赞,成功显示点赞的用户信息:

但是显示的顺序出现了问题:如下所示,分别有三个用户。正确的点赞顺序如redis缓存所示:1033,1,2

但是从数据库中查找返回的用户顺序却是:1,2 ,1033,前端显示的用户头像id也是按照1,2,1033的顺序,这显然不符合我们要的顺序。

我们返回看之前的代码:

代码底层发出的sql语句如下:可以发现传入的参数顺序是正确的(1033,1,2),但是返回的数据顺序并不和我们的入参一致:

怎么保证使用IN子句的时候,返回的数据结果和参数的顺序一致呢?

解决方法:使用 order by 指定排序

(5)修改queryBlogLikes()方法,自定义查询语句:

(6)重新启动项目:可以看到之前的顺序已经变为真正的顺序了