Compare commits
3 Commits
bd775b805c
...
8be6719be8
| Author | SHA1 | Date | |
|---|---|---|---|
| 8be6719be8 | |||
| e3f9b6a5b5 | |||
| 6f22c2b50d |
@@ -7,6 +7,24 @@ public class RedisKeyConstants {
|
|||||||
*/
|
*/
|
||||||
private static final String HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX = "comment:havaFirstReplyCommentId:";
|
private static final String HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX = "comment:havaFirstReplyCommentId:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key 前缀:二级评论分页 ZSET
|
||||||
|
*/
|
||||||
|
private static final String CHILD_COMMENT_LIST_KEY_PREFIX = "comment:childList:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Field: 子评论总数
|
||||||
|
*/
|
||||||
|
public static final String FIELD_CHILD_COMMENT_TOTAL = "childCommentTotal";
|
||||||
|
/**
|
||||||
|
* Hash Field: 点赞总数
|
||||||
|
*/
|
||||||
|
public static final String FIELD_LIKE_TOTAL = "likeTotal";
|
||||||
|
/**
|
||||||
|
* 评论维度计数 Key 前缀
|
||||||
|
*/
|
||||||
|
private static final String COUNT_COMMENT_KEY_PREFIX = "count:comment:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash Field 键:评论总数
|
* Hash Field 键:评论总数
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +45,25 @@ public class RedisKeyConstants {
|
|||||||
*/
|
*/
|
||||||
private static final String COMMENT_DETAIL_KEY_PREFIX = "comment:detail:";
|
private static final String COMMENT_DETAIL_KEY_PREFIX = "comment:detail:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建子评论分页 ZSET 完整 KEY
|
||||||
|
*
|
||||||
|
* @param commentId 一级评论 ID
|
||||||
|
* @return 子评论分页 ZSET 完整 KEY
|
||||||
|
*/
|
||||||
|
public static String buildChildCommentListKey(Long commentId) {
|
||||||
|
return CHILD_COMMENT_LIST_KEY_PREFIX + commentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建评论维度计数 Key
|
||||||
|
*
|
||||||
|
* @param commentId 评论 ID
|
||||||
|
* @return 评论维度计数 Key
|
||||||
|
*/
|
||||||
|
public static String buildCountCommentKey(Long commentId) {
|
||||||
|
return COUNT_COMMENT_KEY_PREFIX + commentId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建完整 KEY
|
* 构建完整 KEY
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package com.hanserwei.hannote.comment.biz.controller;
|
|||||||
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
|
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||||
import com.hanserwei.framework.common.response.PageResponse;
|
import com.hanserwei.framework.common.response.PageResponse;
|
||||||
import com.hanserwei.framework.common.response.Response;
|
import com.hanserwei.framework.common.response.Response;
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentItemRspVO;
|
import com.hanserwei.hannote.comment.biz.model.vo.*;
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentPageListReqVO;
|
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.PublishCommentReqVO;
|
|
||||||
import com.hanserwei.hannote.comment.biz.service.CommentService;
|
import com.hanserwei.hannote.comment.biz.service.CommentService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -35,4 +33,10 @@ public class CommentController {
|
|||||||
return commentService.findCommentPageList(findCommentPageListReqVO);
|
return commentService.findCommentPageList(findCommentPageListReqVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/child/list")
|
||||||
|
@ApiOperationLog(description = "二级评论分页查询")
|
||||||
|
public PageResponse<FindChildCommentItemRspVO> findChildCommentPageList(@Validated @RequestBody FindChildCommentPageListReqVO findChildCommentPageListReqVO) {
|
||||||
|
return commentService.findChildCommentPageList(findChildCommentPageListReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -82,4 +82,42 @@ public interface CommentDOMapper extends BaseMapper<CommentDO> {
|
|||||||
* @return 热门评论
|
* @return 热门评论
|
||||||
*/
|
*/
|
||||||
List<CommentDO> selectHeatComments(Long noteId);
|
List<CommentDO> selectHeatComments(Long noteId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询一级评论下子评论总数
|
||||||
|
*
|
||||||
|
* @param commentId 一级评论 ID
|
||||||
|
* @return 一级评论下子评论总数
|
||||||
|
*/
|
||||||
|
Long selectChildCommentTotalById(Long commentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询二级评论分页数据
|
||||||
|
*
|
||||||
|
* @param parentId 一级评论 ID
|
||||||
|
* @param offset 偏移量
|
||||||
|
* @param pageSize 页大小
|
||||||
|
* @return 二级评论分页数据
|
||||||
|
*/
|
||||||
|
List<CommentDO> selectChildPageList(@Param("parentId") Long parentId,
|
||||||
|
@Param("offset") long offset,
|
||||||
|
@Param("pageSize") long pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询计数数据
|
||||||
|
*
|
||||||
|
* @param commentIds 评论 ID 列表
|
||||||
|
* @return 计数数据
|
||||||
|
*/
|
||||||
|
List<CommentDO> selectCommentCountByIds(@Param("commentIds") List<Long> commentIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询子评论
|
||||||
|
*
|
||||||
|
* @param parentId 一级评论 ID
|
||||||
|
* @param limit 子评论数量限制
|
||||||
|
* @return 子评论
|
||||||
|
*/
|
||||||
|
List<CommentDO> selectChildCommentsByParentIdAndLimit(@Param("parentId") Long parentId,
|
||||||
|
@Param("limit") int limit);
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
|||||||
|
|
||||||
// ----------- 业务异常状态码 -----------
|
// ----------- 业务异常状态码 -----------
|
||||||
COMMENT_NOT_FOUND("COMMENT-20001", "此评论不存在"),
|
COMMENT_NOT_FOUND("COMMENT-20001", "此评论不存在"),
|
||||||
|
PARENT_COMMENT_NOT_FOUND("COMMENT-20000", "此父评论不存在"),
|
||||||
;
|
;
|
||||||
|
|
||||||
// 异常码
|
// 异常码
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.hanserwei.hannote.comment.biz.model.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindChildCommentItemRspVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论 ID
|
||||||
|
*/
|
||||||
|
private Long commentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布者用户 ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论内容
|
||||||
|
*/
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布时间
|
||||||
|
*/
|
||||||
|
private String createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 被点赞数
|
||||||
|
*/
|
||||||
|
private Long likeTotal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复的用户昵称
|
||||||
|
*/
|
||||||
|
private String replyUserName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复的用户 ID
|
||||||
|
*/
|
||||||
|
private Long replyUserId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hanserwei.hannote.comment.biz.model.vo;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FindChildCommentPageListReqVO {
|
||||||
|
|
||||||
|
@NotNull(message = "父评论 ID 不能为空")
|
||||||
|
private Long parentCommentId;
|
||||||
|
|
||||||
|
@NotNull(message = "页码不能为空")
|
||||||
|
private Integer pageNo = 1;
|
||||||
|
}
|
||||||
@@ -4,9 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
import com.hanserwei.framework.common.response.PageResponse;
|
import com.hanserwei.framework.common.response.PageResponse;
|
||||||
import com.hanserwei.framework.common.response.Response;
|
import com.hanserwei.framework.common.response.Response;
|
||||||
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
|
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentItemRspVO;
|
import com.hanserwei.hannote.comment.biz.model.vo.*;
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentPageListReqVO;
|
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.PublishCommentReqVO;
|
|
||||||
|
|
||||||
public interface CommentService extends IService<CommentDO> {
|
public interface CommentService extends IService<CommentDO> {
|
||||||
/**
|
/**
|
||||||
@@ -24,4 +22,12 @@ public interface CommentService extends IService<CommentDO> {
|
|||||||
* @return 响应
|
* @return 响应
|
||||||
*/
|
*/
|
||||||
PageResponse<FindCommentItemRspVO> findCommentPageList(FindCommentPageListReqVO findCommentPageListReqVO);
|
PageResponse<FindCommentItemRspVO> findCommentPageList(FindCommentPageListReqVO findCommentPageListReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二级评论分页查询
|
||||||
|
*
|
||||||
|
* @param findChildCommentPageListReqVO 二级评论分页查询参数
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
PageResponse<FindChildCommentItemRspVO> findChildCommentPageList(FindChildCommentPageListReqVO findChildCommentPageListReqVO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.github.benmanes.caffeine.cache.Caffeine;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
||||||
import com.hanserwei.framework.common.constant.DateConstants;
|
import com.hanserwei.framework.common.constant.DateConstants;
|
||||||
import com.hanserwei.framework.common.exception.ApiException;
|
import com.hanserwei.framework.common.exception.ApiException;
|
||||||
@@ -20,11 +21,10 @@ import com.hanserwei.hannote.comment.biz.constants.RedisKeyConstants;
|
|||||||
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
|
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
|
||||||
import com.hanserwei.hannote.comment.biz.domain.mapper.CommentDOMapper;
|
import com.hanserwei.hannote.comment.biz.domain.mapper.CommentDOMapper;
|
||||||
import com.hanserwei.hannote.comment.biz.domain.mapper.NoteCountDOMapper;
|
import com.hanserwei.hannote.comment.biz.domain.mapper.NoteCountDOMapper;
|
||||||
|
import com.hanserwei.hannote.comment.biz.enums.CommentLevelEnum;
|
||||||
import com.hanserwei.hannote.comment.biz.enums.ResponseCodeEnum;
|
import com.hanserwei.hannote.comment.biz.enums.ResponseCodeEnum;
|
||||||
import com.hanserwei.hannote.comment.biz.model.dto.PublishCommentMqDTO;
|
import com.hanserwei.hannote.comment.biz.model.dto.PublishCommentMqDTO;
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentItemRspVO;
|
import com.hanserwei.hannote.comment.biz.model.vo.*;
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentPageListReqVO;
|
|
||||||
import com.hanserwei.hannote.comment.biz.model.vo.PublishCommentReqVO;
|
|
||||||
import com.hanserwei.hannote.comment.biz.retry.SendMqRetryHelper;
|
import com.hanserwei.hannote.comment.biz.retry.SendMqRetryHelper;
|
||||||
import com.hanserwei.hannote.comment.biz.rpc.DistributedIdGeneratorRpcService;
|
import com.hanserwei.hannote.comment.biz.rpc.DistributedIdGeneratorRpcService;
|
||||||
import com.hanserwei.hannote.comment.biz.rpc.KeyValueRpcService;
|
import com.hanserwei.hannote.comment.biz.rpc.KeyValueRpcService;
|
||||||
@@ -38,9 +38,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.logging.log4j.util.Strings;
|
import org.apache.logging.log4j.util.Strings;
|
||||||
import org.jspecify.annotations.NonNull;
|
import org.jspecify.annotations.NonNull;
|
||||||
import org.springframework.data.redis.core.RedisCallback;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.*;
|
||||||
import org.springframework.data.redis.core.ZSetOperations;
|
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
|
|
||||||
// 先查询本地缓存
|
// 先查询本地缓存
|
||||||
// 新建一个集合用于存储本地缓存中不存在的评论ID
|
// 新建一个集合用于存储本地缓存中不存在的评论ID
|
||||||
List<Long> localeCacheExpiredCommentIds = Lists.newArrayList();
|
List<Long> localCacheExpiredCommentIds = Lists.newArrayList();
|
||||||
|
|
||||||
// 构建本地缓存的key集合
|
// 构建本地缓存的key集合
|
||||||
List<Long> localCacheKeys = commentIdList.stream()
|
List<Long> localCacheKeys = commentIdList.stream()
|
||||||
@@ -226,7 +225,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
Map<Long, String> missingData = Maps.newHashMap();
|
Map<Long, String> missingData = Maps.newHashMap();
|
||||||
missingKeys.forEach(key -> {
|
missingKeys.forEach(key -> {
|
||||||
// 记录缓存中不存在的ID
|
// 记录缓存中不存在的ID
|
||||||
localeCacheExpiredCommentIds.add(key);
|
localCacheExpiredCommentIds.add(key);
|
||||||
// 不存在的评论详情,对其Value设置为空字符串
|
// 不存在的评论详情,对其Value设置为空字符串
|
||||||
missingData.put(key, Strings.EMPTY);
|
missingData.put(key, Strings.EMPTY);
|
||||||
});
|
});
|
||||||
@@ -234,7 +233,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 如果localCacheExpiredCommentIds的大小不等于commentIdList的大小,说明本地缓存中有数据
|
// 如果localCacheExpiredCommentIds的大小不等于commentIdList的大小,说明本地缓存中有数据
|
||||||
if (CollUtil.size(localeCacheExpiredCommentIds) != commentIdList.size()) {
|
if (CollUtil.size(localCacheExpiredCommentIds) != commentIdList.size()) {
|
||||||
// 将本地缓存中的评论详情Json转为实体类添加到VO返参集合中
|
// 将本地缓存中的评论详情Json转为实体类添加到VO返参集合中
|
||||||
for (String value : commentIdAndDetailJsonMap.values()) {
|
for (String value : commentIdAndDetailJsonMap.values()) {
|
||||||
if (StringUtils.isBlank(value)) continue;
|
if (StringUtils.isBlank(value)) continue;
|
||||||
@@ -244,12 +243,16 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果localCacheExpiredCommentIds大小为0,说明评论详情全在本地缓存中,直接响应返参
|
// 如果localCacheExpiredCommentIds大小为0,说明评论详情全在本地缓存中,直接响应返参
|
||||||
if (CollUtil.size(localeCacheExpiredCommentIds) == 0) {
|
if (CollUtil.size(localCacheExpiredCommentIds) == 0) {
|
||||||
|
// 计数数据需要从 Redis 中查
|
||||||
|
if (CollUtil.isNotEmpty(commentRspVOS)) {
|
||||||
|
setCommentCountData(commentRspVOS, localCacheExpiredCommentIds);
|
||||||
|
}
|
||||||
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 MGET 批量查询评论详情的 Key 集合
|
// 构建 MGET 批量查询评论详情的 Key 集合
|
||||||
List<String> commentIdKeys = localeCacheExpiredCommentIds.stream()
|
List<String> commentIdKeys = localCacheExpiredCommentIds.stream()
|
||||||
.map(RedisKeyConstants::buildCommentDetailKey)
|
.map(RedisKeyConstants::buildCommentDetailKey)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -300,6 +303,450 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResponse<FindChildCommentItemRspVO> findChildCommentPageList(FindChildCommentPageListReqVO findChildCommentPageListReqVO) {
|
||||||
|
// 父评论 ID
|
||||||
|
Long parentCommentId = findChildCommentPageListReqVO.getParentCommentId();
|
||||||
|
// 当前页码
|
||||||
|
Integer pageNo = findChildCommentPageListReqVO.getPageNo();
|
||||||
|
// 每页展示的二级评论数 (小红书 APP 中是一次查询 6 条)
|
||||||
|
long pageSize = 6;
|
||||||
|
|
||||||
|
// 先从缓存中查询
|
||||||
|
String countCommentKey = RedisKeyConstants.buildCountCommentKey(parentCommentId);
|
||||||
|
// 子评论总数
|
||||||
|
Number redisCount = (Number) redisTemplate.opsForHash()
|
||||||
|
.get(countCommentKey, RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL);
|
||||||
|
long count = Objects.isNull(redisCount) ? 0L : redisCount.longValue();
|
||||||
|
|
||||||
|
// 若缓存不存在,走数据库查询
|
||||||
|
if (Objects.isNull(redisCount)) {
|
||||||
|
// 查询一级评论下子评论的总数 (直接查询 t_comment 表的 child_comment_total 字段,提升查询性能, 避免 count(*))
|
||||||
|
Long dbCount = commentDOMapper.selectChildCommentTotalById(parentCommentId);
|
||||||
|
|
||||||
|
// 若数据库中也不存在,则抛出业务异常
|
||||||
|
if (Objects.isNull(dbCount)) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.PARENT_COMMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
count = dbCount;
|
||||||
|
// 异步将子评论总数同步到 Redis 中
|
||||||
|
threadPoolTaskExecutor.execute(() -> syncCommentCount2Redis(countCommentKey, dbCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若子评论总数为 0,直接返参
|
||||||
|
if (count == 0) {
|
||||||
|
return PageResponse.success(null, pageNo, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页返回参数VO
|
||||||
|
List<FindChildCommentItemRspVO> childCommentRspVOS = Lists.newArrayList();
|
||||||
|
|
||||||
|
// 计算分页查询的偏移量offset(需要加1,因为最早回复的二级评论已经被展示了)
|
||||||
|
long offset = PageResponse.getOffset(pageNo, pageSize) + 1;
|
||||||
|
|
||||||
|
// 子评论分页缓存使用Zset+String实现
|
||||||
|
// 构建子评论ZSETKey
|
||||||
|
String childCommentZsetKey = RedisKeyConstants.buildChildCommentListKey(parentCommentId);
|
||||||
|
// 先判断ZSET是否存在
|
||||||
|
boolean hasKey = redisTemplate.hasKey(childCommentZsetKey);
|
||||||
|
// 若不存在
|
||||||
|
if (!hasKey) {
|
||||||
|
// 异步将子评论同步到 Redis 中(最多同步 6*10 条)
|
||||||
|
threadPoolTaskExecutor.execute(() -> syncChildComments2Redis(parentCommentId, childCommentZsetKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若子评论 ZSET 缓存存在, 并且查询的是前 10 页的子评论
|
||||||
|
if (hasKey && offset < 6 * 10) {
|
||||||
|
// 使用ZRevRange获取某个一级评论下的子评论,按回复时间升序排序
|
||||||
|
Set<Object> childCommentIds = redisTemplate
|
||||||
|
.opsForZSet().rangeByScore(childCommentZsetKey, 0, Double.MAX_VALUE, offset, pageSize);
|
||||||
|
|
||||||
|
// 若结果不为空
|
||||||
|
if (CollUtil.isNotEmpty(childCommentIds)) {
|
||||||
|
// Set转List
|
||||||
|
List<Object> childCommentIdList = Lists.newArrayList(childCommentIds);
|
||||||
|
// 构建 MGET 批量查询子评论详情的 Key 集合
|
||||||
|
List<String> commentIdKeys = childCommentIds.stream()
|
||||||
|
.map(RedisKeyConstants::buildCommentDetailKey)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// MGET 批量获取评论数据
|
||||||
|
List<Object> commentsJsonList = redisTemplate.opsForValue().multiGet(commentIdKeys);
|
||||||
|
|
||||||
|
// 可能存在部分评论不在缓存中,已经过期被删除,这些评论 ID 需要提取出来,等会查数据库
|
||||||
|
List<Long> expiredChildCommentIds = Lists.newArrayList();
|
||||||
|
|
||||||
|
if (commentsJsonList != null) {
|
||||||
|
for (int i = 0; i < commentsJsonList.size(); i++) {
|
||||||
|
String commentJson = (String) commentsJsonList.get(i);
|
||||||
|
Long commentId = Long.valueOf(childCommentIdList.get(i).toString());
|
||||||
|
if (Objects.nonNull(commentJson)) {
|
||||||
|
// 缓存中存在的评论 Json,直接转换为 VO 添加到返参集合中
|
||||||
|
FindChildCommentItemRspVO childCommentRspVO = JsonUtils.parseObject(commentJson, FindChildCommentItemRspVO.class);
|
||||||
|
childCommentRspVOS.add(childCommentRspVO);
|
||||||
|
} else {
|
||||||
|
// 评论失效,添加到失效评论列表
|
||||||
|
expiredChildCommentIds.add(commentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于缓存中存在的子评论, 需要再次查询 Hash, 获取其计数数据
|
||||||
|
if (CollUtil.isNotEmpty(childCommentRspVOS)) {
|
||||||
|
setChildCommentCountData(childCommentRspVOS, expiredChildCommentIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于不存在的子评论,需要批量从数据库中查询,并添加到 commentRspVOS 中
|
||||||
|
if (CollUtil.isNotEmpty(expiredChildCommentIds)) {
|
||||||
|
List<CommentDO> commentDOS = commentDOMapper.selectByCommentIds(expiredChildCommentIds);
|
||||||
|
getChildCommentDataAndSync2Redis(commentDOS, childCommentRspVOS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按评论 ID 升序排列(等同于按回复时间升序)
|
||||||
|
childCommentRspVOS = childCommentRspVOS.stream()
|
||||||
|
.sorted(Comparator.comparing(FindChildCommentItemRspVO::getCommentId))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return PageResponse.success(childCommentRspVOS, pageNo, count, pageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询子评论
|
||||||
|
List<CommentDO> childCommentDOS = commentDOMapper.selectChildPageList(parentCommentId, offset, pageSize);
|
||||||
|
|
||||||
|
getChildCommentDataAndSync2Redis(childCommentDOS, childCommentRspVOS);
|
||||||
|
|
||||||
|
return PageResponse.success(childCommentRspVOS, pageNo, count, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置子评论计数数据
|
||||||
|
*
|
||||||
|
* @param commentRspVOS 子评论VO
|
||||||
|
* @param expiredCommentIds 失效的子评论ID
|
||||||
|
*/
|
||||||
|
private void setChildCommentCountData(List<FindChildCommentItemRspVO> commentRspVOS, List<Long> expiredCommentIds) {
|
||||||
|
// 准备从评论 Hash 中查询计数 (被点赞数)
|
||||||
|
// 缓存中存在的子评论 ID
|
||||||
|
List<Long> notExpiredCommentIds = Lists.newArrayList();
|
||||||
|
|
||||||
|
// 遍历从缓存中解析出的 VO 集合,提取二级评论 ID
|
||||||
|
commentRspVOS.forEach(commentRspVO -> {
|
||||||
|
Long childCommentId = commentRspVO.getCommentId();
|
||||||
|
notExpiredCommentIds.add(childCommentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 从 Redis 中查询评论计数 Hash 数据
|
||||||
|
Map<Long, Map<Object, Object>> commentIdAndCountMap = getCommentCountDataAndSync2RedisHash(notExpiredCommentIds);
|
||||||
|
|
||||||
|
// 遍历 VO, 设置对应子评论的点赞数
|
||||||
|
for (FindChildCommentItemRspVO commentRspVO : commentRspVOS) {
|
||||||
|
// 评论 ID
|
||||||
|
Long commentId = commentRspVO.getCommentId();
|
||||||
|
|
||||||
|
// 若当前这条评论是从数据库中查询出来的, 则无需设置点赞数,以数据库查询出来的为主
|
||||||
|
if (CollUtil.isNotEmpty(expiredCommentIds)
|
||||||
|
&& expiredCommentIds.contains(commentId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置子评论的点赞数
|
||||||
|
Map<Object, Object> hash = commentIdAndCountMap.get(commentId);
|
||||||
|
if (CollUtil.isNotEmpty(hash)) {
|
||||||
|
Long likeTotal = Long.valueOf(hash.get(RedisKeyConstants.FIELD_LIKE_TOTAL).toString());
|
||||||
|
commentRspVO.setLikeTotal(likeTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论计数数据,并同步到 Redis 中
|
||||||
|
*
|
||||||
|
* @param notExpiredCommentIds 缓存中存在的评论 ID
|
||||||
|
* @return 评论计数数据
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<Long, Map<Object, Object>> getCommentCountDataAndSync2RedisHash(List<Long> notExpiredCommentIds) {
|
||||||
|
// 已失效的 Hash 评论 ID
|
||||||
|
List<Long> expiredCountCommentIds = Lists.newArrayList();
|
||||||
|
// 构建需要查询的 Hash Key 集合
|
||||||
|
List<String> commentCountKeys = notExpiredCommentIds.stream()
|
||||||
|
.map(RedisKeyConstants::buildCountCommentKey).toList();
|
||||||
|
|
||||||
|
// 使用 RedisTemplate 执行管道批量操作
|
||||||
|
List<Object> results = redisTemplate.executePipelined(new SessionCallback<>() {
|
||||||
|
@Override
|
||||||
|
public Object execute(@NonNull RedisOperations operations) {
|
||||||
|
// 遍历需要查询的评论计数的 Hash 键集合
|
||||||
|
commentCountKeys.forEach(key ->
|
||||||
|
// 在管道中执行 Redis 的 hash.entries 操作
|
||||||
|
// 此操作会获取指定 Hash 键中所有的字段和值
|
||||||
|
operations.opsForHash().entries(key));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 评论 ID - 计数数据字典
|
||||||
|
Map<Long, Map<Object, Object>> commentIdAndCountMap = Maps.newHashMap();
|
||||||
|
// 遍历未过期的评论 ID 集合
|
||||||
|
for (int i = 0; i < notExpiredCommentIds.size(); i++) {
|
||||||
|
// 当前评论 ID
|
||||||
|
Long currCommentId = Long.valueOf(notExpiredCommentIds.get(i).toString());
|
||||||
|
// 从缓存查询结果中,获取对应 Hash
|
||||||
|
Map<Object, Object> hash = (Map<Object, Object>) results.get(i);
|
||||||
|
// 若 Hash 结果为空,说明缓存中不存在,添加到 expiredCountCommentIds 中,保存一下
|
||||||
|
if (CollUtil.isEmpty(hash)) {
|
||||||
|
expiredCountCommentIds.add(currCommentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 若存在,则将数据添加到 commentIdAndCountMap 中,方便后续读取
|
||||||
|
commentIdAndCountMap.put(currCommentId, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若已过期的计数评论 ID 集合大于 0,说明部分计数数据不在 Redis 缓存中
|
||||||
|
// 需要查询数据库,并将这部分的评论计数 Hash 同步到 Redis 中
|
||||||
|
if (CollUtil.size(expiredCountCommentIds) > 0) {
|
||||||
|
// 查询数据库
|
||||||
|
List<CommentDO> commentDOS = commentDOMapper.selectCommentCountByIds(expiredCountCommentIds);
|
||||||
|
|
||||||
|
commentDOS.forEach(commentDO -> {
|
||||||
|
Integer level = commentDO.getLevel();
|
||||||
|
Map<Object, Object> map = Maps.newHashMap();
|
||||||
|
map.put(RedisKeyConstants.FIELD_LIKE_TOTAL, commentDO.getLikeTotal());
|
||||||
|
// 只有一级评论需要统计子评论总数
|
||||||
|
if (Objects.equals(level, CommentLevelEnum.ONE.getCode())) {
|
||||||
|
map.put(RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, commentDO.getChildCommentTotal());
|
||||||
|
}
|
||||||
|
// 统一添加到 commentIdAndCountMap 字典中,方便后续查询
|
||||||
|
commentIdAndCountMap.put(commentDO.getId(), map);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 异步同步到 Redis 中
|
||||||
|
threadPoolTaskExecutor.execute(() -> redisTemplate.executePipelined(new SessionCallback<>() {
|
||||||
|
@Override
|
||||||
|
public Object execute(@NonNull RedisOperations operations) {
|
||||||
|
commentDOS.forEach(commentDO -> {
|
||||||
|
// 构建 Hash Key
|
||||||
|
String key = RedisKeyConstants.buildCountCommentKey(commentDO.getId());
|
||||||
|
// 评论级别
|
||||||
|
Integer level = commentDO.getLevel();
|
||||||
|
// 设置 Field 数据
|
||||||
|
Map<String, Long> fieldsMap = Objects.equals(level, CommentLevelEnum.ONE.getCode()) ?
|
||||||
|
Map.of(RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, commentDO.getChildCommentTotal(),
|
||||||
|
RedisKeyConstants.FIELD_LIKE_TOTAL, commentDO.getLikeTotal()) : Map.of(RedisKeyConstants.FIELD_LIKE_TOTAL, commentDO.getLikeTotal());
|
||||||
|
// 添加 Hash 数据
|
||||||
|
operations.opsForHash().putAll(key, fieldsMap);
|
||||||
|
|
||||||
|
// 设置随机过期时间 (5小时以内)
|
||||||
|
long expireTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
|
||||||
|
operations.expire(key, expireTime, TimeUnit.SECONDS);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return commentIdAndCountMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取子评论数据
|
||||||
|
*
|
||||||
|
* @param childCommentDOS 子评论DO
|
||||||
|
* @param childCommentRspVOS 子评论VO
|
||||||
|
*/
|
||||||
|
private void getChildCommentDataAndSync2Redis(List<CommentDO> childCommentDOS, List<FindChildCommentItemRspVO> childCommentRspVOS) {
|
||||||
|
// 调用KV服务需要的入参
|
||||||
|
List<FindCommentContentReqDTO> findCommentContentReqDTOS = Lists.newArrayList();
|
||||||
|
// 调用用户服务需要的入参
|
||||||
|
Set<Long> userIds = Sets.newHashSet();
|
||||||
|
|
||||||
|
// 归属的笔记ID
|
||||||
|
Long noteId = null;
|
||||||
|
|
||||||
|
// 循环提取RPC调用所需要的入参数据
|
||||||
|
for (CommentDO childCommentDO : childCommentDOS) {
|
||||||
|
noteId = childCommentDO.getNoteId();
|
||||||
|
// 构建调用KV服务批量查询评论内容的入参
|
||||||
|
Boolean isContentEmpty = childCommentDO.getIsContentEmpty();
|
||||||
|
if (!isContentEmpty) {
|
||||||
|
FindCommentContentReqDTO findCommentContentReqDTO = FindCommentContentReqDTO.builder()
|
||||||
|
.contentId(childCommentDO.getContentUuid())
|
||||||
|
.yearMonth(DateConstants.DATE_FORMAT_Y_M.format(childCommentDO.getCreateTime()))
|
||||||
|
.build();
|
||||||
|
findCommentContentReqDTOS.add(findCommentContentReqDTO);
|
||||||
|
}
|
||||||
|
// 构建调用用户服务批量查询用户信息入参
|
||||||
|
userIds.add(childCommentDO.getUserId());
|
||||||
|
|
||||||
|
Long parentId = childCommentDO.getParentId();
|
||||||
|
Long replyCommentId = childCommentDO.getReplyCommentId();
|
||||||
|
// 若当前评论的 replyCommentId 不等于 parentId,则前端需要展示回复的哪个用户,如 “回复 Hanserwei:”
|
||||||
|
if (!Objects.equals(parentId, replyCommentId)) {
|
||||||
|
userIds.add(childCommentDO.getReplyUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPC 调用KV服务批量查询评论内容
|
||||||
|
List<FindCommentContentRspDTO> findCommentContentRspDTOS = keyValueRpcService.batchFindCommentContent(noteId, findCommentContentReqDTOS);
|
||||||
|
|
||||||
|
// DTO转Map方便后续拼接数据
|
||||||
|
Map<String, String> commentUuidAndContentMap = null;
|
||||||
|
if (CollUtil.isNotEmpty(findCommentContentRspDTOS)) {
|
||||||
|
commentUuidAndContentMap = findCommentContentRspDTOS.stream()
|
||||||
|
.collect(Collectors.toMap(FindCommentContentRspDTO::getContentId, FindCommentContentRspDTO::getContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPC 调用用户服务批量查询用户信息
|
||||||
|
List<FindUserByIdRspDTO> findUserByIdRspDTOS = userRpcService.findByIds(userIds.stream().toList());
|
||||||
|
|
||||||
|
// DTO转Map方便后续拼接数据
|
||||||
|
Map<Long, FindUserByIdRspDTO> userIdAndDTOMap = null;
|
||||||
|
if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) {
|
||||||
|
userIdAndDTOMap = findUserByIdRspDTOS.stream()
|
||||||
|
.collect(Collectors.toMap(FindUserByIdRspDTO::getId, e -> e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO 转 VO
|
||||||
|
for (CommentDO childCommentDO : childCommentDOS) {
|
||||||
|
// 构建 VO 实体类
|
||||||
|
Long userId = childCommentDO.getUserId();
|
||||||
|
FindChildCommentItemRspVO childCommentRspVO = FindChildCommentItemRspVO.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.commentId(childCommentDO.getId())
|
||||||
|
.imageUrl(childCommentDO.getImageUrl())
|
||||||
|
.createTime(DateUtils.formatRelativeTime(childCommentDO.getCreateTime()))
|
||||||
|
.likeTotal(childCommentDO.getLikeTotal())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 填充用户信息(包括评论发布者、回复的用户)
|
||||||
|
if (CollUtil.isNotEmpty(userIdAndDTOMap)) {
|
||||||
|
FindUserByIdRspDTO findUserByIdRspDTO = userIdAndDTOMap.get(userId);
|
||||||
|
// 评论发布者用户信息(头像、昵称)
|
||||||
|
if (Objects.nonNull(findUserByIdRspDTO)) {
|
||||||
|
childCommentRspVO.setAvatar(findUserByIdRspDTO.getAvatar());
|
||||||
|
childCommentRspVO.setNickname(findUserByIdRspDTO.getNickName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论回复的哪个
|
||||||
|
Long replyCommentId = childCommentDO.getReplyCommentId();
|
||||||
|
Long parentId = childCommentDO.getParentId();
|
||||||
|
|
||||||
|
if (Objects.nonNull(replyCommentId)
|
||||||
|
&& !Objects.equals(replyCommentId, parentId)) {
|
||||||
|
Long replyUserId = childCommentDO.getReplyUserId();
|
||||||
|
FindUserByIdRspDTO replyUser = userIdAndDTOMap.get(replyUserId);
|
||||||
|
childCommentRspVO.setReplyUserName(replyUser.getNickName());
|
||||||
|
childCommentRspVO.setReplyUserId(replyUser.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论内容
|
||||||
|
if (CollUtil.isNotEmpty(commentUuidAndContentMap)) {
|
||||||
|
String contentUuid = childCommentDO.getContentUuid();
|
||||||
|
if (StringUtils.isNotBlank(contentUuid)) {
|
||||||
|
childCommentRspVO.setContent(commentUuidAndContentMap.get(contentUuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
childCommentRspVOS.add(childCommentRspVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步将笔记详情,同步到 Redis 中
|
||||||
|
threadPoolTaskExecutor.execute(() -> {
|
||||||
|
// 准备批量写入的数据
|
||||||
|
Map<String, String> data = Maps.newHashMap();
|
||||||
|
childCommentRspVOS.forEach(commentRspVO -> {
|
||||||
|
// 评论 ID
|
||||||
|
Long commentId = commentRspVO.getCommentId();
|
||||||
|
// 构建 Key
|
||||||
|
String key = RedisKeyConstants.buildCommentDetailKey(commentId);
|
||||||
|
data.put(key, JsonUtils.toJsonString(commentRspVO));
|
||||||
|
});
|
||||||
|
|
||||||
|
batchAddCommentDetailJson2Redis(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加评论详情 Json 到 Redis 中
|
||||||
|
*
|
||||||
|
* @param data 批量写入的数据
|
||||||
|
*/
|
||||||
|
private void batchAddCommentDetailJson2Redis(Map<String, String> data) {
|
||||||
|
// 使用 Redis Pipeline 提升写入性能
|
||||||
|
redisTemplate.executePipelined((RedisCallback<?>) (connection) -> {
|
||||||
|
for (Map.Entry<String, String> entry : data.entrySet()) {
|
||||||
|
// 将 Java 对象序列化为 JSON 字符串
|
||||||
|
String jsonStr = JsonUtils.toJsonString(entry.getValue());
|
||||||
|
|
||||||
|
// 随机生成过期时间 (5小时以内)
|
||||||
|
int randomExpire = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
|
||||||
|
|
||||||
|
// 批量写入并设置过期时间
|
||||||
|
connection.stringCommands().setEx(
|
||||||
|
Objects.requireNonNull(redisTemplate.getStringSerializer().serialize(entry.getKey())),
|
||||||
|
randomExpire,
|
||||||
|
Objects.requireNonNull(redisTemplate.getStringSerializer().serialize(jsonStr))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步子评论到 Redis 中
|
||||||
|
*
|
||||||
|
* @param parentCommentId 父评论ID
|
||||||
|
* @param childCommentZSetKey 子评论ZSet Key
|
||||||
|
*/
|
||||||
|
private void syncChildComments2Redis(Long parentCommentId, String childCommentZSetKey) {
|
||||||
|
List<CommentDO> childCommentDOS = commentDOMapper.selectChildCommentsByParentIdAndLimit(parentCommentId, 6 * 10);
|
||||||
|
if (CollUtil.isNotEmpty(childCommentDOS)) {
|
||||||
|
// 使用 Redis Pipeline 提升写入性能
|
||||||
|
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
||||||
|
ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
|
||||||
|
|
||||||
|
// 遍历子评论数据并批量写入 ZSet
|
||||||
|
for (CommentDO childCommentDO : childCommentDOS) {
|
||||||
|
Long commentId = childCommentDO.getId();
|
||||||
|
// create_time 转时间戳
|
||||||
|
long commentTimestamp = DateUtils.localDateTime2Timestamp(childCommentDO.getCreateTime());
|
||||||
|
zSetOps.add(childCommentZSetKey, commentId, commentTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置随机过期时间,(保底1小时 + 随机时间),单位:秒
|
||||||
|
int randomExpiryTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60); // 5小时以内
|
||||||
|
redisTemplate.expire(childCommentZSetKey, randomExpiryTime, TimeUnit.SECONDS);
|
||||||
|
return null; // 无返回值
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步评论数量到 Redis 中
|
||||||
|
*
|
||||||
|
* @param countCommentKey 评论数量缓存的键值
|
||||||
|
* @param dbCount 数据库中的评论数量
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void syncCommentCount2Redis(String countCommentKey, Long dbCount) {
|
||||||
|
redisTemplate.executePipelined(new SessionCallback<Void>() {
|
||||||
|
@Override
|
||||||
|
public <K, V> Void execute(@NonNull RedisOperations<K, V> operations) throws DataAccessException {
|
||||||
|
// 这里用强制类型转换指定类型
|
||||||
|
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||||
|
|
||||||
|
ops.opsForHash()
|
||||||
|
.put(countCommentKey, RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, dbCount);
|
||||||
|
|
||||||
|
long expireTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
|
||||||
|
ops.expire(countCommentKey, expireTime, TimeUnit.SECONDS);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步评论详情到本地缓存中
|
* 同步评论详情到本地缓存中
|
||||||
*
|
*
|
||||||
@@ -445,25 +892,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
data.put(key, JsonUtils.toJsonString(commentRspVO));
|
data.put(key, JsonUtils.toJsonString(commentRspVO));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用 Redis Pipeline 提升写入性能
|
batchAddCommentDetailJson2Redis(data);
|
||||||
redisTemplate.executePipelined((RedisCallback<?>) (connection) -> {
|
|
||||||
for (Map.Entry<String, String> entry : data.entrySet()) {
|
|
||||||
// 将 Java 对象序列化为 JSON 字符串
|
|
||||||
String jsonStr = JsonUtils.toJsonString(entry.getValue());
|
|
||||||
|
|
||||||
// 随机生成过期时间 (5小时以内)
|
|
||||||
int randomExpire = RandomUtil.randomInt(5 * 60 * 60);
|
|
||||||
|
|
||||||
|
|
||||||
// 批量写入并设置过期时间
|
|
||||||
connection.stringCommands().setEx(
|
|
||||||
Objects.requireNonNull(redisTemplate.getStringSerializer().serialize(entry.getKey())),
|
|
||||||
randomExpire,
|
|
||||||
Objects.requireNonNull(redisTemplate.getStringSerializer().serialize(jsonStr))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,4 +938,64 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置评论 VO 的计数
|
||||||
|
*
|
||||||
|
* @param commentRspVOS 返参 VO 集合
|
||||||
|
* @param expiredCommentIds 缓存中已失效的评论 ID 集合
|
||||||
|
*/
|
||||||
|
private void setCommentCountData(List<FindCommentItemRspVO> commentRspVOS,
|
||||||
|
List<Long> expiredCommentIds) {
|
||||||
|
// 准备从评论 Hash 中查询计数 (子评论总数、被点赞数)
|
||||||
|
// 缓存中存在的评论 ID
|
||||||
|
List<Long> notExpiredCommentIds = Lists.newArrayList();
|
||||||
|
|
||||||
|
// 遍历从缓存中解析出的 VO 集合,提取一级、二级评论 ID
|
||||||
|
commentRspVOS.forEach(commentRspVO -> {
|
||||||
|
Long oneLevelCommentId = commentRspVO.getCommentId();
|
||||||
|
notExpiredCommentIds.add(oneLevelCommentId);
|
||||||
|
FindCommentItemRspVO firstCommentVO = commentRspVO.getFirstReplyComment();
|
||||||
|
if (Objects.nonNull(firstCommentVO)) {
|
||||||
|
notExpiredCommentIds.add(firstCommentVO.getCommentId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 已失效的 Hash 评论 ID
|
||||||
|
Map<Long, Map<Object, Object>> commentIdAndCountMap = getCommentCountDataAndSync2RedisHash(notExpiredCommentIds);
|
||||||
|
|
||||||
|
// 遍历 VO, 设置对应评论的二级评论数、点赞数
|
||||||
|
for (FindCommentItemRspVO commentRspVO : commentRspVOS) {
|
||||||
|
// 评论 ID
|
||||||
|
Long commentId = commentRspVO.getCommentId();
|
||||||
|
|
||||||
|
// 若当前这条评论是从数据库中查询出来的, 则无需设置二级评论数、点赞数,以数据库查询出来的为主
|
||||||
|
if (CollUtil.isNotEmpty(expiredCommentIds)
|
||||||
|
&& expiredCommentIds.contains(commentId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置一级评论的子评论总数、点赞数
|
||||||
|
Map<Object, Object> hash = commentIdAndCountMap.get(commentId);
|
||||||
|
if (CollUtil.isNotEmpty(hash)) {
|
||||||
|
Object likeTotalObj = hash.get(RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL);
|
||||||
|
Long childCommentTotal = Objects.isNull(likeTotalObj) ? 0 : Long.parseLong(likeTotalObj.toString());
|
||||||
|
Long likeTotal = Long.valueOf(hash.get(RedisKeyConstants.FIELD_LIKE_TOTAL).toString());
|
||||||
|
commentRspVO.setChildCommentTotal(childCommentTotal);
|
||||||
|
commentRspVO.setLikeTotal(likeTotal);
|
||||||
|
// 最初回复的二级评论
|
||||||
|
FindCommentItemRspVO firstCommentVO = commentRspVO.getFirstReplyComment();
|
||||||
|
if (Objects.nonNull(firstCommentVO)) {
|
||||||
|
Long firstCommentId = firstCommentVO.getCommentId();
|
||||||
|
Map<Object, Object> firstCommentHash = commentIdAndCountMap.get(firstCommentId);
|
||||||
|
if (CollUtil.isNotEmpty(firstCommentHash)) {
|
||||||
|
Long firstCommentLikeTotal = Long.valueOf(firstCommentHash.get(RedisKeyConstants.FIELD_LIKE_TOTAL).toString());
|
||||||
|
firstCommentVO.setLikeTotal(firstCommentLikeTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,4 +154,51 @@
|
|||||||
order by heat desc
|
order by heat desc
|
||||||
limit 500
|
limit 500
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectChildCommentTotalById" resultType="long">
|
||||||
|
select child_comment_total
|
||||||
|
from t_comment
|
||||||
|
where id = #{commentId}
|
||||||
|
and level = 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectChildPageList" resultMap="BaseResultMap" parameterType="map">
|
||||||
|
select id,
|
||||||
|
user_id,
|
||||||
|
note_id,
|
||||||
|
content_uuid,
|
||||||
|
is_content_empty,
|
||||||
|
image_url,
|
||||||
|
like_total,
|
||||||
|
create_time,
|
||||||
|
reply_user_id,
|
||||||
|
parent_id,
|
||||||
|
reply_comment_id
|
||||||
|
from t_comment
|
||||||
|
where parent_id = #{parentId}
|
||||||
|
and level = 2
|
||||||
|
order by id
|
||||||
|
limit #{offset}, #{pageSize}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectCommentCountByIds" resultMap="BaseResultMap" parameterType="list">
|
||||||
|
select id,
|
||||||
|
child_comment_total,
|
||||||
|
like_total,
|
||||||
|
level
|
||||||
|
from t_comment
|
||||||
|
where id in
|
||||||
|
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
|
||||||
|
#{commentId}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectChildCommentsByParentIdAndLimit" resultMap="BaseResultMap" parameterType="map">
|
||||||
|
select id, create_time
|
||||||
|
from t_comment
|
||||||
|
where parent_id = #{parentId}
|
||||||
|
and level = 2
|
||||||
|
order by create_time
|
||||||
|
limit #{limit}
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -36,6 +36,25 @@ public class RedisKeyConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String FIELD_COLLECT_TOTAL = "collectTotal";
|
public static final String FIELD_COLLECT_TOTAL = "collectTotal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Field: 子评论总数
|
||||||
|
*/
|
||||||
|
public static final String FIELD_CHILD_COMMENT_TOTAL = "childCommentTotal";
|
||||||
|
/**
|
||||||
|
* 评论维度计数 Key 前缀
|
||||||
|
*/
|
||||||
|
private static final String COUNT_COMMENT_KEY_PREFIX = "count:comment:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建评论维度计数 Key
|
||||||
|
*
|
||||||
|
* @param commentId 评论ID
|
||||||
|
* @return 评论维度计数 Key
|
||||||
|
*/
|
||||||
|
public static String buildCountCommentKey(Long commentId) {
|
||||||
|
return COUNT_COMMENT_KEY_PREFIX + commentId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建用户维度计数 Key
|
* 构建用户维度计数 Key
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.github.phantomthief.collection.BufferTrigger;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||||
import com.hanserwei.hannote.count.biz.constant.MQConstants;
|
import com.hanserwei.hannote.count.biz.constant.MQConstants;
|
||||||
|
import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants;
|
||||||
import com.hanserwei.hannote.count.biz.domain.mapper.CommentDOMapper;
|
import com.hanserwei.hannote.count.biz.domain.mapper.CommentDOMapper;
|
||||||
import com.hanserwei.hannote.count.biz.enums.CommentLevelEnum;
|
import com.hanserwei.hannote.count.biz.enums.CommentLevelEnum;
|
||||||
import com.hanserwei.hannote.count.biz.model.dto.CountPublishCommentMqDTO;
|
import com.hanserwei.hannote.count.biz.model.dto.CountPublishCommentMqDTO;
|
||||||
@@ -15,6 +16,7 @@ import org.apache.rocketmq.client.producer.SendResult;
|
|||||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.support.MessageBuilder;
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -39,6 +41,9 @@ public class CountNoteChildCommentConsumer implements RocketMQListener<String> {
|
|||||||
@Resource
|
@Resource
|
||||||
private CommentDOMapper commentDOMapper;
|
private CommentDOMapper commentDOMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
private final BufferTrigger<String> bufferTrigger = BufferTrigger.<String>batchBlocking()
|
private final BufferTrigger<String> bufferTrigger = BufferTrigger.<String>batchBlocking()
|
||||||
.bufferSize(50000) // 缓存队列的最大容量
|
.bufferSize(50000) // 缓存队列的最大容量
|
||||||
.batchSize(1000) // 一批次最多聚合 1000 条
|
.batchSize(1000) // 一批次最多聚合 1000 条
|
||||||
@@ -82,6 +87,19 @@ public class CountNoteChildCommentConsumer implements RocketMQListener<String> {
|
|||||||
// 评论数
|
// 评论数
|
||||||
int count = CollUtil.size(entry.getValue());
|
int count = CollUtil.size(entry.getValue());
|
||||||
|
|
||||||
|
// 更新 Redis 缓存中的评论计数数据
|
||||||
|
// 构建 Key
|
||||||
|
String commentCountHashKey = RedisKeyConstants.buildCountCommentKey(parentId);
|
||||||
|
// 判断 Hash 是否存在
|
||||||
|
boolean hasKey = redisTemplate.hasKey(commentCountHashKey);
|
||||||
|
|
||||||
|
// 若 Hash 存在,则更新子评论总数
|
||||||
|
if (hasKey) {
|
||||||
|
// 累加
|
||||||
|
redisTemplate.opsForHash()
|
||||||
|
.increment(commentCountHashKey, RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, count);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新一级评论的下级评论总数,进行累加操作
|
// 更新一级评论的下级评论总数,进行累加操作
|
||||||
commentDOMapper.updateChildCommentTotal(parentId, count);
|
commentDOMapper.updateChildCommentTotal(parentId, count);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,9 +298,9 @@ Authorization: Bearer {{token}}
|
|||||||
|
|
||||||
{
|
{
|
||||||
"noteId": 1862481582414102549,
|
"noteId": 1862481582414102549,
|
||||||
"content": "这是一条测试同步Redis并更新热度的评论",
|
"content": "这是一条测试同步Redis更新计数的评论",
|
||||||
"imageUrl": "https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg",
|
"imageUrl": "https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg",
|
||||||
"replyCommentId": 8001
|
"replyCommentId": 4002
|
||||||
}
|
}
|
||||||
|
|
||||||
### 批量添加评论
|
### 批量添加评论
|
||||||
@@ -356,3 +356,13 @@ Content-Type: application/json
|
|||||||
"noteId": 1862481582414102549,
|
"noteId": 1862481582414102549,
|
||||||
"pageNo": 1
|
"pageNo": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### 分页查询子评论
|
||||||
|
POST http://localhost:8000/comment/comment/child/list
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"parentCommentId": 4002,
|
||||||
|
"pageNo": 1
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user