经过我们分析,圈子中的互动数据有一下特点:
综上,我们采用MongoDB来存储圈子中的互动数据
我们采用一张表来记录所有的互动信息,通过指定不同互动类型的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; //当前评论的点赞数
}
我们在刷动态的时候,每一次刷新除了需要查询动态信息外,还需要查询当前用户是否对这条动态点过赞,因此查询的内容过多导致效率低。因此,我们引入Redis,在里面保存用户点赞的信息,来判断用户是否点过赞或者喜欢过。
Redis中Key说明:
Redis中采用Hash类型存储,一条动态对应一个key,value里面又包含hashKey和value,其中一个用户点赞对应hashKey,value为标志位,1代表用户点赞。
MOVEMENTS_INTERACT_KEY + movementId;
MOVEMENT_LIKE_HASHKEY + userId;
MOVEMENT_LOVE_HASHKEY + userId;
这里判断用户是否已经点过赞了,可以从MongoDB中获取,也可以查询Redis。本项目中从MongoDB中获取。
用户点赞的业务流程如下:
用户取消点赞:和用户点赞正好相反
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);
}
这里的逻辑和点赞是一致的,就不在赘述。
和点赞的逻辑基本一致。用户发送评论,后端将评论保存到comment表中,并更新评论对应的动态的相关计数。接口如下:
代码实现
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层之前已经展示
需求:当用户点击某一个动态时,会显示动态详情和对应的评论列表。接口如下:
代码实现
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中查询登录用户是否对这条评论点过赞,如果点过赞,则需要设置相关属性。因此我们修改查询动态列表中的相关代码。
由于目前无论是点赞,还是喜欢,还是评论,实际上操作差不多,代码存在很多的冗余,因此考虑将代码封装起来复用。
首先在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);
}
}