【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)

1. 圈子中的互动功能的分析与设计

1.1 数据库的原则

经过我们分析,圈子中的互动数据有一下特点:

  • 数据量非常大
  • 数据变化迅速
  • 数据价值相对较低

综上,我们采用MongoDB来存储圈子中的互动数据

1.2 表设计

我们采用一张表来记录所有的互动信息,通过指定不同互动类型的type来区分是点赞还是评论或者时喜欢。表结构如下:
在这里插入图片描述
对应的实体类如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "comment")
public class Comment implements java.io.Serializable{
    
    private ObjectId id;
    private ObjectId publishId;    //发布id
    private Integer commentType;   //评论类型,1-点赞,2-评论,3-喜欢
    private String content;        //评论内容  
    private Long userId;           //评论人   
    private Long publishUserId;    //被评论人ID
    private Long created; 		   //发表时间
    private Integer likeCount = 0; //当前评论的点赞数
    
}

1.3 Redis缓存

我们在刷动态的时候,每一次刷新除了需要查询动态信息外,还需要查询当前用户是否对这条动态点过赞,因此查询的内容过多导致效率低。因此,我们引入Redis,在里面保存用户点赞的信息,来判断用户是否点过赞或者喜欢过。

Redis中Key说明:
Redis中采用Hash类型存储,一条动态对应一个key,value里面又包含hashKey和value,其中一个用户点赞对应hashKey,value为标志位,1代表用户点赞。

  • key:MOVEMENTS_INTERACT_KEY + movementId;
  • hashKey:MOVEMENT_LIKE_HASHKEY + userId;
  • value:MOVEMENT_LOVE_HASHKEY + userId;

这里判断用户是否已经点过赞了,可以从MongoDB中获取,也可以查询Redis。本项目中从MongoDB中获取。

2. 用户点赞和取消点赞

2.1 需求分析

用户点赞的业务流程如下:

  • 根据前端传来的动态ID,先去MongoDB中的评论表中查询是否有记录。如果有了在抛出异常
  • 封装comment对象,调用api保存
  • Comment保存成功后,还需要同步更新动态表格中的点赞数量+1
  • 在Redis中写入数据,保存用户点赞信息。

【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)_第1张图片

用户取消点赞:和用户点赞正好相反

  • 根据前端传来的动态ID,先去MongoDB中的评论表中查询是否有记录。如果没有则抛出异常
  • 封装comment对象,调用api保存
  • Comment保存成功后,还需要同步更新动态表格中的点赞数量-1
  • 在Redis中删除用户点赞的信息

【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)_第2张图片
接口如下:
【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)_第3张图片
【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)_第4张图片

2.2 代码实现

Controller

/**
 * 用户点赞
 * @param movementId 动态Id
 * @return 点赞之后最新的点赞数量
 */
@GetMapping("/{id}/likeMovement")
public ResponseEntity like(@PathVariable(name = "id") String movementId) {
    // 1. 调用Service方法完成点赞
    Integer likeCount = this.commentService.like(movementId);
    // 2. 返回结果
    return ResponseEntity.ok(likeCount);
}

/**
 * 取消点赞
 * @param movementId 动态Id
 * @return 取消点赞之后最新的点赞数量
 */
@GetMapping("/dislikeMovement/{id}")
public ResponseEntity dislike(@PathVariable(name = "id") String movementId) {
    // 1. 调用Service方法完成点赞
    Integer likeCount = this.commentService.dislike(movementId);
    // 2. 返回结果
    return ResponseEntity.ok(likeCount);
}

Service

public Integer like(String movementId) {
    // 1. 获取当前用户ID
    Long userId = UserHolder.getUserId();
    // 2. 查询comment表,判断用户是否已经点过赞,如果点过,则抛出异常
    Boolean isLiked = this.commentApi.check(movementId, CommentType.LIKE.getType(), userId);
    if (isLiked) {
        // 用户已经点过赞了,这里就直接抛出异常
        throw new BusinessException(ErrorResult.likeError());
    }
    // 3. 封装Comment对象,调用api保存comment
    Comment comment = new Comment();
    comment.setPublishId(new ObjectId(movementId));
    comment.setUserId(userId);
    comment.setCreated(System.currentTimeMillis());
    Integer count = this.commentApi.save(comment, CommentType.LIKE.getType());
    // 4. 将用户点赞保存到Redis
    // 构造key prefix + movementId
    String key = MOVEMENTS_INTERACT_KEY + movementId;
    // 构造哈希key prefix + userId
    String hashKey = MOVEMENT_LIKE_HASHKEY + userId;
    this.redisTemplate.opsForHash().put(key, hashKey, "1");
    // 5. 返回结果
    return count;
}

// 取消点赞
public Integer dislike(String movementId) {
    // 1. 获取当前用户id
    Long userId = UserHolder.getUserId();
    // 2. 查询用户是否点过赞,如果没有点过赞,则抛出异常
    Boolean isLiked = this.commentApi.check(movementId, CommentType.LIKE.getType(), userId);
    if (!isLiked) {
        // 用户已经点过赞了,这里就直接抛出异常
        throw new BusinessException(ErrorResult.disLikeError());
    }
    // 3. 调用api取消点赞
    Comment comment = new Comment();
    comment.setPublishId(new ObjectId(movementId));
    comment.setUserId(userId);
    Integer count = this.commentApi.delete(comment, CommentType.LIKE.getType());
    // 4. 删除redis中的键
    // 构造key prefix + movementId
    String key = MOVEMENTS_INTERACT_KEY + movementId;
    // 构造哈希key prefix + userId
    String hashKey = MOVEMENT_LIKE_HASHKEY + userId;
    this.redisTemplate.opsForHash().delete(key, hashKey);
    // 5. 返回结果
    return count;
}

API

@Override
public Boolean check(String movementId, int type, Long userId) {
    // 构建条件 动态id,用户id,点赞类型
    Criteria criteria = Criteria.where("publishId").is(new ObjectId(movementId))
            .and("userId").is(userId)
            .and("commentType").is(type);
    Query query = new Query(criteria);
    return this.mongoTemplate.exists(query, Comment.class);
}
/**
* 保存Comment到数据库
*
* @param comment 评论对象
* @param type    评论类型
* @return
*/
@Override
public Integer save(Comment comment, int type) {
   // 1. 从Comment对象中获取到动态id,查询动态id,
   try {
       ObjectId publishId = comment.getPublishId();
       Movement movementById = this.mongoTemplate.findById(publishId, Movement.class);
       if (movementById != null || type == CommentType.LIKECOMMENT.getType()) {
           if (movementById == null) {
               // 1. 获取到动态id的作者 并设置到Comment的publishUserId字段中
               Comment publishUser = this.mongoTemplate.findById(comment.getPublishId(), Comment.class);
               if (publishUser != null) {
                   comment.setPublishUserId(publishUser.getPublishUserId());
               } else {
                   return 0;
               }
           } else {
               comment.setPublishUserId(movementById.getUserId());
           }
           // 2. 设置评论的类型
           comment.setCommentType(type);
           // 3. 保存到数据库
           this.mongoTemplate.save(comment);
           // 4. 根据不同的评论类型,更新Movement或者Comment表中数据表中对象的记录
           // 4.1 构造查询条件 如果是动态的点赞或者喜欢
           if (type == CommentType.LIKECOMMENT.getType()) {
               // 评论点赞
               Query query = new Query(Criteria.where("id").is(comment.getPublishId()));
               Update update = new Update();
               update.inc("likeCount", 1);
               FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
               return this.mongoTemplate.findAndModify(query, update, findAndModifyOptions, Comment.class).getLikeCount();
           } else {
               Criteria criteria = Criteria.where("id").is(movementById.getId());
               Query query = new Query(criteria);
               Update update = new Update();
               Integer commentType = comment.getCommentType();
               if (commentType == CommentType.LIKE.getType()) {
                   update.inc("likeCount", 1);
               } else if (commentType == CommentType.COMMENT.getType()) {
                   update.inc("commentCount", 1);
               } else {
                   update.inc("loveCount", 1);
               }
               FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
               // 调用template更新 返回更新后的Movement对象
               Movement modify = this.mongoTemplate.findAndModify(query, update, findAndModifyOptions, Movement.class);

               // 根据不同的评论类型,返回对应的计数
               return modify.getCount(commentType);
           }
       } else {
           return 0;
       }

   } catch (Exception e) {
       throw new RuntimeException(e);
   }
}
/**
* 取消点赞 喜欢 评论点赞
*
* @param comment 要删除的评论,喜欢 评论点赞
* @return 取消点赞之后最新的点赞数量
*/
@Override
public Integer delete(Comment comment, int type) {
   // 1. 解析数据
   ObjectId publishId = comment.getPublishId();
   Long userId = comment.getUserId();
   // 2. 构造条件
   Criteria criteria = Criteria.where("publishId").is(publishId)
           .and("userId").is(userId)
           .and("commentType").is(type);
   Query query = new Query(criteria);
   // 3. 删除comment中数据
   this.mongoTemplate.remove(query, Comment.class);
   if (type == CommentType.LIKECOMMENT.getType()) {
       Query modifyQuery = new Query(Criteria.where("id").is(publishId));
       Update update = new Update();
       update.inc("likeCount", -1);
       FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
       return this.mongoTemplate.findAndModify(modifyQuery, update, findAndModifyOptions, Comment.class).getLikeCount();
   } else {
       // 4. 更新相应的movement中数据
       Query modifyQuery = new Query(Criteria.where("id").is(publishId));
       Update update = new Update();
       Integer commentType = type;
       if (commentType == CommentType.LIKE.getType()) {
           update.inc("likeCount", -1);
       } else if (commentType == CommentType.COMMENT.getType()) {
           update.inc("commentCount", -1);
       } else {
           update.inc("loveCount", -1);
       }

       // 调用template更新 返回更新后的Movement对象
       FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
       Movement modify = this.mongoTemplate.findAndModify(modifyQuery, update, findAndModifyOptions, Movement.class);
       // 5.返回结果
       return modify.getCount(type);
   }
}

需要注意的是,当我们查询动态的时候,还需要从Redis中查询登录用户是否对这条动态点过赞,如果点过赞,则需要设置相关属性。因此我们修改查询动态列表中的相关代码

if (userInfo != null) {
    MovementsVo init = MovementsVo.init(userInfo, movement);
    // 使用EmptyList 包报错 https://blog.csdn.net/fengbin111/article/details/105909654/
    // 从Redis中获取数据,判断用户书否喜欢过或者点赞过这条动态
    String key = MOVEMENTS_INTERACT_KEY + movement.getId().toHexString();
    String loveHashKey = MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId();
    String likeHashKey = MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId();
    if (this.redisTemplate.opsForHash().hasKey(key, loveHashKey)) {
        init.setHasLoved(1);
    }
    if (this.redisTemplate.opsForHash().hasKey(key, likeHashKey)) {
        init.setHasLiked(1);
    }
    voList.add(init);
}

3. 用户喜欢和取消喜欢

这里的逻辑和点赞是一致的,就不在赘述。

4. 用户评论

4.1 发布评论

和点赞的逻辑基本一致。用户发送评论,后端将评论保存到comment表中,并更新评论对应的动态的相关计数。接口如下:
【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)_第5张图片
代码实现
Controller

/**
 * 发布一条评论
 * @param map 动态ID+评论正文
 * @return
 */
@PostMapping
public ResponseEntity publishComment(@RequestBody Map map) {
    // 1. 解析到前端传递的评论的动态id和用户的评论正文
    String movementId = map.get("movementId").toString();
    String comment = map.get("comment").toString();
    // 2. 调用Service方法
    this.commentService.publishComment(movementId, comment);
    // 3. 构建返回值
    return ResponseEntity.ok(null);
}

Service

/**
 * 根据动态id和评论正文 新增一条评论
 *
 * @param movementId 动态id
 * @param comment    评论正文
 */
public Integer publishComment(String movementId, String comment) {
    // 1. 根据动态ID查询到动态的对象
    Comment newComment = new Comment();
    // 4. 继续封装其他的Comment属性
    newComment.setPublishId(new ObjectId(movementId));
    newComment.setContent(comment);
    newComment.setUserId(UserHolder.getUserId());
    newComment.setCreated(System.currentTimeMillis());
    // 5. 调用方法保存Comment, 并且返回保存后的评论数
    return this.commentApi.save(newComment, CommentType.COMMENT.getType());
}

API层之前已经展示

4.2 根据动态查询评论列表

需求:当用户点击某一个动态时,会显示动态详情和对应的评论列表。接口如下:
【探花交友DAY 06】圈子中的互动功能(点赞、评论、喜欢)_第6张图片

代码实现

Controller

/**
 * 根据动态ID查询到动态的所有的评论列表
 * @param page 页号
 * @param pagesize 页大小
 * @param movementId 动态ID
 * @return
 */
@GetMapping
public ResponseEntity getCommentListByMovementId(@RequestParam(defaultValue = "1") Integer page,
                                                 @RequestParam(defaultValue = "5") Integer pagesize,
                                                 String movementId) {
    // 1. 调用Service方法查询到CommentVO集合的PageResult
    PageResult result = this.commentService.getCommentListByMovementId(page,pagesize, movementId);
    // 2. 返回结果
    return ResponseEntity.ok(result);
}

Service

/**
* 根据动态ID查询到动态的所有的评论列表
*
* @param page       页号
* @param pagesize   页大小
* @param movementId 动态ID
* @return
*/
public PageResult getCommentListByMovementId(Integer page, Integer pagesize, String movementId) {
   // 1. 根据动态ID查询到所有的评论集合
   List<Comment> commentList = this.commentApi.getCommentListByMovementId(page, pagesize, movementId);
   if (CollUtil.isEmpty(commentList)) {
       return new PageResult();
   }
   // 2. 从评论集合中抽取出userId评论发布人的id
   List<Long> userIds = CollUtil.getFieldValues(commentList, "userId", Long.class);
   // 3. 根据发布人的Id去查询对应的用户详情
   Map<Long, UserInfo> userInfoByIds = this.userInfoApi.getUserInfoByIds(userIds, null);
   // 4. 封装Vo对象
   List<CommentVo> commentVos = new ArrayList<>();
   for (Comment comment : commentList) {
       Long userId = comment.getUserId();
       UserInfo userInfo = userInfoByIds.get(userId);
       if (userInfo != null) {
           CommentVo init = CommentVo.init(userInfo, comment);
           String key = MOVEMENTS_INTERACT_KEY + comment.getId();
           // 构造哈希key prefix + userId
           String likeHashKey = MOVEMENT_LIKE_HASHKEY + userId;
           if (this.redisTemplate.opsForHash().hasKey(key, likeHashKey)) {
               init.setHasLiked(1);
           }
           commentVos.add(init);
       }
   }
   // 5. 返回结果
   return new PageResult(page, pagesize, 0, commentVos);
}

注意:当我们查询评论的时候,还需要从Redis中查询登录用户是否对这条评论点过赞,如果点过赞,则需要设置相关属性。因此我们修改查询动态列表中的相关代码。

5. 代码的封装

由于目前无论是点赞,还是喜欢,还是评论,实际上操作差不多,代码存在很多的冗余,因此考虑将代码封装起来复用。

首先在CommentService中封装一个方法用来处理comment

/**
* 处理喜欢和点赞请求
*
* @param movementId 动态id
* @param type       类型 判断是喜欢还是点赞
* @param flag       标志位 true表示新增 false表示删除
* @return
*/
private Integer processLikeOrLove(String movementId, Integer type, Boolean flag) {
   // 1. 根据用户id,判断用户是否已经喜欢过或者点过赞
   Long userId = UserHolder.getUserId();
   Boolean check = this.commentApi.check(movementId, type, userId);
   if (!flag) {
       check = !check;
   }
   if (check) {
       switch (type) {
           case 1:
           case 4:
               throw new BusinessException(ErrorResult.likeError());
           case 3:
               throw new BusinessException(ErrorResult.loveError());
       }
   }
   // 2. 用户没有点过赞或者喜欢过  封装Comment对象,调用api保存comment
   Comment comment = new Comment();
   comment.setPublishId(new ObjectId(movementId));
   comment.setUserId(userId);
   comment.setCreated(System.currentTimeMillis());

   // 3. 设置Redis
   String key = MOVEMENTS_INTERACT_KEY + movementId;
   // 构造哈希key prefix + userId
   String likeHashKey = MOVEMENT_LIKE_HASHKEY + userId;
   String loveHashKey = MOVEMENT_LOVE_HASHKEY + userId;

   Integer count;
   if (flag) {
       // 插入数据
       count = this.commentApi.save(comment, type);
       if (type == CommentType.LOVE.getType()) {
           this.redisTemplate.opsForHash().put(key, loveHashKey, "1");
       } else if (type == CommentType.LIKE.getType() || type == CommentType.LIKECOMMENT.getType()) {
           this.redisTemplate.opsForHash().put(key, likeHashKey, "1");
       }
   } else {
       // 删除数据
       count = this.commentApi.delete(comment, type);
       if (type == CommentType.LOVE.getType()) {
           this.redisTemplate.opsForHash().delete(key, loveHashKey);
       } else if (type == CommentType.LIKE.getType() || type == CommentType.LIKECOMMENT.getType()) {
           this.redisTemplate.opsForHash().delete(key, likeHashKey);
       }
   }
   return count;
}

然后在API层封装保存comment和删除commet的通用方法

/**
* 保存Comment到数据库
*
* @param comment 评论对象
* @param type    评论类型
* @return
*/
@Override
public Integer save(Comment comment, int type) {
   // 1. 从Comment对象中获取到动态id,查询动态id,
   try {
       ObjectId publishId = comment.getPublishId();
       Movement movementById = this.mongoTemplate.findById(publishId, Movement.class);
       if (movementById != null || type == CommentType.LIKECOMMENT.getType()) {
           if (movementById == null) {
               // 1. 获取到动态id的作者 并设置到Comment的publishUserId字段中
               Comment publishUser = this.mongoTemplate.findById(comment.getPublishId(), Comment.class);
               if (publishUser != null) {
                   comment.setPublishUserId(publishUser.getPublishUserId());
               } else {
                   return 0;
               }
           } else {
               comment.setPublishUserId(movementById.getUserId());
           }
           // 2. 设置评论的类型
           comment.setCommentType(type);
           // 3. 保存到数据库
           this.mongoTemplate.save(comment);
           // 4. 根据不同的评论类型,更新Movement或者Comment表中数据表中对象的记录
           // 4.1 构造查询条件 如果是动态的点赞或者喜欢
           if (type == CommentType.LIKECOMMENT.getType()) {
               // 评论点赞
               Query query = new Query(Criteria.where("id").is(comment.getPublishId()));
               Update update = new Update();
               update.inc("likeCount", 1);
               FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
               return this.mongoTemplate.findAndModify(query, update, findAndModifyOptions, Comment.class).getLikeCount();
           } else {
               Criteria criteria = Criteria.where("id").is(movementById.getId());
               Query query = new Query(criteria);
               Update update = new Update();
               Integer commentType = comment.getCommentType();
               if (commentType == CommentType.LIKE.getType()) {
                   update.inc("likeCount", 1);
               } else if (commentType == CommentType.COMMENT.getType()) {
                   update.inc("commentCount", 1);
               } else {
                   update.inc("loveCount", 1);
               }
               FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
               // 调用template更新 返回更新后的Movement对象
               Movement modify = this.mongoTemplate.findAndModify(query, update, findAndModifyOptions, Movement.class);

               // 根据不同的评论类型,返回对应的计数
               return modify.getCount(commentType);
           }
       } else {
           return 0;
       }

   } catch (Exception e) {
       throw new RuntimeException(e);
   }
}
/**
* 取消点赞 喜欢 评论点赞
*
* @param comment 要删除的评论,喜欢 评论点赞
* @return 取消点赞之后最新的点赞数量
*/
@Override
public Integer delete(Comment comment, int type) {
   // 1. 解析数据
   ObjectId publishId = comment.getPublishId();
   Long userId = comment.getUserId();
   // 2. 构造条件
   Criteria criteria = Criteria.where("publishId").is(publishId)
           .and("userId").is(userId)
           .and("commentType").is(type);
   Query query = new Query(criteria);
   // 3. 删除comment中数据
   this.mongoTemplate.remove(query, Comment.class);
   if (type == CommentType.LIKECOMMENT.getType()) {
       Query modifyQuery = new Query(Criteria.where("id").is(publishId));
       Update update = new Update();
       update.inc("likeCount", -1);
       FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
       return this.mongoTemplate.findAndModify(modifyQuery, update, findAndModifyOptions, Comment.class).getLikeCount();
   } else {
       // 4. 更新相应的movement中数据
       Query modifyQuery = new Query(Criteria.where("id").is(publishId));
       Update update = new Update();
       Integer commentType = type;
       if (commentType == CommentType.LIKE.getType()) {
           update.inc("likeCount", -1);
       } else if (commentType == CommentType.COMMENT.getType()) {
           update.inc("commentCount", -1);
       } else {
           update.inc("loveCount", -1);
       }

       // 调用template更新 返回更新后的Movement对象
       FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options().returnNew(true);
       Movement modify = this.mongoTemplate.findAndModify(modifyQuery, update, findAndModifyOptions, Movement.class);
       // 5.返回结果
       return modify.getCount(type);
   }
}

你可能感兴趣的:(探花交友项目,交友,mongodb,数据库)